diff --git a/tests/FSharp.Data.Benchmarks/FSharp.Data.Benchmarks.fsproj b/tests/FSharp.Data.Benchmarks/FSharp.Data.Benchmarks.fsproj index ae1b88d7b..f6a26bbae 100644 --- a/tests/FSharp.Data.Benchmarks/FSharp.Data.Benchmarks.fsproj +++ b/tests/FSharp.Data.Benchmarks/FSharp.Data.Benchmarks.fsproj @@ -14,6 +14,7 @@ PreserveNewest + diff --git a/tests/FSharp.Data.Benchmarks/JsonStjBenchmarks.fs b/tests/FSharp.Data.Benchmarks/JsonStjBenchmarks.fs new file mode 100644 index 000000000..9be7c324f --- /dev/null +++ b/tests/FSharp.Data.Benchmarks/JsonStjBenchmarks.fs @@ -0,0 +1,84 @@ +/// Benchmark: System.Text.Json-backed parser vs current FSharp.Data hand-written parser +/// Prototypes "Option 2" from https://github.com/fsprojects/FSharp.Data/issues/1671 — +/// keep the JsonValue public API while using Utf8JsonReader / JsonDocument as the parsing kernel. +/// +/// Run with: dotnet run -c Release -- stj +namespace FSharp.Data.Benchmarks + +open System.IO +open System.Text.Json +open BenchmarkDotNet.Attributes +open FSharp.Data + +/// Converts a System.Text.Json JsonElement into a FSharp.Data JsonValue. +/// This is the prototype STJ backend that could replace the hand-written parser. +module private StjConverter = + + let rec ofJsonElement (el: JsonElement) : JsonValue = + match el.ValueKind with + | JsonValueKind.Null -> JsonValue.Null + | JsonValueKind.True -> JsonValue.Boolean true + | JsonValueKind.False -> JsonValue.Boolean false + | JsonValueKind.String -> JsonValue.String(el.GetString()) + | JsonValueKind.Number -> + let mutable d = 0m + + if el.TryGetDecimal(&d) then + JsonValue.Number(d) + else + JsonValue.Float(el.GetDouble()) + | JsonValueKind.Array -> + el.EnumerateArray() + |> Seq.map ofJsonElement + |> Seq.toArray + |> JsonValue.Array + | JsonValueKind.Object -> + el.EnumerateObject() + |> Seq.map (fun p -> p.Name, ofJsonElement p.Value) + |> Seq.toArray + |> JsonValue.Record + | _ -> JsonValue.Null + + /// Parse a JSON string to JsonValue using System.Text.Json as the parsing backend. + let parse (text: string) : JsonValue = + use doc = JsonDocument.Parse(text) + ofJsonElement doc.RootElement + +/// Compares the current FSharp.Data hand-written parser with a System.Text.Json-backed +/// prototype on three representative real-world JSON files. See issue #1671 for context. +[] +[] +type JsonStjBenchmarks() = + + let mutable githubJsonText = "" + let mutable twitterJsonText = "" + let mutable worldBankJsonText = "" + + [] + member _.Setup() = + let dataPath = Path.Combine(__SOURCE_DIRECTORY__, "../FSharp.Data.Tests/Data") + githubJsonText <- File.ReadAllText(Path.Combine(dataPath, "GitHub.json")) + twitterJsonText <- File.ReadAllText(Path.Combine(dataPath, "TwitterSample.json")) + worldBankJsonText <- File.ReadAllText(Path.Combine(dataPath, "WorldBank.json")) + + // ── Current hand-written parser (baseline) ────────────────────────────────────── + + [] + member _.ParseGitHub_Current() = JsonValue.Parse(githubJsonText) + + [] + member _.ParseTwitter_Current() = JsonValue.Parse(twitterJsonText) + + [] + member _.ParseWorldBank_Current() = JsonValue.Parse(worldBankJsonText) + + // ── STJ-backed prototype (Option 2 from #1671) ────────────────────────────────── + + [] + member _.ParseGitHub_Stj() = StjConverter.parse githubJsonText + + [] + member _.ParseTwitter_Stj() = StjConverter.parse twitterJsonText + + [] + member _.ParseWorldBank_Stj() = StjConverter.parse worldBankJsonText diff --git a/tests/FSharp.Data.Benchmarks/Program.fs b/tests/FSharp.Data.Benchmarks/Program.fs index 9e66baea8..a07e74048 100644 --- a/tests/FSharp.Data.Benchmarks/Program.fs +++ b/tests/FSharp.Data.Benchmarks/Program.fs @@ -12,11 +12,13 @@ let main args = | [| "conversions" |] -> BenchmarkRunner.Run() |> ignore | [| "html" |] -> BenchmarkRunner.Run() |> ignore | [| "csv" |] -> BenchmarkRunner.Run() |> ignore + | [| "stj" |] -> BenchmarkRunner.Run() |> ignore | _ -> printfn "Running all benchmarks..." BenchmarkRunner.Run() |> ignore BenchmarkRunner.Run() |> ignore BenchmarkRunner.Run() |> ignore BenchmarkRunner.Run() |> ignore + BenchmarkRunner.Run() |> ignore 0 \ No newline at end of file diff --git a/tests/FSharp.Data.Core.Tests/JsonParserProperties.fs b/tests/FSharp.Data.Core.Tests/JsonParserProperties.fs index 7fcda85be..66a4f3b33 100644 --- a/tests/FSharp.Data.Core.Tests/JsonParserProperties.fs +++ b/tests/FSharp.Data.Core.Tests/JsonParserProperties.fs @@ -120,15 +120,34 @@ let unescape s = r.Replace(s, convert) [] -let ``Parsing stringified JsonValue returns the same JsonValue`` () = +let ``Parsing stringified JsonValue returns the same JsonValue`` () = Arb.register() |> ignore let parseStringified (json: JsonValue) = json.ToString(JsonSaveOptions.DisableFormatting) |> JsonValue.Parse = json - Check.One ({Config.QuickThrowOnFailure with MaxTest = 1000}, - parseStringified) + Check.One({Config.QuickThrowOnFailure with MaxTest = 1000}, parseStringified) + +[] +let ``Parsing JsonValue formatted with None (indented) returns the same JsonValue`` () = + Arb.register() |> ignore + + let parseFormatted (json: JsonValue) = + json.ToString(JsonSaveOptions.None) + |> JsonValue.Parse = json + + Check.One({Config.QuickThrowOnFailure with MaxTest = 500}, parseFormatted) + +[] +let ``Parsing JsonValue formatted with CompactSpaceAfterComma returns the same JsonValue`` () = + Arb.register() |> ignore + + let parseCompact (json: JsonValue) = + json.ToString(JsonSaveOptions.CompactSpaceAfterComma) + |> JsonValue.Parse = json + + Check.One({Config.QuickThrowOnFailure with MaxTest = 500}, parseCompact) [] let ``Stringifying parsed string returns the same string`` () =