Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tests/FSharp.Data.Benchmarks/FSharp.Data.Benchmarks.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<Compile Include="JsonBenchmarks.fs" />
<Compile Include="JsonStjBenchmarks.fs" />
<Compile Include="HtmlBenchmarks.fs" />
<Compile Include="CsvBenchmarks.fs" />
<Compile Include="Program.fs" />
Expand Down
84 changes: 84 additions & 0 deletions tests/FSharp.Data.Benchmarks/JsonStjBenchmarks.fs
Original file line number Diff line number Diff line change
@@ -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.
[<MemoryDiagnoser>]
[<SimpleJob>]
type JsonStjBenchmarks() =

let mutable githubJsonText = ""
let mutable twitterJsonText = ""
let mutable worldBankJsonText = ""

[<GlobalSetup>]
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) ──────────────────────────────────────

[<Benchmark(Baseline = true)>]
member _.ParseGitHub_Current() = JsonValue.Parse(githubJsonText)

[<Benchmark>]
member _.ParseTwitter_Current() = JsonValue.Parse(twitterJsonText)

[<Benchmark>]
member _.ParseWorldBank_Current() = JsonValue.Parse(worldBankJsonText)

// ── STJ-backed prototype (Option 2 from #1671) ──────────────────────────────────

[<Benchmark>]
member _.ParseGitHub_Stj() = StjConverter.parse githubJsonText

[<Benchmark>]
member _.ParseTwitter_Stj() = StjConverter.parse twitterJsonText

[<Benchmark>]
member _.ParseWorldBank_Stj() = StjConverter.parse worldBankJsonText
2 changes: 2 additions & 0 deletions tests/FSharp.Data.Benchmarks/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ let main args =
| [| "conversions" |] -> BenchmarkRunner.Run<JsonConversionBenchmarks>() |> ignore
| [| "html" |] -> BenchmarkRunner.Run<HtmlBenchmarks>() |> ignore
| [| "csv" |] -> BenchmarkRunner.Run<CsvBenchmarks>() |> ignore
| [| "stj" |] -> BenchmarkRunner.Run<JsonStjBenchmarks>() |> ignore
| _ ->
printfn "Running all benchmarks..."
BenchmarkRunner.Run<JsonBenchmarks>() |> ignore
BenchmarkRunner.Run<JsonConversionBenchmarks>() |> ignore
BenchmarkRunner.Run<HtmlBenchmarks>() |> ignore
BenchmarkRunner.Run<CsvBenchmarks>() |> ignore
BenchmarkRunner.Run<JsonStjBenchmarks>() |> ignore

0
25 changes: 22 additions & 3 deletions tests/FSharp.Data.Core.Tests/JsonParserProperties.fs
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,34 @@ let unescape s =
r.Replace(s, convert)

[<Test>]
let ``Parsing stringified JsonValue returns the same JsonValue`` () =
let ``Parsing stringified JsonValue returns the same JsonValue`` () =
Arb.register<Generators>() |> 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)

[<Test>]
let ``Parsing JsonValue formatted with None (indented) returns the same JsonValue`` () =
Arb.register<Generators>() |> ignore

let parseFormatted (json: JsonValue) =
json.ToString(JsonSaveOptions.None)
|> JsonValue.Parse = json

Check.One({Config.QuickThrowOnFailure with MaxTest = 500}, parseFormatted)

[<Test>]
let ``Parsing JsonValue formatted with CompactSpaceAfterComma returns the same JsonValue`` () =
Arb.register<Generators>() |> ignore

let parseCompact (json: JsonValue) =
json.ToString(JsonSaveOptions.CompactSpaceAfterComma)
|> JsonValue.Parse = json

Check.One({Config.QuickThrowOnFailure with MaxTest = 500}, parseCompact)

[<Test>]
let ``Stringifying parsed string returns the same string`` () =
Expand Down
Loading