Skip to content

Commit 81bc85e

Browse files
committed
Updated WebSocket reading logic to eliminate large arrays allocation (#464)
1 parent 2c86a71 commit 81bc85e

File tree

5 files changed

+42
-28
lines changed

5 files changed

+42
-28
lines changed

Packages.props

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<FAKEVersion>6.*</FAKEVersion>
1010
</PropertyGroup>
1111
<ItemGroup Label="Common">
12+
<PackageReference Update="Collections.Pooled" Version="1.0.*" />
1213
<PackageReference Update="FParsec" Version="1.1.1" />
1314
<PackageReference Include="FSharp.Core" Version="$(FSharpCoreVersion)">
1415
<ExcludeAssets>contentFiles</ExcludeAssets>

src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
</ItemGroup>
3737

3838
<ItemGroup>
39+
<PackageReference Include="Collections.Pooled" />
3940
<PackageReference Include="FsToolkit.ErrorHandling" />
4041
<PackageReference Include="FsToolkit.ErrorHandling.TaskResult" />
4142
<PackageReference Include="Giraffe" />

src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ type IGraphQLOptions =
2121
type GraphQLOptions<'Root> = {
2222
SchemaExecutor : Executor<'Root>
2323
RootFactory : HttpContext -> 'Root
24+
/// The minimum rented array size to read a message from WebSocket
25+
ReadBufferSize : int
2426
SerializerOptions : JsonSerializerOptions
2527
WebsocketOptions : GraphQLTransportWSOptions
2628
} with

src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs

+36-27
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
namespace FSharp.Data.GraphQL.Server.AspNetCore
22

33
open System
4+
open System.Buffers
45
open System.Collections.Generic
6+
open System.Diagnostics
7+
open System.Linq
58
open System.Net.WebSockets
69
open System.Text.Json
710
open System.Text.Json.Serialization
@@ -11,6 +14,8 @@ open Microsoft.AspNetCore.Http
1114
open Microsoft.Extensions.Hosting
1215
open Microsoft.Extensions.Logging
1316
open Microsoft.Extensions.Options
17+
18+
open Collections.Pooled
1419
open FsToolkit.ErrorHandling
1520

1621
open FSharp.Data.GraphQL
@@ -47,9 +52,9 @@ type GraphQLWebSocketMiddleware<'Root>
4752
static let invalidJsonInClientMessageError =
4853
Result.Error <| InvalidMessage (4400, "Invalid json in client message")
4954

50-
let deserializeClientMessage (serializerOptions : JsonSerializerOptions) (msg : string) = taskResult {
55+
let deserializeClientMessage (serializerOptions : JsonSerializerOptions) (msg : IReadOnlyPooledList<byte>) = taskResult {
5156
try
52-
return JsonSerializer.Deserialize<ClientMessage> (msg, serializerOptions)
57+
return JsonSerializer.Deserialize<ClientMessage> (msg.Span, serializerOptions)
5358
with
5459
| :? InvalidWebsocketMessageException as ex ->
5560
logger.LogError(ex, "Invalid websocket message:\n{payload}", msg)
@@ -75,31 +80,35 @@ type GraphQLWebSocketMiddleware<'Root>
7580
&& not (theSocket.State = WebSocketState.Closed)
7681

7782
let receiveMessageViaSocket (cancellationToken : CancellationToken) (serializerOptions : JsonSerializerOptions) (socket : WebSocket) = taskResult {
78-
let buffer = Array.zeroCreate 4096
79-
let completeMessage = new List<byte> ()
80-
let mutable segmentResponse : WebSocketReceiveResult = null
81-
while (not cancellationToken.IsCancellationRequested)
82-
&& socket |> isSocketOpen
83-
&& ((segmentResponse = null)
84-
|| (not segmentResponse.EndOfMessage)) do
85-
try
86-
let! r = socket.ReceiveAsync (new ArraySegment<byte> (buffer), cancellationToken)
87-
segmentResponse <- r
88-
completeMessage.AddRange (new ArraySegment<byte> (buffer, 0, r.Count))
89-
with :? OperationCanceledException ->
90-
()
91-
92-
// TODO: Allocate string only if a debugger is attached
93-
let message =
94-
completeMessage
95-
|> Seq.filter (fun x -> x > 0uy)
96-
|> Array.ofSeq
97-
|> System.Text.Encoding.UTF8.GetString
98-
if String.IsNullOrWhiteSpace message then
99-
return ValueNone
100-
else
101-
let! result = message |> deserializeClientMessage serializerOptions
102-
return ValueSome result
83+
let buffer = ArrayPool.Shared.Rent options.ReadBufferSize
84+
try
85+
let completeMessage = new PooledList<byte> ()
86+
let mutable segmentResponse : WebSocketReceiveResult = null
87+
while (not cancellationToken.IsCancellationRequested)
88+
&& socket |> isSocketOpen
89+
&& ((segmentResponse = null)
90+
|| (not segmentResponse.EndOfMessage)) do
91+
try
92+
let! r = socket.ReceiveAsync (new ArraySegment<byte> (buffer), cancellationToken)
93+
segmentResponse <- r
94+
completeMessage.AddRange (new ArraySegment<byte> (buffer, 0, r.Count))
95+
with :? OperationCanceledException ->
96+
()
97+
98+
if Debugger.IsAttached then
99+
let message =
100+
completeMessage
101+
|> Seq.filter (fun x -> x > 0uy)
102+
|> Array.ofSeq
103+
|> System.Text.Encoding.UTF8.GetString
104+
logger.LogInformation ("-> Request: {request}", message)
105+
if completeMessage.All(fun b -> b = 0uy) then
106+
return ValueNone
107+
else
108+
let! result = deserializeClientMessage serializerOptions completeMessage
109+
return ValueSome result
110+
finally
111+
ArrayPool.Shared.Return buffer
103112
}
104113

105114
let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) : Task = task {

src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ open System.Runtime.CompilerServices
66
open Microsoft.AspNetCore.Builder
77
open Microsoft.AspNetCore.Http
88
open Microsoft.Extensions.DependencyInjection
9-
open FSharp.Data.GraphQL
109
open Microsoft.Extensions.Options
10+
open FSharp.Data.GraphQL
1111

1212
[<AutoOpen; Extension>]
1313
module ServiceCollectionExtensions =
1414

1515
let createStandardOptions executor rootFactory endpointUrl = {
1616
SchemaExecutor = executor
1717
RootFactory = rootFactory
18+
ReadBufferSize = 4096
1819
SerializerOptions = Json.serializerOptions
1920
WebsocketOptions = {
2021
EndpointUrl = endpointUrl

0 commit comments

Comments
 (0)