Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Blazor UI locking

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>
}
like image 439
Jed Avatar asked Dec 03 '20 17:12

Jed


1 Answers

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:

  • JSON.Net - JsonTextReader
  • System.Text.Json - Utf8JsonReader
  • Utf8Json - JsonReader

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.).
like image 95
A. Milto Avatar answered Sep 19 '22 23:09

A. Milto