8

UPDATE: Its the conversion from json to the class that is locking the thread. I have tried the same process with a smaller packet of data and the UI didnt freeze. So the question is how to overcome this?

I am trying to get a value from the JSON that is parsed into classes:

 public async Task<BitcoinDetails> GetData()
    {

        return await _httpClient.GetFromJsonAsync<BitcoinDetails>("https://localhost:5001/proxy");

    }

I use the OnInitializedAsync to load the data into the view however the following code locks the UI

_bitcoinDetails = new BitcoinDetails();
        _bitcoinDetails = await _bitcoinApiService.GetData();

        var price = _bitcoinDetails.data.Find(x => x.symbol == "BTC");
        if (price == null)
        {
            _bitcoinPrice = 0;
        }

        _bitcoinPrice = double.Parse(price.quote.USD.price.ToString());

How do I restructure this code to load the data without locking the UI?

View code:

 @if (_bitcoinDetails == null)
{
    <p><em>Loading...</em></p>
}
else
{
<h3>BTC:@_bitcoinPrice</h3>
}
Jed
  • 929
  • 2
  • 19
  • 32
  • Which line is blocking the UI thread? Your service call is awaited, so that's async. – gunr2171 Dec 03 '20 at 18:00
  • Also, instead of using OnInitializedAsync, try another [lifecycle method](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle?view=aspnetcore-5.0), like OnAfterRenderAsync. – gunr2171 Dec 03 '20 at 18:04
  • @gunr2171 Same issue with OnAfterRenderAsync – Jed Dec 03 '20 at 18:14
  • Which line is blocking the UI thread? – mxmissile Dec 03 '20 at 18:14
  • This is the line _bitcoinDetails = await _bitcoinApiService.GetData(); – Jed Dec 03 '20 at 18:15
  • 1
    Then look inside `_bitcoinApiService.GetData()`. Someone must be [not doing something right](https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html) in there. – GSerg Dec 03 '20 at 18:29
  • @GSerg There is nothing complex there. I have added that method – Jed Dec 03 '20 at 18:47
  • 1
    I have seen httpclient awaited calls blocking in a situation with network problems, such as when DNS resolution was not fast. – GSerg Dec 03 '20 at 18:51
  • @Jed, is the BitcoinDetails object populated with data ? – enet Dec 03 '20 at 18:51
  • OnAfterRenderAsync + https://stackoverflow.com/questions/56604886/blazor-display-wait-or-spinner-on-api-call – dani herrera Dec 03 '20 at 19:47
  • Describe "locks the UI". When you do the `@if (forecasts == null) ...loading...` trick from the FetchData page then you at least have a something to look at. Your API is slow. – H H Dec 03 '20 at 21:03
  • @HenkHolterman I have that there already – Jed Dec 04 '20 at 13:59
  • @enet it works perfectly however it locks the UI as in you cannot click anywhere else as if the call were to take 1 minute then the UI freezes for that time – Jed Dec 04 '20 at 14:00
  • @HenkHolterman The lock means that when the call is being run you cannot click on the navigation bar. – Jed Dec 04 '20 at 14:04
  • @Jed only UI freezes or also your Visual Studio or the whole system ? I mean maybe the backend API itself freezes your computer ? – nAviD Dec 06 '20 at 10:33

1 Answers1

11

Multithreading in Blazor WebAssembly

Blazor WebAssembly doesn't have real multithreading support yet. All tasks effectively run on the same thread as the UI, which means any CPU-intensive work that takes longer than a few milliseconds to execute may cause noticable freezes in the user interface.

The situation with multithreading in Blazor WebAssembly isn't likely to improve until .NET 6 (November 2021), unfortunately. Until then the workaround is to manually introduce short pauses into a flow of a CPU-intensive task so that the UI can take control during these breaks and do its work:

async Task PerformCpuIntensiveWorkAsync()
{
    for (int i = 0; i < 100; i++)
    {
        PerformOneHundredthOfWork();
        await Task.Delay(1);
    }
}

Deserialization of Large JSON

Most JSON serializers provide low-level API that gives you full control over the deserialization process:

If you need to deserialize a large JSON, for example, containing an array of 100,000 cars

[
  { "make": "Ford", "model": "Mustang", "year": 2000 },
  { "make": "Honda", "model": "Civic", "year": 2005 },
  { "make": "Chevrolet", "model": "Corvette", "year": 2008 },
  ...
]

download this JSON at https://api.npoint.io/d159a22063995b37c52d

this is how you can introduce short breaks into the process of deserialization using JSON.Net:

using Newtonsoft.Json;
using System.IO;

...

public class Car
{
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
}

...

using var jsonStream = await Http.GetStreamAsync("https://api.npoint.io/d159a22063995b37c52d");

List<Car> cars = await DeserializeCarsAsync(jsonStream);

static async Task<List<Car>> DeserializeCarsAsync(Stream jsonStream)
{
    using var streamReader = new StreamReader(jsonStream);
    using var jsonReader = new JsonTextReader(streamReader);
    var serializer = new JsonSerializer();

    if (!jsonReader.Read())
        throw new JsonException($"Deserialization failed at line {jsonReader.LineNumber} position {jsonReader.LinePosition}.");
    if (jsonReader.TokenType != JsonToken.StartArray)
        throw new JsonException($"Deserialization failed at line {jsonReader.LineNumber} position {jsonReader.LinePosition}.");

    List<Car> cars = new List<Car>();
    while (true)
    {
        if (!jsonReader.Read())
            throw new JsonException($"Deserialization failed at line {jsonReader.LineNumber} position {jsonReader.LinePosition}.");
        if (jsonReader.TokenType == JsonToken.EndArray)
            return cars;
        if (jsonReader.TokenType != JsonToken.StartObject)
            throw new JsonException($"Deserialization failed at line {jsonReader.LineNumber} position {jsonReader.LinePosition}.");

        var car = serializer.Deserialize<Car>(jsonReader);
        cars.Add(car);

        // Pause after every 10th deserialized car:

        if (cars.Count % 10 == 0)
            await Task.Delay(1);
    }
}

It does look overcomplicated and gets even worse if you have to deal with nested collections, but it solves the problem.

Other Options

  • Work with smaller JSON, if possible. It looks like you're either fetching quotes or listings from CoinMarketCap using a proxy. You get the entire list but only need one item - BTC. It's hard to say without knowing the details whether this will suit you, but it's possible to ask CoinMarketCap server to filter the results for you and only return the data for the BTC quote - this would result in much smaller JSON: https://sandbox-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?CMC_PRO_API_KEY=b54bcf4d-1bca-4e8e-9a24-22ff2c3d462c&symbol=BTC .

  • Try faster JSON serializer. Utf8Json looks promising.

  • If you need to deserialize only a few fields from a large JSON, there's many potential optimizations:

    • Try deserializing into a class with fewer fields. E.g., if you deserialize Quote objects and only need to obtain their prices, try using a class with the only property Price to deserialize to: class Quote { decimal Price {get; set;} }
    • Try deserializing only the specific JSON nodes that you need using JsonDocument. It would still require parsing the entire JSON first to create a map of its nodes though, but anyway it should be faster than deserializing the entire JSON.
    • If you use a serializer's low-level API, most of them allow skipping JSON elements while parsing (JsonReader.Skip, Utf8JsonReader.Skip, JsonReader.ReadNextBlock, etc.).
A. Milto
  • 2,253
  • 1
  • 13
  • 19