Browse Source

[F#/Oxpecker] Improved "update" and "fortunes" benchmarks (#9357)

Vladimir Shchur 9 months ago
parent
commit
c0164be4be

+ 5 - 5
frameworks/FSharp/oxpecker/src/App/App.fsproj

@@ -13,10 +13,10 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Update="FSharp.Core" Version="8.0.400" />
-    <PackageReference Include="Oxpecker" Version="0.14.1" />
-    <PackageReference Include="Oxpecker.ViewEngine" Version="0.14.0" />
-    <PackageReference Include="Npgsql" Version="8.0.3" />
-    <PackageReference Include="SpanJson" Version="4.2.0" />
+    <PackageReference Update="FSharp.Core" Version="9.0.100" />
+    <PackageReference Include="Oxpecker" Version="1.0.0" />
+    <PackageReference Include="Oxpecker.ViewEngine" Version="1.0.0" />
+    <PackageReference Include="Npgsql" Version="9.0.1" />
+    <PackageReference Include="SpanJson" Version="4.2.1" />
   </ItemGroup>
   </ItemGroup>
 </Project>
 </Project>

+ 4 - 2
frameworks/FSharp/oxpecker/src/App/Common.fs

@@ -9,7 +9,7 @@ module Common =
     [<Struct>]
     [<Struct>]
     [<CLIMutable>]
     [<CLIMutable>]
     type JsonMessage = {
     type JsonMessage = {
-        message : string
+        message: string
     }
     }
 
 
     [<CLIMutable>]
     [<CLIMutable>]
@@ -26,7 +26,9 @@ module Common =
     }
     }
 
 
     [<Literal>]
     [<Literal>]
-    let ConnectionString = "Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=1024;NoResetOnClose=true;Enlist=false;Max Auto Prepare=3"
+    let ConnectionString = "Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=1024;NoResetOnClose=true;Enlist=false"
+    [<Literal>]
+    let MultiplexedConnectionString = ConnectionString + ";Max Auto Prepare=3;Multiplexing=true"
 
 
     let FortuneComparer = {
     let FortuneComparer = {
         new IComparer<Fortune> with
         new IComparer<Fortune> with

+ 18 - 13
frameworks/FSharp/oxpecker/src/App/Db.fs

@@ -2,8 +2,7 @@ namespace App
 
 
 open System
 open System
 open System.Data
 open System.Data
-open System.Data.Common
-open System.Text
+open Microsoft.Extensions.ObjectPool
 open Npgsql
 open Npgsql
 
 
 
 
@@ -15,6 +14,7 @@ module Db =
             use db = new NpgsqlConnection(ConnectionString)
             use db = new NpgsqlConnection(ConnectionString)
             use cmd = db.CreateCommand(CommandText = "SELECT id, message FROM fortune")
             use cmd = db.CreateCommand(CommandText = "SELECT id, message FROM fortune")
             do! db.OpenAsync()
             do! db.OpenAsync()
+            do! cmd.PrepareAsync()
             use! rdr = cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection)
             use! rdr = cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection)
             while! rdr.ReadAsync() do
             while! rdr.ReadAsync() do
                 result.Add { id = rdr.GetInt32(0); message = rdr.GetString(1) }
                 result.Add { id = rdr.GetInt32(0); message = rdr.GetString(1) }
@@ -31,7 +31,7 @@ module Db =
             TypedValue = Random.Shared.Next(1, 10001)
             TypedValue = Random.Shared.Next(1, 10001)
         )
         )
         cmd.Parameters.Add(id) |> ignore
         cmd.Parameters.Add(id) |> ignore
-        cmd
+        struct(cmd, id)
 
 
     let private readSingleRow (cmd: NpgsqlCommand) =
     let private readSingleRow (cmd: NpgsqlCommand) =
         task {
         task {
@@ -43,41 +43,46 @@ module Db =
     let loadSingleRow () =
     let loadSingleRow () =
         task {
         task {
             use db = new NpgsqlConnection(ConnectionString)
             use db = new NpgsqlConnection(ConnectionString)
+            let struct(cmd', _) = createReadCommand db
+            use cmd = cmd'
             do! db.OpenAsync()
             do! db.OpenAsync()
-            use cmd = createReadCommand db
+            do! cmd.PrepareAsync()
             return! readSingleRow cmd
             return! readSingleRow cmd
         }
         }
 
 
     let private readMultipleRows (count: int) (conn: NpgsqlConnection) =
     let private readMultipleRows (count: int) (conn: NpgsqlConnection) =
         let result = Array.zeroCreate count
         let result = Array.zeroCreate count
         task {
         task {
-            use cmd = createReadCommand conn
+            let struct(cmd', idParam) = createReadCommand conn
+            use cmd = cmd'
             for i in 0..result.Length-1 do
             for i in 0..result.Length-1 do
-                cmd.Parameters["@Id"].Value <- Random.Shared.Next(1, 10001)
                 let! row = readSingleRow cmd
                 let! row = readSingleRow cmd
                 result[i] <- row
                 result[i] <- row
+                idParam.TypedValue <- Random.Shared.Next(1, 10001)
             return result
             return result
         }
         }
 
 
     let loadMultipleRows (count: int) =
     let loadMultipleRows (count: int) =
         task {
         task {
-            use db = new NpgsqlConnection(ConnectionString)
+            use db = new NpgsqlConnection(MultiplexedConnectionString)
             do! db.OpenAsync()
             do! db.OpenAsync()
             return! readMultipleRows count db
             return! readMultipleRows count db
         }
         }
 
 
     let private maxBatch = 500
     let private maxBatch = 500
     let private queries = Array.zeroCreate (maxBatch + 1)
     let private queries = Array.zeroCreate (maxBatch + 1)
+    let private stringBuilderPool = DefaultObjectPoolProvider().CreateStringBuilderPool()
     let private batchUpdateString batchSize =
     let private batchUpdateString batchSize =
         match queries[batchSize] with
         match queries[batchSize] with
         | null ->
         | null ->
             let lastIndex = batchSize - 1
             let lastIndex = batchSize - 1
-            let sb = StringBuilder()
+            let sb = stringBuilderPool.Get()
             sb.Append("UPDATE world SET randomNumber = temp.randomNumber FROM (VALUES ") |> ignore
             sb.Append("UPDATE world SET randomNumber = temp.randomNumber FROM (VALUES ") |> ignore
             for i in 0..lastIndex-1 do
             for i in 0..lastIndex-1 do
                 sb.AppendFormat("(@Id_{0}, @Rn_{0}), ", i) |> ignore
                 sb.AppendFormat("(@Id_{0}, @Rn_{0}), ", i) |> ignore
             sb.AppendFormat("(@Id_{0}, @Rn_{0}) ORDER BY 1) AS temp(id, randomNumber) WHERE temp.id = world.id", lastIndex) |> ignore
             sb.AppendFormat("(@Id_{0}, @Rn_{0}) ORDER BY 1) AS temp(id, randomNumber) WHERE temp.id = world.id", lastIndex) |> ignore
             let result = sb.ToString()
             let result = sb.ToString()
+            stringBuilderPool.Return(sb)
             queries[batchSize] <- result
             queries[batchSize] <- result
             result
             result
         | q ->
         | q ->
@@ -95,15 +100,15 @@ module Db =
         for i in 0..results.Length-1 do
         for i in 0..results.Length-1 do
             let randomNumber = Random.Shared.Next(1, 10001)
             let randomNumber = Random.Shared.Next(1, 10001)
             let struct(rnParamName, idParamName) = paramNames[i]
             let struct(rnParamName, idParamName) = paramNames[i]
-            let random = NpgsqlParameter<int>(ParameterName = rnParamName, DbType = DbType.Int32, TypedValue = randomNumber)
-            command.Parameters.Add(random) |> ignore
-            let id = NpgsqlParameter<int>(ParameterName = idParamName, DbType = DbType.Int32, TypedValue = results[i].id)
-            command.Parameters.Add(id) |> ignore
+            command.Parameters.Add(NpgsqlParameter<int>(
+                ParameterName = rnParamName, DbType = DbType.Int32, TypedValue = randomNumber)) |> ignore
+            command.Parameters.Add(NpgsqlParameter<int>(
+                ParameterName = idParamName, DbType = DbType.Int32, TypedValue = results[i].id)) |> ignore
             results[i] <- { results[i] with randomnumber = randomNumber }
             results[i] <- { results[i] with randomnumber = randomNumber }
 
 
     let doMultipleUpdates (count: int) =
     let doMultipleUpdates (count: int) =
         task {
         task {
-            use conn = new NpgsqlConnection(ConnectionString)
+            use conn = new NpgsqlConnection(MultiplexedConnectionString)
             do! conn.OpenAsync()
             do! conn.OpenAsync()
             let! results = readMultipleRows count conn
             let! results = readMultipleRows count conn
             use cmd = conn.CreateCommand(CommandText = batchUpdateString count)
             use cmd = conn.CreateCommand(CommandText = batchUpdateString count)

+ 13 - 26
frameworks/FSharp/oxpecker/src/App/Program.fs

@@ -2,13 +2,21 @@ namespace App
 
 
 open System
 open System
 open Oxpecker
 open Oxpecker
-open System.Runtime.InteropServices
 
 
 [<RequireQualifiedAccess>]
 [<RequireQualifiedAccess>]
-module HtmlViews =
+module HttpHandlers =
+    open System.Text
+    open Microsoft.AspNetCore.Http
+    open SpanJson
     open Oxpecker.ViewEngine
     open Oxpecker.ViewEngine
 
 
-    let private head, tail =
+    let private extra =
+        {
+            id = 0
+            message = "Additional fortune added at request time."
+        }
+
+    let private fortunesHeadAndTail =
         (fun (content: HtmlElement) ->
         (fun (content: HtmlElement) ->
             html() {
             html() {
                 head() {
                 head() {
@@ -26,32 +34,11 @@ module HtmlViews =
             } :> HtmlElement
             } :> HtmlElement
         ) |> RenderHelpers.prerender
         ) |> RenderHelpers.prerender
 
 
-    let fortunes (fortunesData: ResizeArray<Fortune>) =
-        let fragment = __()
-        for fortune in CollectionsMarshal.AsSpan fortunesData do
-            tr() {
-                td() { raw <| string fortune.id }
-                td() { fortune.message }
-            }
-            |> fragment.AddChild
-        RenderHelpers.combine head tail fragment
-
-[<RequireQualifiedAccess>]
-module HttpHandlers =
-    open System.Text
-    open Microsoft.AspNetCore.Http
-    open SpanJson
-
-    let private extra =
-        {
-            id      = 0
-            message = "Additional fortune added at request time."
-        }
-
     let rec private renderFortunes (ctx: HttpContext) (data: ResizeArray<Fortune>) =
     let rec private renderFortunes (ctx: HttpContext) (data: ResizeArray<Fortune>) =
         data.Add extra
         data.Add extra
         data.Sort FortuneComparer
         data.Sort FortuneComparer
-        data |> HtmlViews.fortunes |> ctx.WriteHtmlViewChunked
+        RenderHelpers.CombinedElement(fortunesHeadAndTail, data)
+        |> ctx.WriteHtmlViewChunked
 
 
     let fortunes : EndpointHandler =
     let fortunes : EndpointHandler =
         fun ctx ->
         fun ctx ->

+ 20 - 9
frameworks/FSharp/oxpecker/src/App/RenderHelpers.fs

@@ -1,8 +1,27 @@
 module RenderHelpers
 module RenderHelpers
 
 
     open System.Text
     open System.Text
+    open App
     open Oxpecker.ViewEngine
     open Oxpecker.ViewEngine
 
 
+    type HeadAndTail =
+        {
+            Head: string
+            Tail: string
+        }
+
+    [<Struct>]
+    type CombinedElement(ht: HeadAndTail, fortunesData: ResizeArray<Fortune>) =
+        interface HtmlElement with
+            member this.Render(sb) =
+                sb.Append(ht.Head) |> ignore
+                for fortune in fortunesData do
+                    (tr() {
+                        td() { raw <| string fortune.id }
+                        td() { fortune.message }
+                    }).Render(sb)
+                sb.Append(ht.Tail) |> ignore
+
     let prerender (view: HtmlElement -> HtmlElement) =
     let prerender (view: HtmlElement -> HtmlElement) =
         let sb = StringBuilder()
         let sb = StringBuilder()
         let mutable head = ""
         let mutable head = ""
@@ -13,12 +32,4 @@
                  sb.Clear() |> ignore }
                  sb.Clear() |> ignore }
         let readyView = view fakeHole
         let readyView = view fakeHole
         readyView.Render(sb)
         readyView.Render(sb)
-        (head, sb.ToString())
-
-    let inline combine (head: string) (tail: string) (hole: HtmlElement) =
-        { new HtmlElement with
-            member this.Render(sb) =
-                sb.Append(head) |> ignore
-                hole.Render(sb)
-                sb.Append(tail) |> ignore
-        }
+        { Head = head; Tail = sb.ToString() }