Reputation: 1352
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;
}
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.
Upvotes: 9
Views: 4260
Reputation: 126
Just found the way to use .net byte or float arrays in js.
[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);
}
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.
Upvotes: 11