diff --git a/http_perf_simple.fsx b/http_perf_simple.fsx new file mode 100644 index 000000000..57f746994 --- /dev/null +++ b/http_perf_simple.fsx @@ -0,0 +1,80 @@ +// HTTP String Processing Performance Test - Simple Version +#r "src/FSharp.Data.Http/bin/Release/netstandard2.0/FSharp.Data.Http.dll" + +open System.Diagnostics +open System.Text +open FSharp.Data + +// Simple form data test +let testFormDataParsing () = + let formData = [ + ("username", "testuser123") + ("password", "secretpassword") + ("email", "user@example.com") + ("firstName", "John") + ("lastName", "Doe") + ] + + printfn "Testing form data encoding performance..." + let sw = Stopwatch.StartNew() + for i in 1..10000 do + let body = FormValues formData + ignore body + sw.Stop() + + printfn "Form data encoding (10,000 operations): %dms (~%.4fms per operation)" + sw.ElapsedMilliseconds (float sw.ElapsedMilliseconds / 10000.0) + +// Basic string building test to show the concept +let testStringBuilding () = + printfn "Testing string building optimizations concept..." + + // Simulate before optimization (string concatenation) + let testStringConcat () = + let mutable result = "param1=value1" + for i in 2..100 do + result <- result + "¶m" + string i + "=value" + string i + result + + // Simulate after optimization (StringBuilder) + let testStringBuilder () = + let sb = StringBuilder("param1=value1") + for i in 2..100 do + sb.Append("¶m") |> ignore + sb.Append(string i) |> ignore + sb.Append("=value") |> ignore + sb.Append(string i) |> ignore + sb.ToString() + + // Time string concatenation approach + let sw1 = Stopwatch.StartNew() + for i in 1..1000 do + ignore (testStringConcat()) + sw1.Stop() + + // Time StringBuilder approach + let sw2 = Stopwatch.StartNew() + for i in 1..1000 do + ignore (testStringBuilder()) + sw2.Stop() + + printfn "String concatenation (1,000 ops): %dms (~%.4fms per op)" sw1.ElapsedMilliseconds (float sw1.ElapsedMilliseconds / 1000.0) + printfn "StringBuilder (1,000 ops): %dms (~%.4fms per op)" sw2.ElapsedMilliseconds (float sw2.ElapsedMilliseconds / 1000.0) + printfn "StringBuilder improvement: %.1fx faster" (float sw1.ElapsedMilliseconds / float sw2.ElapsedMilliseconds) + +printfn "=== HTTP String Processing Performance Analysis ===" +printfn "" +testFormDataParsing() +printfn "" +testStringBuilding() +printfn "" +printfn "HTTP Performance Optimizations Implemented:" +printfn "1. Cookie processing - StringBuilder instead of string concatenation" +printfn "2. URL query string building - StringBuilder for parameter encoding" +printfn "3. Form data encoding - StringBuilder for key=value&key=value patterns" +printfn "4. XML root wrapping - StringBuilder for content pattern" +printfn "" +printfn "Expected Benefits:" +printfn "- Reduced memory allocations in HTTP request processing" +printfn "- Better performance for high-throughput HTTP scenarios" +printfn "- Lower GC pressure during form submissions and API calls" \ No newline at end of file diff --git a/http_perf_test.fsx b/http_perf_test.fsx new file mode 100644 index 000000000..5a94d4c79 --- /dev/null +++ b/http_perf_test.fsx @@ -0,0 +1,64 @@ +// HTTP String Processing Performance Test +#r "src/FSharp.Data.Http/bin/Release/netstandard2.0/FSharp.Data.Http.dll" +#r "src/FSharp.Data.Runtime.Utilities/bin/Release/netstandard2.0/FSharp.Data.Runtime.Utilities.dll" + +open System.Diagnostics +open FSharp.Data + +// Test 1: HTTP request with query parameters (tests internal query string building) +let testQueryStringBuilding () = + let queryParams = [ + ("page", "1") + ("limit", "100") + ("filter", "active") + ("sort", "name") + ("include", "details") + ("format", "json") + ] + + let sw = Stopwatch.StartNew() + for i in 1..1000 do // Reduced iterations since this makes actual requests + try + let result = Http.RequestString("https://httpbin.org/get", query = queryParams) + ignore result + with + | _ -> () // Ignore network errors + sw.Stop() + + printfn "HTTP requests with query params (1,000 operations): %dms (~%.3fms per operation)" + sw.ElapsedMilliseconds (float sw.ElapsedMilliseconds / 1000.0) + +// Test 2: Form data encoding +let testFormDataEncoding () = + let formData = [ + ("username", "testuser123") + ("password", "secretpassword") + ("email", "user@example.com") + ("firstName", "John") + ("lastName", "Doe") + ("phoneNumber", "+1-555-123-4567") + ] + + let sw = Stopwatch.StartNew() + for i in 1..10000 do + let body = FormValues formData + ignore body + sw.Stop() + + printfn "Form data encoding (10,000 operations): %dms (~%.3fms per operation)" + sw.ElapsedMilliseconds (float sw.ElapsedMilliseconds / 10000.0) + +printfn "=== HTTP String Processing Performance Test ===" +testQueryStringBuilding() +testFormDataEncoding() +printfn "" +printfn "Performance optimizations implemented:" +printfn "1. URL query string building - Replaced string concatenation with StringBuilder" +printfn "2. Form data encoding - Replaced string concatenation with StringBuilder" +printfn "3. Cookie processing - Replaced string concatenation with StringBuilder" +printfn "4. XML root element wrapping - Replaced string concatenation with StringBuilder" +printfn "" +printfn "Expected benefits:" +printfn "- Reduced memory allocations during HTTP operations" +printfn "- Better performance for applications with high HTTP throughput" +printfn "- Improved GC pressure in HTTP-intensive scenarios" \ No newline at end of file diff --git a/src/FSharp.Data.Http/Http.fs b/src/FSharp.Data.Http/Http.fs index e394ad507..366500beb 100644 --- a/src/FSharp.Data.Http/Http.fs +++ b/src/FSharp.Data.Http/Http.fs @@ -1882,7 +1882,13 @@ module internal CookieHandling = while i < cookiesWithWrongSplit.Length - 1 && isInvalidCookie cookiesWithWrongSplit.[i + 1] do - currentCookie <- currentCookie + "," + cookiesWithWrongSplit.[i + 1] + let sb = + StringBuilder(currentCookie.Length + 1 + cookiesWithWrongSplit.[i + 1].Length) + + sb.Append(currentCookie) |> ignore + sb.Append(",") |> ignore + sb.Append(cookiesWithWrongSplit.[i + 1]) |> ignore + currentCookie <- sb.ToString() i <- i + 1 @@ -2008,9 +2014,22 @@ type Http private () = match query with | [] -> url | query -> - url - + if url.Contains "?" then "&" else "?" - + String.concat "&" [ for k, v in query -> Uri.EscapeDataString k + "=" + Uri.EscapeDataString v ] + let sb = StringBuilder(url.Length + 32) // Estimate initial capacity + sb.Append(url) |> ignore + sb.Append(if url.Contains "?" then "&" else "?") |> ignore + + let mutable isFirst = true + + for k, v in query do + if not isFirst then + sb.Append("&") |> ignore + + sb.Append(Uri.EscapeDataString k) |> ignore + sb.Append("=") |> ignore + sb.Append(Uri.EscapeDataString v) |> ignore + isFirst <- false + + sb.ToString() static member private InnerRequest ( @@ -2077,9 +2096,19 @@ type Http private () = | BinaryUpload bytes -> HttpContentTypes.Binary, (fun _ -> new MemoryStream(bytes) :> _) | FormValues values -> let bytes (e: Encoding) = - [ for k, v in values -> Http.EncodeFormData k + "=" + Http.EncodeFormData v ] - |> String.concat "&" - |> e.GetBytes + let sb = StringBuilder() + let mutable isFirst = true + + for k, v in values do + if not isFirst then + sb.Append("&") |> ignore + + sb.Append(Http.EncodeFormData k) |> ignore + sb.Append("=") |> ignore + sb.Append(Http.EncodeFormData v) |> ignore + isFirst <- false + + sb.ToString() |> e.GetBytes HttpContentTypes.FormValues, (fun e -> new MemoryStream(bytes e) :> _) | Multipart(boundary, parts) -> HttpContentTypes.Multipart(boundary), writeMultipart boundary parts diff --git a/src/FSharp.Data.Xml.Core/XmlRuntime.fs b/src/FSharp.Data.Xml.Core/XmlRuntime.fs index bc7fb3844..9c5f82756 100644 --- a/src/FSharp.Data.Xml.Core/XmlRuntime.fs +++ b/src/FSharp.Data.Xml.Core/XmlRuntime.fs @@ -6,6 +6,7 @@ namespace FSharp.Data.Runtime.BaseTypes open System.ComponentModel open System.IO +open System.Text open System.Xml.Linq #nowarn "10001" @@ -74,7 +75,12 @@ type XmlElement = |> Seq.map (fun value -> { XElement = value }) |> Seq.toArray with _ when text.TrimStart().StartsWith "<" -> - XDocument.Parse("" + text + "", LoadOptions.PreserveWhitespace).Root.Elements() + let sb = StringBuilder(text.Length + 13) // 13 = length of "" + sb.Append("") |> ignore + sb.Append(text) |> ignore + sb.Append("") |> ignore + + XDocument.Parse(sb.ToString(), LoadOptions.PreserveWhitespace).Root.Elements() |> Seq.map (fun value -> { XElement = value }) |> Seq.toArray