Runtime: GetFromJsonAsync is ~20% slower than GetJsonAsync in Blazor WASM

Created on 2 Apr 2020  路  5Comments  路  Source: dotnet/runtime

Describe the bug

Not really a bug per say, but the new GetFromJsonAsync method is ~20% slower than the GetJsonAsync method in Blazor WASM in my (admittedly _extremely_ primitive) perf testing. I was gonna write up a blog post on the perf improvements after 3.2 Preview 3 dropped, but was surprised to see the significant drop in perf. Granted, does it matter most of the time? Probably not, but thought I'd share.

To Reproduce

  1. Clone https://github.com/scottsauber/blazor-json-speed-test
  2. Run the project
  3. Navigate to the Fetch Data page

BlazorSpeedTest

Further technical details

  • ASP.NET Core version: 3.1
  • Include the output of dotnet --info:
.NET Core SDK (reflecting any global.json):
 Version:   3.1.300-preview-015048
 Commit:    13f19b4682

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.18362
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\3.1.300-preview-015048\

Host (useful for support):
  Version: 3.1.2
  Commit:  916b5cba26

.NET Core SDKs installed:
  2.1.403 [C:\Program Files\dotnet\sdk]
  2.1.802 [C:\Program Files\dotnet\sdk]
  2.2.101 [C:\Program Files\dotnet\sdk]
  2.2.202 [C:\Program Files\dotnet\sdk]
  2.2.300 [C:\Program Files\dotnet\sdk]
  2.2.401 [C:\Program Files\dotnet\sdk]
  2.2.402 [C:\Program Files\dotnet\sdk]
  3.0.100-preview7-012821 [C:\Program Files\dotnet\sdk]
  3.0.100 [C:\Program Files\dotnet\sdk]
  3.1.200 [C:\Program Files\dotnet\sdk]
  3.1.300-preview-015048 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.16 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.16 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.0-preview7.19365.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.0-preview7-27912-14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.0.0-preview7-27912-14 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  • The IDE (VS / VS Code/ VS4Mac) you're running on, and it's version:
    Microsoft Visual Studio Community 2019 Preview
    Version 16.6.0 Preview 2.0
area-System.Text.Json tenet-performance

Most helpful comment

@ericstj I recently discussed this issue with @sebastienros, and yes, its probably the JIT optimizing out stuff due lack of warmup on the benchmark provided by @scottsauber.

Sebastien walk me through the process of creating benchmarks for this using the https://github.com/aspnet/Benchmarks/tree/master/src/BenchmarksDriver2 suite and there is indeed a regression caused by the lack of a persistent JsonSerializerOptions as described in https://github.com/dotnet/runtime/issues/34440#issuecomment-608316775.

Now I am looking if further optimizations can be made.

All 5 comments

/cc @Jozkee

As of now, for each call to GetFromJsonAsync, we are creating a new instance of JsonSerializerOptions, now this is bad because internally we cache several things into it.

The first thing that I noticed is the amount of allocations is ridiculously high when calling GetFromJsonAsync compared to the old method, GetJsonAsync, and this is aliviated when you pass an options instance.

private static readonly System.Text.Json.JsonSerializerOptions s_options = 
    new System.Text.Json.JsonSerializerOptions() 
{
    PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
    PropertyNameCaseInsensitive = true,
};

private async Task RunNewJsonMethod()
{
    for (int i = 0; i < iterations; i++)
    {
        var stopwatch = new System.Diagnostics.Stopwatch();
        stopwatch.Start();
        await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast", s_options);
        stopwatch.Stop();

        // throw away the first result because it's usually inherently slower than subsequent requests
        if (i != 0)
        {
            newTimes.Add(stopwatch.ElapsedMilliseconds);
        }
    }
}

@scottsauber can you try above snippet and tell me if you see improvement?

Second thing that I noticed is that when you change the order in that you call the methods, the former called performs slightly worse. I am unaware of the reason of such behavior.

protected override async Task OnInitializedAsync()
{
    await RunOldJsonMethod(); // slower 
    await RunNewJsonMethod(); // faster
    loading = false;
}

I will prepare a PR to correct the default JsonSerializerOptions used on the System.Net.Http.Json methods, that will aliviate the allocations and potentially the preformance decrease as well.

Thanks for reporting this.

@Jokzee - Thanks for the really quick reply!

The first snippet didn't seem to noticeably change performance.

I did see the same behavior as you though when I changed the order of the methods, that the 2nd method is always faster.

Even if I change the implementation of the Old and New method to be identical and use the same Json method (doesn't matter if it's GetJsonAsync or GetFromJsonAsync), the second one is always faster.

@adamsitnik might be able to provide some advice for better creating reliable benchmarks in blazor. When I hear things like "second one is always faster" it sounds to me like there isn't control for JIT / warmup in the benchmark.

@ericstj I recently discussed this issue with @sebastienros, and yes, its probably the JIT optimizing out stuff due lack of warmup on the benchmark provided by @scottsauber.

Sebastien walk me through the process of creating benchmarks for this using the https://github.com/aspnet/Benchmarks/tree/master/src/BenchmarksDriver2 suite and there is indeed a regression caused by the lack of a persistent JsonSerializerOptions as described in https://github.com/dotnet/runtime/issues/34440#issuecomment-608316775.

Now I am looking if further optimizations can be made.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bencz picture bencz  路  3Comments

omariom picture omariom  路  3Comments

jchannon picture jchannon  路  3Comments

matty-hall picture matty-hall  路  3Comments

jkotas picture jkotas  路  3Comments