1
1
namespace App
2
2
3
+ open System
4
+ open System.Collections .Generic
5
+ open System.Threading .Tasks
6
+ open Oxpecker
7
+
3
8
[<AutoOpen>]
4
9
module Common =
5
- open System
6
10
7
11
[<CLIMutable>]
8
12
type Fortune =
@@ -14,8 +18,10 @@ module Common =
14
18
[<Literal>]
15
19
let ConnectionString = " Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;SSL Mode=Disable;Maximum Pool Size=1024;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4;Multiplexing=true;Write Coalescing Buffer Threshold Bytes=1000"
16
20
17
- let fortuneComparer a b =
18
- String.CompareOrdinal( a.message, b.message)
21
+ let FortuneComparer = {
22
+ new IComparer< Fortune> with
23
+ member self.Compare ( a , b ) = String.CompareOrdinal( a.message, b.message)
24
+ }
19
25
20
26
[<RequireQualifiedAccess>]
21
27
module HtmlViews =
@@ -50,63 +56,173 @@ module HtmlViews =
50
56
51
57
[<RequireQualifiedAccess>]
52
58
module HttpHandlers =
53
- open Oxpecker
54
59
open Dapper
55
60
open Npgsql
61
+ open System.Text
62
+ open Microsoft.AspNetCore .Http
56
63
57
64
let private extra =
58
65
{
59
66
id = 0
60
67
message = " Additional fortune added at request time."
61
68
}
62
69
70
+ let private renderFortunes ( ctx : HttpContext ) dbFortunes =
71
+ let augmentedData = [|
72
+ yield ! dbFortunes
73
+ extra
74
+ |]
75
+ Array.Sort( augmentedData, FortuneComparer)
76
+ augmentedData |> HtmlViews.fortunes |> ctx.WriteHtmlView
77
+
63
78
let private fortunes : EndpointHandler =
64
79
fun ctx ->
65
80
task {
66
81
use conn = new NpgsqlConnection( ConnectionString)
67
- let! data = conn.QueryAsync< Fortune>( " SELECT id, message FROM fortune" )
68
- let augmentedData = [|
69
- yield ! data
70
- extra
71
- |]
72
- augmentedData |> Array.sortInPlaceWith fortuneComparer
73
- let view = HtmlViews.fortunes augmentedData
74
- return ! ctx.WriteHtmlView view
82
+ let! dbFortunes = conn.QueryAsync< Fortune>( " SELECT id, message FROM fortune" )
83
+ return ! renderFortunes ctx dbFortunes
84
+ }
85
+
86
+ [<Struct>]
87
+ [<CLIMutable>]
88
+ type World =
89
+ {
90
+ id: int
91
+ randomnumber: int
92
+ }
93
+
94
+ let private readSingleRow ( conn : NpgsqlConnection ) =
95
+ conn.QueryFirstOrDefaultAsync< World>(
96
+ " SELECT id, randomnumber FROM world WHERE id = @Id" ,
97
+ {| Id = Random.Shared.Next( 1 , 10001 ) |}
98
+ )
99
+
100
+ let private parseQueries ( ctx : HttpContext ) =
101
+ match ctx.TryGetRouteValue< string>( " count" ) with
102
+ | Some q ->
103
+ match Int32.TryParse q with
104
+ | true , q when q > 1 -> if q < 500 then q else 500
105
+ | _, _ -> 1
106
+ | _ -> 1
107
+
108
+ let private singleQuery : EndpointHandler =
109
+ fun ctx ->
110
+ task {
111
+ use conn = new NpgsqlConnection( ConnectionString)
112
+ let! result = readSingleRow conn
113
+ return ! ctx.WriteJsonChunked result
75
114
}
76
115
77
- let endpoints : Endpoint [] =
116
+ let private multipleQueries : EndpointHandler =
117
+ fun ctx ->
118
+ let count = parseQueries ctx
119
+ let results = Array.zeroCreate< World> count
120
+ task {
121
+ use conn = new NpgsqlConnection( ConnectionString)
122
+ do ! conn.OpenAsync()
123
+ for i in 0 .. results.Length-1 do
124
+ let! result = readSingleRow conn
125
+ results[ i] <- result
126
+ return ! ctx.WriteJsonChunked results
127
+ }
128
+
129
+ let private maxBatch = 500
130
+ let mutable private queries = Array.zeroCreate ( maxBatch + 1 )
131
+
132
+ let private batchUpdateString batchSize =
133
+ match queries[ batchSize] with
134
+ | null ->
135
+ let lastIndex = batchSize - 1
136
+ let sb = StringBuilder()
137
+ sb.Append( " UPDATE world SET randomNumber = temp.randomNumber FROM (VALUES " ) |> ignore
138
+ for i in 0 .. lastIndex-1 do
139
+ sb.AppendFormat( " (@Id_{0}, @Rn_{0}), " , i) |> ignore
140
+ sb.AppendFormat( " (@Id_{0}, @Rn_{0}) ORDER BY 1) AS temp(id, randomNumber) WHERE temp.id = world.id" , lastIndex) |> ignore
141
+ let result = sb.ToString()
142
+ queries[ batchSize] <- result
143
+ result
144
+ | q -> q
145
+
146
+ let private multipleUpdates : EndpointHandler =
147
+ fun ctx ->
148
+ let count = parseQueries ctx
149
+ let results = Array.zeroCreate< World> count
150
+ task {
151
+ use conn = new NpgsqlConnection( ConnectionString)
152
+ do ! conn.OpenAsync()
153
+ for i in 0 .. results.Length-1 do
154
+ let! result = readSingleRow conn
155
+ results[ i] <- result
156
+ let parameters = Dictionary< string, obj>()
157
+ for i in 0 .. results.Length-1 do
158
+ let randomNumber = Random.Shared.Next( 1 , 10001 )
159
+ parameters[ $" @Rn_{i}" ] <- randomNumber
160
+ parameters[ $" @Id_{i}" ] <- results[ i]. id
161
+ results[ i] <- { results[ i] with randomnumber = randomNumber }
162
+ let! _ = conn.ExecuteAsync( batchUpdateString count, parameters)
163
+ return ! ctx.WriteJsonChunked results
164
+ }
165
+
166
+ let utf8Const ( s : string ): EndpointHandler =
167
+ let result = s |> Encoding.UTF8.GetBytes
168
+ fun ctx ->
169
+ ctx.SetContentType( " text/plain" )
170
+ ctx.WriteBytes( result)
171
+
172
+ let endpoints =
78
173
[|
79
- route " /plaintext" <| text " Hello, World!"
174
+ route " /plaintext" <| utf8Const " Hello, World!"
80
175
route " /json" <| jsonChunked {| message = " Hello, World!" |}
81
176
route " /fortunes" fortunes
177
+ route " /db" singleQuery
178
+ route " /queries/{count?}" multipleQueries
179
+ route " /updates/{count?}" multipleUpdates
82
180
|]
83
181
84
182
85
183
module Main =
184
+ open SpanJson
185
+ open Microsoft.AspNetCore .Http
86
186
open Microsoft.AspNetCore .Builder
87
187
open Microsoft.AspNetCore .Hosting
88
188
open Microsoft.Extensions .DependencyInjection
89
- open Oxpecker
90
189
open Microsoft.Extensions .Hosting
91
190
open Microsoft.Extensions .Logging
191
+ open System.Buffers
192
+
193
+ type SpanJsonSerializer () =
194
+ interface Serializers.IJsonSerializer with
195
+ member this.Serialize ( value , ctx , chunked ) =
196
+ ctx.Response.ContentType <- " application/json"
197
+ if chunked then
198
+ if ctx.Request.Method <> HttpMethods.Head then
199
+ JsonSerializer.Generic.Utf8.SerializeAsync<_>( value, stream = ctx.Response.Body) .AsTask()
200
+ else
201
+ Task.CompletedTask
202
+ else
203
+ task {
204
+ let buffer = JsonSerializer.Generic.Utf8.SerializeToArrayPool<_>( value)
205
+ ctx.Response.Headers.ContentLength <- buffer.Count
206
+ if ctx.Request.Method <> HttpMethods.Head then
207
+ do ! ctx.Response.Body.WriteAsync( buffer)
208
+ ArrayPool< byte>. Shared.Return( buffer.Array)
209
+ else
210
+ return ()
211
+ }
212
+ member this.Deserialize _ =
213
+ failwith " Not implemented"
92
214
93
215
[<EntryPoint>]
94
216
let main args =
95
-
96
217
let builder = WebApplication.CreateBuilder( args)
97
-
98
218
builder.Services
99
219
.AddRouting()
100
- .AddOxpecker() |> ignore
101
-
220
+ .AddOxpecker()
221
+ .AddSingleton< Serializers.IJsonSerializer>( SpanJsonSerializer())
222
+ |> ignore
102
223
builder.Logging.ClearProviders() |> ignore
103
- builder.WebHost.ConfigureKestrel( fun options -> options.AllowSynchronousIO <- true ) |> ignore
104
-
105
224
let app = builder.Build()
106
-
107
225
app.UseRouting()
108
226
.UseOxpecker HttpHandlers.endpoints |> ignore
109
-
110
227
app.Run()
111
-
112
228
0
0 commit comments