Skip to content

Commit f0d4a48

Browse files
authored
Merge branch 'main' into perf/json-parsing-optimization
2 parents 9175ed5 + eaa9c4a commit f0d4a48

12 files changed

Lines changed: 569 additions & 8 deletions

File tree

FSharp.Data.sln

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.Json.Core", "sr
3838
EndProject
3939
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.Tests", "tests\FSharp.Data.Tests\FSharp.Data.Tests.fsproj", "{750148EC-6A05-421D-96A4-E5AC9D18AF58}"
4040
EndProject
41+
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.Benchmarks", "tests\FSharp.Data.Benchmarks\FSharp.Data.Benchmarks.fsproj", "{A1B2C3D4-5E6F-7890-ABCD-EF1234567890}"
42+
EndProject
4143
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.DesignTime", "src\FSharp.Data.DesignTime\FSharp.Data.DesignTime.fsproj", "{44E0DF97-D8FD-4805-8A84-B888D9589C8A}"
4244
EndProject
4345
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.Html.Core", "src\FSharp.Data.Html.Core\FSharp.Data.Html.Core.fsproj", "{E91BF68E-257C-43E6-BDE9-672D4E3BFAFA}"

build/build.fs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,26 @@ let buildscript () =
158158
Target.create "GenerateDocs" (fun _ ->
159159
Shell.cleanDir ".fsdocs"
160160

161+
// List of projects to include in documentation (excluding benchmark project)
162+
let docProjects = [
163+
"src/FSharp.Data/FSharp.Data.fsproj"
164+
"src/FSharp.Data.DesignTime/FSharp.Data.DesignTime.fsproj"
165+
"src/FSharp.Data.Json.Core/FSharp.Data.Json.Core.fsproj"
166+
"src/FSharp.Data.Csv.Core/FSharp.Data.Csv.Core.fsproj"
167+
"src/FSharp.Data.Html.Core/FSharp.Data.Html.Core.fsproj"
168+
"src/FSharp.Data.Http/FSharp.Data.Http.fsproj"
169+
"src/FSharp.Data.Runtime.Utilities/FSharp.Data.Runtime.Utilities.fsproj"
170+
"src/FSharp.Data.Xml.Core/FSharp.Data.Xml.Core.fsproj"
171+
"src/FSharp.Data.WorldBank.Core/FSharp.Data.WorldBank.Core.fsproj"
172+
]
173+
174+
let projectArgs = docProjects |> String.concat " "
175+
161176
let result =
162177
DotNet.exec
163178
id
164179
"fsdocs"
165-
("build --properties Configuration=Release --strict --eval --clean --parameters fsdocs-package-version "
180+
("build --projects " + projectArgs + " --properties Configuration=Release --strict --eval --clean --parameters fsdocs-package-version "
166181
+ release.NugetVersion)
167182

168183
if not result.OK then
@@ -221,6 +236,23 @@ let buildscript () =
221236
Trace.logf "Errors while formatting: %A" result.Errors
222237
failwith "Unknown errors while formatting")
223238

239+
Target.create "RunBenchmarks" (fun _ ->
240+
let benchmarkProject =
241+
"tests" </> "FSharp.Data.Benchmarks" </> "FSharp.Data.Benchmarks.fsproj"
242+
243+
DotNet.build
244+
(fun opts ->
245+
{ opts with
246+
Configuration = DotNet.BuildConfiguration.Release })
247+
benchmarkProject
248+
249+
let benchmarkDir = "tests" </> "FSharp.Data.Benchmarks"
250+
251+
Shell.Exec("dotnet", "run -c Release -- --job dry --filter \"*ParseSimpleJson*\"", benchmarkDir)
252+
|> fun exitCode ->
253+
if exitCode <> 0 then
254+
failwith "Benchmark execution failed")
255+
224256
Target.create "All" ignore
225257

226258
"Clean" ==> "AssemblyInfo" ==> "CheckFormat" ==> "Build"
@@ -230,6 +262,7 @@ let buildscript () =
230262
"Build" ==> "Pack" ==> "All"
231263
"Build" ==> "All"
232264
"Build" ==> "RunTests" ==> "All"
265+
"Build" ==> "RunBenchmarks"
233266

234267
Target.runOrDefaultWithArguments "Help"
235268

paket.dependencies

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,10 @@ group Test
5656
nuget FsUnit 4.0.4
5757
nuget FsCheck 2.15.1
5858
nuget GitHubActionsTestLogger
59+
60+
group Benchmarks
61+
frameworks: net8.0
62+
source https://api.nuget.org/v3/index.json
63+
64+
nuget FSharp.Core 6.0.1
65+
nuget BenchmarkDotNet

paket.lock

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,77 @@ NUGET
2424
NETStandard.Library.NETFramework (2.0.0-preview2-25405-01)
2525
GITHUB
2626
remote: fsprojects/FSharp.TypeProviders.SDK
27-
src/ProvidedTypes.fs (3b22420be5eddecaee89105d208cc4fb7b2e4df3)
28-
src/ProvidedTypes.fsi (3b22420be5eddecaee89105d208cc4fb7b2e4df3)
29-
tests/ProvidedTypesTesting.fs (3b22420be5eddecaee89105d208cc4fb7b2e4df3)
27+
src/ProvidedTypes.fs (ce34c1cc71096857b8342f1dedf93391addc9df6)
28+
src/ProvidedTypes.fsi (ce34c1cc71096857b8342f1dedf93391addc9df6)
29+
tests/ProvidedTypesTesting.fs (ce34c1cc71096857b8342f1dedf93391addc9df6)
30+
GROUP Benchmarks
31+
RESTRICTION: == net8.0
32+
NUGET
33+
remote: https://api.nuget.org/v3/index.json
34+
BenchmarkDotNet (0.15.2)
35+
BenchmarkDotNet.Annotations (>= 0.15.2)
36+
CommandLineParser (>= 2.9.1)
37+
Gee.External.Capstone (>= 2.3)
38+
Iced (>= 1.21)
39+
Microsoft.CodeAnalysis.CSharp (>= 4.14)
40+
Microsoft.Diagnostics.Runtime (>= 3.1.512801)
41+
Microsoft.Diagnostics.Tracing.TraceEvent (>= 3.1.21)
42+
Microsoft.DotNet.PlatformAbstractions (>= 3.1.6)
43+
Perfolizer (0.5.3)
44+
System.Management (>= 9.0.5)
45+
BenchmarkDotNet.Annotations (0.15.2)
46+
CommandLineParser (2.9.1)
47+
FSharp.Core (6.0.1)
48+
Gee.External.Capstone (2.3)
49+
Iced (1.21)
50+
Microsoft.CodeAnalysis.Analyzers (4.14)
51+
Microsoft.CodeAnalysis.Common (4.14)
52+
Microsoft.CodeAnalysis.Analyzers (>= 3.11)
53+
System.Collections.Immutable (>= 9.0)
54+
System.Reflection.Metadata (>= 9.0)
55+
Microsoft.CodeAnalysis.CSharp (4.14)
56+
Microsoft.CodeAnalysis.Analyzers (>= 3.11)
57+
Microsoft.CodeAnalysis.Common (4.14)
58+
System.Collections.Immutable (>= 9.0)
59+
System.Reflection.Metadata (>= 9.0)
60+
Microsoft.Diagnostics.NETCore.Client (0.2.621003)
61+
Microsoft.Extensions.Logging.Abstractions (>= 6.0.4)
62+
Microsoft.Diagnostics.Runtime (3.1.512801)
63+
Microsoft.Diagnostics.NETCore.Client (>= 0.2.410101)
64+
Microsoft.Diagnostics.Tracing.TraceEvent (3.1.24)
65+
Microsoft.Diagnostics.NETCore.Client (>= 0.2.510501)
66+
Microsoft.Win32.Registry (>= 5.0)
67+
System.Collections.Immutable (>= 9.0.8)
68+
System.Reflection.Metadata (>= 9.0.8)
69+
System.Reflection.TypeExtensions (>= 4.7)
70+
System.Runtime.CompilerServices.Unsafe (>= 6.1.2)
71+
System.Text.Json (>= 9.0.8)
72+
Microsoft.DotNet.PlatformAbstractions (3.1.6)
73+
Microsoft.Extensions.DependencyInjection.Abstractions (9.0.8)
74+
Microsoft.Extensions.Logging.Abstractions (9.0.8)
75+
Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.8)
76+
System.Diagnostics.DiagnosticSource (>= 9.0.8)
77+
Microsoft.Win32.Registry (5.0)
78+
System.Security.AccessControl (>= 5.0)
79+
System.Security.Principal.Windows (>= 5.0)
80+
Perfolizer (0.5.3)
81+
System.CodeDom (9.0.8)
82+
System.Collections.Immutable (9.0.8)
83+
System.Diagnostics.DiagnosticSource (9.0.8)
84+
System.IO.Pipelines (9.0.8)
85+
System.Management (9.0.8)
86+
System.CodeDom (>= 9.0.8)
87+
System.Reflection.Metadata (9.0.8)
88+
System.Collections.Immutable (>= 9.0.8)
89+
System.Reflection.TypeExtensions (4.7)
90+
System.Runtime.CompilerServices.Unsafe (6.1.2)
91+
System.Security.AccessControl (6.0.1)
92+
System.Security.Principal.Windows (5.0)
93+
System.Text.Encodings.Web (9.0.8)
94+
System.Text.Json (9.0.8)
95+
System.IO.Pipelines (>= 9.0.8)
96+
System.Text.Encodings.Web (>= 9.0.8)
97+
3098
GROUP Fake
3199
STORAGE: NONE
32100
NUGET

src/FSharp.Data.Runtime.Utilities/TextConversions.fs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,22 @@ type TextConversions private () =
9292
//of the culture. Which is probably better since we have lots of scenarios where we want to
9393
//consume values prefixed with € or $ but in a different culture.
9494
static member private RemoveAdorners(value: string) =
95-
String(
96-
value.ToCharArray()
97-
|> Array.filter (not << TextConversions.DefaultRemovableAdornerCharacters.Contains)
98-
)
95+
// Fast path: check if any adorners exist before doing expensive operations
96+
let mutable hasAdorners = false
97+
98+
for i = 0 to value.Length - 1 do
99+
if TextConversions.DefaultRemovableAdornerCharacters.Contains(value.[i]) then
100+
hasAdorners <- true
101+
102+
if not hasAdorners then
103+
// No adorners found, return original string to avoid allocation
104+
value
105+
else
106+
// Adorners found, perform filtering
107+
String(
108+
value.ToCharArray()
109+
|> Array.filter (not << TextConversions.DefaultRemovableAdornerCharacters.Contains)
110+
)
99111

100112
/// Turns empty or null string value into None, otherwise returns Some
101113
static member AsString str =
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<IsPackable>false</IsPackable>
6+
<OutputType>Exe</OutputType>
7+
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
8+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
9+
<OtherFlags>$(OtherFlags) --warnon:1182 --nowarn:44</OtherFlags>
10+
<Tailcalls>true</Tailcalls>
11+
</PropertyGroup>
12+
<ItemGroup>
13+
<EmbeddedResource Include="../FSharp.Data.Tests/Data/**/*.*">
14+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
15+
</EmbeddedResource>
16+
<Compile Include="JsonBenchmarks.fs" />
17+
<Compile Include="Program.fs" />
18+
</ItemGroup>
19+
<ItemGroup>
20+
<None Include="paket.references" />
21+
</ItemGroup>
22+
<ItemGroup>
23+
<ProjectReference Include="..\..\src\FSharp.Data.Json.Core\FSharp.Data.Json.Core.fsproj" />
24+
<ProjectReference Include="..\..\src\FSharp.Data.Runtime.Utilities\FSharp.Data.Runtime.Utilities.fsproj" />
25+
<ProjectReference Include="..\..\src\FSharp.Data.Csv.Core\FSharp.Data.Csv.Core.fsproj" />
26+
<ProjectReference Include="..\..\src\FSharp.Data.Html.Core\FSharp.Data.Html.Core.fsproj" />
27+
<ProjectReference Include="..\..\src\FSharp.Data.Http\FSharp.Data.Http.fsproj" />
28+
</ItemGroup>
29+
<ItemGroup>
30+
<PackageReference Update="FSharp.Core" Version="6.0.1" />
31+
</ItemGroup>
32+
<Import Project="..\..\.paket\Paket.Restore.targets" />
33+
</Project>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
namespace FSharp.Data.Benchmarks
2+
3+
open System
4+
open System.IO
5+
open BenchmarkDotNet.Attributes
6+
open FSharp.Data
7+
8+
[<MemoryDiagnoser>]
9+
[<SimpleJob>]
10+
type JsonBenchmarks() =
11+
12+
let mutable githubJsonText = ""
13+
let mutable twitterJsonText = ""
14+
let mutable worldBankJsonText = ""
15+
let mutable simpleJsonText = ""
16+
let mutable nestedJsonText = ""
17+
18+
[<GlobalSetup>]
19+
member this.Setup() =
20+
let dataPath = Path.Combine(__SOURCE_DIRECTORY__, "../FSharp.Data.Tests/Data")
21+
22+
githubJsonText <- File.ReadAllText(Path.Combine(dataPath, "GitHub.json"))
23+
twitterJsonText <- File.ReadAllText(Path.Combine(dataPath, "TwitterSample.json"))
24+
worldBankJsonText <- File.ReadAllText(Path.Combine(dataPath, "WorldBank.json"))
25+
simpleJsonText <- File.ReadAllText(Path.Combine(dataPath, "Simple.json"))
26+
nestedJsonText <- File.ReadAllText(Path.Combine(dataPath, "Nested.json"))
27+
28+
[<Benchmark>]
29+
member this.ParseSimpleJson() =
30+
JsonValue.Parse(simpleJsonText)
31+
32+
[<Benchmark>]
33+
member this.ParseNestedJson() =
34+
JsonValue.Parse(nestedJsonText)
35+
36+
[<Benchmark>]
37+
member this.ParseGitHubJson() =
38+
JsonValue.Parse(githubJsonText)
39+
40+
[<Benchmark>]
41+
member this.ParseTwitterJson() =
42+
JsonValue.Parse(twitterJsonText)
43+
44+
[<Benchmark>]
45+
member this.ParseWorldBankJson() =
46+
JsonValue.Parse(worldBankJsonText)
47+
48+
[<Benchmark>]
49+
member this.ToStringGitHubJson() =
50+
let json = JsonValue.Parse(githubJsonText)
51+
json.ToString()
52+
53+
[<Benchmark>]
54+
member this.ToStringTwitterJson() =
55+
let json = JsonValue.Parse(twitterJsonText)
56+
json.ToString()
57+
58+
[<MemoryDiagnoser>]
59+
[<SimpleJob>]
60+
type JsonConversionBenchmarks() =
61+
62+
let mutable sampleJson = JsonValue.Null
63+
64+
[<GlobalSetup>]
65+
member this.Setup() =
66+
let dataPath = Path.Combine(__SOURCE_DIRECTORY__, "../FSharp.Data.Tests/Data")
67+
let githubJsonText = File.ReadAllText(Path.Combine(dataPath, "GitHub.json"))
68+
sampleJson <- JsonValue.Parse(githubJsonText)
69+
70+
[<Benchmark>]
71+
member this.AccessProperties() =
72+
sampleJson.Properties()
73+
|> Array.head
74+
|> fun (_, value) -> value
75+
76+
[<Benchmark>]
77+
member this.GetArrayElements() =
78+
match sampleJson with
79+
| JsonValue.Record props ->
80+
match Array.tryFind (fun (k, _) -> k = "items") props with
81+
| Some (_, JsonValue.Array elements) -> elements
82+
| _ -> [||]
83+
| _ -> [||]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
open BenchmarkDotNet.Running
2+
open FSharp.Data.Benchmarks
3+
4+
[<EntryPoint>]
5+
let main args =
6+
printfn "FSharp.Data Benchmarks"
7+
printfn "====================="
8+
printfn ""
9+
10+
match args with
11+
| [| "json" |] -> BenchmarkRunner.Run<JsonBenchmarks>() |> ignore
12+
| [| "conversions" |] -> BenchmarkRunner.Run<JsonConversionBenchmarks>() |> ignore
13+
| _ ->
14+
printfn "Running all benchmarks..."
15+
BenchmarkRunner.Run<JsonBenchmarks>() |> ignore
16+
BenchmarkRunner.Run<JsonConversionBenchmarks>() |> ignore
17+
18+
0

0 commit comments

Comments
 (0)