Search code examples
javascriptperformanceblazorblazor-webassembly

Blazor wasm invoke javascript, pass large array is very slow


I have a blazor wasm app. In that I am invoking a javascript function that receives an array of double. This is very slow, especially when the array is large.

For a test see the code below:

javascript ("test.js"):

function testSumArray(array) {
    var t0 = performance.now();
    sumArray(array);
    var t1 = performance.now();
    console.log('From JS, time to sum: ' + (t1 - t0) / 1000 + ' s');
}

function sumArray(array) {
    var i;
    var s = 0;
    for (i = 0; i < array.length; i++) {
        s += array[i];
    }
    return s;
}

And c# code (index.razor):

@page "/"
@inject IJSRuntime JSRuntime;

@using System.Text
@using BlazorWasmOnlyTest.Shared
<h1>Hello, world!</h1>

Welcome to your new app.

<div class="container">
    <div class="row mb-2">
        <div class="col">
            <button class="btn btn-primary" @onclick="@TestInvokeJS">Test invoke js</button>
        </div>
    </div>
</div>

@code {
    private int _id;
    private string _status = "";
    private DataInputFileForm _dataInputFileForm;

    private async void TestInvokeJS()
    {
        var n = 100000;
        var array = new double[n];
        for (int i = 0; i < n; i++)
        {
            array[i] = i;
        }
        var w = new System.Diagnostics.Stopwatch();
        w.Start();
        await JSRuntime.InvokeVoidAsync("testSumArray",array);
        w.Stop();
        Console.WriteLine($"C# time to invoke js and sum: {w.ElapsedMilliseconds/1000:F3} s");
    }
}

And for completion - index.html:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorWasmOnlyTest</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <script src="js/test.js"></script>
</head>

<body>
    <app>Loading...</app>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
</body>

</html>

Running this once gives the following output on my machine:

From JS, time to sum: 0.0037800000282004476 s

C# time to invoke js and sum: 7.000 s

That seems like a pretty high overhead time... Does anyone know if there is a way to speed this up (the real function does something I presently cannot do in Blazor/C# - updating a layer in Leaflet)

EDIT: I have tried the synchronous method described here, without any difference in execution time.

c#:

    var jsInProcess2 = (IJSInProcessRuntime)JSRuntime;
    jsInProcess2.InvokeVoid("testSumArray", array);

js: javascript same as testSumArray above.

EDIT 2:

I have tried passing a JSON string with synchronous interop:

c#:

    var jsInProcess3 = (IJSInProcessRuntime)JSRuntime;
    var array_json3 = System.Text.Json.JsonSerializer.Serialize(array);
    jsInProcess3.InvokeVoid("testSumArray3", array_json);

js:

function testSumArray3(array_json_string) {
    var t0 = performance.now();
    var array = JSON.parse(array_json_string);
    var s = sumArray(array);
    var t1 = performance.now();
    console.log('From JS, time to sum: ' + (t1 - t0) / 1000 + ' s');
    console.log('Array sum = ' + s);
}

and with JSON string and InvokeUnmarshalled js interopcall:

c#:

    var jsInProcess4 = (Microsoft.JSInterop.WebAssembly.WebAssemblyJSRuntime)JSRuntime;
    var array_json4 = System.Text.Json.JsonSerializer.Serialize(array);
    jsInProcess4.InvokeUnmarshalled<string,string>("testSumArray4", array_json4);

js:

function testSumArray4(array_mono_json_string) {
    var t0 = performance.now();
    const array_json_string = BINDING.conv_string(array_mono_json_string);
    var array = JSON.parse(array_json_string);
    var s = sumArray(array);
    var t1 = performance.now();
    console.log('From JS, time to sum: ' + (t1 - t0) / 1000 + ' s');
    console.log('Array sum = ' + s);
}

All methods take approximately the same time, 6-7 secs to complete (of that about 0.0015-0.006 seconds in the javascript function).

I have tried to figure out how to call unmarshalled passing an array, using BINDING.mono_array_to_js_array found in in this file but that throws a long error. c#:

    var sum = jsInProcess4.InvokeUnmarshalled<double[],double>("testSumArray5",array)

js:

function testSumArray5(array_in) {
    var t0 = performance.now();
    var array = BINDING.mono_array_to_js_array(array_in);
    console.log(array[0]);
    var s = sumArray(array);
    var t1 = performance.now();
    console.log('From JS, time to sum: ' + (t1 - t0) / 1000 + ' s');
    console.log('Array sum = ' + s);
    return s;
}

EDIT 2024-01-02


It seems that (at least with NET 8) this is no issue anymore as the default IJSRuntime.InvokeVoidAsync seems to be (almost) as fast, and the InvokeUnmarshalled is marked [System.Obsolete("This method is obsolete. Use JSImportAttribute instead.")] at least since NET 7. However I do not not understand the JSImportAttribute and I got almost as fast without any attributes.

Solution

  • Just found the way to use .net byte or float arrays in js.

    c#:

    [Inject] //Injected JSRuntime from Blazor DI
    private IJSRuntime JSRuntime { get; set; }
    
    byte[] bytes1;
    float[] floats2;
    ...
    if (JSRuntime is IJSUnmarshalledRuntime webAssemblyJSRuntime)
    {
        webAssemblyJSRuntime.InvokeUnmarshalled<byte[], float[], object> 
            ("downloadArray", bytes1, floats2);
    }
    

    JavaScript:

    function downloadArray(bytes1, floats2) {
        // Easy way to convert Uint8 arrays
        var byteArray = Blazor.platform.toUint8Array(bytes1);
    
        // Adapted method above for float32
        var m = floats2 + 12;
        var r = Module.HEAP32[m >> 2]
        var j = new Float32Array(Module.HEAPF32.buffer, m + 4, r);
    }
    

    Here result is Uint8Array and Float32Array objects from byte[] and float[] respectively within a reasonable period of time.

    May be there are any approaches to get js arrays because you have access to the whole .net heap from ArrayBuffers like Module.HEAPU8 (heap inside Uint8Array) or Module.HEAPF32 (heap inside Float32Array) and can easily access objects by pointer from InvokeUnmarshalled parameters.