akamsteeg / AtleX.HaveIBeenPwned

A fully async .NET Standard client library for the API of HaveIBeenPwned.com
https://www.nuget.org/packages/AtleX.HaveIBeenPwned/
MIT License
5 stars 0 forks source link

Investigation replacing the dependency on NewtonSoft.Json with System.Text.Json #25

Closed akamsteeg closed 4 years ago

akamsteeg commented 5 years ago

In .NET Core 3.0.0, a new non-allocating JSON reader/writer system is introduced in the System.Text.Json namespace. We should investigate what it takes to replace NewtonSoft.Json with System.Text.Json and measure any performance impact.

akamsteeg commented 5 years ago

This looks promising. Apparently we only need to change a single method in HaveIBeenPwnedClient:

private async Task<T> GetAsync<T>(HttpRequestMessage requestMessage, CancellationToken cancellationToken)
{
    Throw.ArgumentNull.WhenNull(requestMessage, nameof(requestMessage));
    this.ThrowIfDisposed();

    using (var data = await this.GetAsync(requestMessage, cancellationToken).ConfigureAwait(false))
    {
    var result = await JsonSerializer
        .DeserializeAsync<T>(data, cancellationToken: cancellationToken)
        .ConfigureAwait(false);

    return result;
    }
}

The performance gains are interesting. With NewtonSoft.Json:

Method Toolchain Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
GetAllBreachesAsync .NET Core 2.2 74.722 us 8.1300 us 0.4456 us 1.00 0.00 8.6670 - - 40.18 KB
GetAllBreachesAsync .NET Core 3.0 66.253 us 19.7596 us 1.0831 us 0.89 0.01 8.5449 0.4883 - 39.63 KB
GetBreachesAsync .NET Core 2.2 11.682 us 5.4719 us 0.2999 us 1.00 0.00 2.1210 - - 9.84 KB
GetBreachesAsync .NET Core 3.0 8.118 us 0.9361 us 0.0513 us 0.70 0.01 2.1057 - - 9.73 KB
GetBreachesAsync_BreachMode .NET Core 2.2 11.130 us 4.2680 us 0.2339 us 1.00 0.00 2.1210 - - 9.84 KB
GetBreachesAsync_BreachMode .NET Core 3.0 8.340 us 0.1332 us 0.0073 us 0.75 0.02 2.1057 0.0153 - 9.73 KB
GetPastesAsync .NET Core 2.2 129.509 us 114.5536 us 6.2791 us 1.00 0.00 8.3008 0.2441 - 39.03 KB
GetPastesAsync .NET Core 3.0 112.601 us 16.0286 us 0.8786 us 0.87 0.05 8.1787 0.4883 - 37.92 KB
IsPwnedPasswordAsync .NET Core 2.2 137.335 us 129.6631 us 7.1073 us 1.00 0.00 31.9824 - - 148.14 KB
IsPwnedPasswordAsync .NET Core 3.0 122.137 us 15.9686 us 0.8753 us 0.89 0.05 31.0059 2.9297 - 142.75 KB

With System.Text.Json:

Method Toolchain Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
GetAllBreachesAsync .NET Core 2.2 48.674 us 1.845 us 0.1011 us 1.00 0.00 5.8594 - - 27.07 KB
GetAllBreachesAsync .NET Core 3.0 41.674 us 16.627 us 0.9114 us 0.86 0.02 5.6152 0.3052 - 25.93 KB
GetBreachesAsync .NET Core 2.2 10.619 us 8.765 us 0.4804 us 1.00 0.00 0.9613 - - 4.48 KB
GetBreachesAsync .NET Core 3.0 9.633 us 21.701 us 1.1895 us 0.91 0.08 0.9155 - - 4.22 KB
GetBreachesAsync_BreachMode .NET Core 2.2 10.150 us 2.370 us 0.1299 us 1.00 0.00 0.9613 - - 4.48 KB
GetBreachesAsync_BreachMode .NET Core 3.0 8.481 us 9.697 us 0.5315 us 0.84 0.04 0.9155 - - 4.22 KB
GetPastesAsync .NET Core 2.2 88.236 us 27.455 us 1.5049 us 1.00 0.00 6.1035 - - 28.49 KB
GetPastesAsync .NET Core 3.0 76.838 us 39.508 us 2.1655 us 0.87 0.03 5.8594 0.2441 - 27.23 KB
IsPwnedPasswordAsync .NET Core 2.2 133.719 us 89.381 us 4.8993 us 1.00 0.00 31.9824 - - 148.14 KB
IsPwnedPasswordAsync .NET Core 3.0 118.261 us 12.301 us 0.6742 us 0.89 0.03 31.0059 3.0518 - 142.75 KB

The savings on especially memory usage are very impressive. The gains in performance are nice too, but since we're going off-box to a service saving 20 nanoseconds while the call to the service takes easily 100+ milliseconds feels like a bit unimportant. I'll take them though, faster is (almost) always better.

akamsteeg commented 4 years ago

With System.Text.Json 4.7.0 the results are even more impressive:

Method Toolchain Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
GetAllBreachesAsync .NET Core 2.1 42.410 us 2.8998 us 0.1589 us 1.00 0.00 4.3335 - - 20.17 KB
GetAllBreachesAsync .NET Core 3.0 34.890 us 1.9815 us 0.1086 us 0.82 0.01 4.1504 0.1831 - 19.28 KB
GetAllBreachesAsync net472 64.528 us 5.2056 us 0.2853 us 1.52 0.01 4.5166 0.1221 - 21.28 KB
GetBreachesAsync .NET Core 2.1 9.258 us 1.3894 us 0.0762 us 1.00 0.00 0.9155 - - 4.27 KB
GetBreachesAsync .NET Core 3.0 6.744 us 0.6732 us 0.0369 us 0.73 0.01 0.8698 - - 4.02 KB
GetBreachesAsync net472 12.661 us 3.4387 us 0.1885 us 1.37 0.01 1.2360 - - 5.73 KB
GetBreachesAsync_BreachMode .NET Core 2.1 11.026 us 36.6618 us 2.0096 us 1.00 0.00 0.9155 - - 4.27 KB
GetBreachesAsync_BreachMode .NET Core 3.0 6.693 us 4.3693 us 0.2395 us 0.62 0.12 0.8698 - - 4.02 KB
GetBreachesAsync_BreachMode net472 13.597 us 8.9619 us 0.4912 us 1.26 0.25 1.2360 - - 5.73 KB
GetPastesAsync .NET Core 2.1 70.643 us 8.4793 us 0.4648 us 1.00 0.00 4.7607 - - 22.23 KB
GetPastesAsync .NET Core 3.0 60.057 us 11.7007 us 0.6414 us 0.85 0.01 4.5166 0.2441 - 20.97 KB
GetPastesAsync net472 111.125 us 9.5911 us 0.5257 us 1.57 0.01 5.0049 0.2441 - 23.37 KB
IsPwnedPasswordAsync .NET Core 2.1 77.462 us 5.3375 us 0.2926 us 1.00 0.00 25.6348 - - 118.14 KB
IsPwnedPasswordAsync .NET Core 3.0 56.008 us 3.6827 us 0.2019 us 0.72 0.00 25.2686 4.1504 - 116.58 KB
IsPwnedPasswordAsync net472 101.628 us 17.0611 us 0.9352 us 1.31 0.02 26.6113 3.5400 - 123.41 KB