Skip to content

WebWorkers

Add Nuget

NuGet

Example Program.cs

// Program.cs
...
using SpawnDev.BlazorJS;
using SpawnDev.BlazorJS.WebWorkers;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
// Add SpawnDev.BlazorJS.BlazorJSRuntime
builder.Services.AddBlazorJSRuntime();
// Add SpawnDev.BlazorJS.WebWorkers.WebWorkerService
builder.Services.AddWebWorkerService(webWorkerService =>
{
    // Optionally configure the WebWorkerService service before it is used
    // Default WebWorkerService.TaskPool settings: PoolSize = 0, MaxPoolSize = 1, AutoGrow = true
    // Below sets TaskPool max size to 2. By default the TaskPool size will grow as needed up to the max pool size.
    // Setting max pool size to -1 will set it to the value of navigator.hardwareConcurrency
    webWorkerService.TaskPool.MaxPoolSize = 2;
    // Below is telling the WebWorkerService TaskPool to set the initial size to 2 if running in a Window scope and 0 otherwise
    // This starts up 2 WebWorkers to handle TaskPool tasks as needed
    // Setting this to -1 will set the initial pool size to max pool size
    webWorkerService.TaskPool.PoolSize = webWorkerService.GlobalScope == GlobalScope.Window ? 2 : 0;
});
// Add services
builder.Services.AddSingleton<IFaceAPIService, FaceAPIService>();
builder.Services.AddSingleton<IMathsService, MathsService>();
builder.Services.AddScoped((sp) => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// ...

// build and Init using BlazorJSRunAsync (instead of RunAsync)
await builder.Build().BlazorJSRunAsync();

WebWorkerService.TaskPool

WebWorkerService.TaskPool is ready to call any registered service in a background thread. If WebWorkers are not supported, TaskPool calls will run in the Window scope. The TaskPool settings can be configured when calling AddWebWorkerService(). By default, no worker tasks are started automatically at startup and the max pool size is set to 1.

Supported Calling Conventions

This applies to any class that inherits from AsyncCallDispatcher such as the instances TaskPool, and WindowTask or the classes WebWorker, SharedWebWorker, and WebWorkerPool.

Expressions - Run(), Set()
- Supports generics, property get and set, asynchronous and synchronous method calls. - Supports calling private methods from inside the owning class.

Delegates - Invoke()
- Supports generics, asynchronous and synchronous method calls.
- Supports calling private methods from inside the owning class.

Interface proxy - GetService() - Supports generics, and asynchronous method calls. (uses DispatchProxy)
- Does not support static methods, private methods, synchronous calls, or properties. - Requires services to be registered using an interface.

Example that demonstrates using an Expression and a Delegate.

public class MyService
{
    WebWorkerService WebWorkerService;
    public MyService(WebWorkerService webWorkerService)
    {
        WebWorkerService = webWorkerService;
    }
    string WorkerMethod(string input)
    {
        return $"Hello {input} from {WebWorkerService.InstanceId}";
    }
    public async Task CallWorkerMethod()
    {
        // Call the private method WorkerMethod on this scope (normal)
        Console.WriteLine(WorkerMethod(WebWorkerService.InstanceId));

        // Call the private method WorkerMethod in a WebWorker thread using an Expression
        Console.WriteLine(await WebWorkerService.TaskPool.Run(() => WorkerMethod(WebWorkerService.InstanceId)));

        // Call the private method WorkerMethod in a WebWorker thread using a Delegate
        Console.WriteLine(await WebWorkerService.TaskPool.Invoke(WorkerMethod, WebWorkerService.InstanceId));
    }
}

WebWorkerService.WindowTask

Sometimes WebWorkers may need to call back into the Window thread that owns them. This can easily be achieved using WebWorkerService.WindowTask.

public class MyService
{
    WebWorkerService WebWorkerService;
    public MyService(WebWorkerService webWorkerService)
    {
        WebWorkerService = webWorkerService;
    }
    string CalledOnWindow(string input)
    {
        return $"Hello {input} from {WebWorkerService.InstanceId}";
    }
    public async Task StartedInWorker()
    {   
        // Do some work ...         
        // report back to Window (Expression example)
        // Call the private method CalledOnWindow on the Window thread using an Expression
        Console.WriteLine(await WebWorkerService.WindowTask.Run(() => CalledOnWindow(WebWorkerService.InstanceId)));

        // Do some more work ...         
        // report back to Window again (Delegate example)
        // Call the private method CalledOnWindow on the Window thread using a Delegate
        Console.WriteLine(await WebWorkerService.WindowTask.Invoke(CalledOnWindow, WebWorkerService.InstanceId));
    }
}

WebWorker

You can use the WebWorkerService properties SharedWebWorkerSupported and WebWorkerSupported to check for support.

Example component code that uses the service IMyService in a WebWorker if supported and falls back to the Window scoped IMyService if not supported.

[Inject]
WebWorkerService workerService { get; set; }

[Inject]
IServiceProvider serviceProvider { get; set; }

// MyServiceAuto will be IMyService running in the WebWorker context if available and IMyService running in the Window context if not
IMyService MyService { get; set; }

WebWorker? webWorker { get; set; }

protected override async Task OnInitializedAsync()
{
    // GetWebWorker() will return null if workerService.WebWorkerSupported == false
    webWorker = await workerService.GetWebWorker();
    // get the WebWorker's service instance if available or this Window's service instance if not
    MyService = webWorker != null ? webWorker.GetService<IMyService>() : serviceProvider.GetService<IMyService>();
    await base.OnInitializedAsync();
}

Another example with a progress callback.

// Create a WebWorker

[Inject]
WebWorkerService workerService { get; set; }

 // ...

var webWorker = await workerService.GetWebWorker();

// Call GetService<ServiceInterface> on a web worker to get a proxy for the service on the web worker.
// GetService can only be called with Interface types
var workerMathService = webWorker.GetService<IMathsService>();

// Call async methods on your worker service 
var result = await workerMathService.CalculatePi(piDecimalPlaces);

// Action types can be passed for progress reporting
var result = await workerMathService.CalculatePiWithActionProgress(piDecimalPlaces, new Action<int>((i) =>
{
    // the worker thread can call this method to report progress if desired
    piProgress = i;
    StateHasChanged();
}));

SharedWebWorker

Calling GetSharedWebWorker in another window with the same sharedWorkerName will return the same SharedWebWorker

// Create or get SHaredWebWorker with the provided sharedWorkerName
var sharedWebWorker = await workerService.GetSharedWebWorker("workername");

// Just like WebWorker but shared
var workerMathService = sharedWebWorker.GetService<IMathsService>();

// Call async methods on your shared worker service
var result = await workerMathService.CalculatePi(piDecimalPlaces);

Send events

// Optionally listen for event messages
worker.OnMessage += (sender, msg) =>
{
    if (msg.TargetName == "progress")
    {
        PiProgress msgData = msg.GetData<PiProgress>();
        piProgress = msgData.Progress;
        StateHasChanged();
    }
};

// From SharedWebWorker or WebWorker threads send an event to connected parents
workerService.SendEventToParents("progress", new PiProgress { Progress = piProgress });

// Or on send an event to a connected worker
webWorker.SendEvent("progress", new PiProgress { Progress = piProgress });

Worker Transferable JSObjects

Faster is better. SpawnDev WebWorkers use transferable objects by default for better performance, but it can be disabled with WorkerTransferAttribute. Setting WorkerTransfer to false will cause the property, return value, or parameter to be copied to the receiving thread instead of transferred.

Example

public class ProcessFrameResult : IDisposable
{
    [WorkerTransfer(false)]
    public ArrayBuffer? ArrayBuffer { get; set; }
    public byte[]? HomographyBytes { get; set; }
    public void Dispose(){
        ArrayBuffer?.Dispose();
    }
}

[return: WorkerTransfer(false)]
public async Task<ProcessFrameResult?> ProcessFrame([WorkerTransfer(false)] ArrayBuffer? frameBuffer, int width, int height, int _canny0, int _canny1, double _needlePatternSize)
{
    var ret = new ProcessFrameResult();
    // ...
    return ret;
}

In the above example; the WorkerTransferAttribute on the return type set to false will prevent all properties of the return type from being transferred.

Transferable JSObject types. Source MDN

ArrayBuffer
MessagePort
ReadableStream
WritableStream
TransformStream
AudioData
ImageBitmap
VideoFrame
OffscreenCanvas
RTCDataChannel