How to cancel a wasm process from within a webworker

For Chrome (only) you may use shared memory (shared buffer as memory). And raise a flag in memory when you want to halt. Not a big fan of this solution (is complex and is supported only in chrome). It also depends on how your query works, and if there are places where the lengthy query can check the flag.

Instead you should probably call the c++ function multiple times (e.g. for each query) and check if you should halt after each call (just send a message to the worker to halt).

What I mean by multiple time is make the query in stages (multiple function cals for a single query). It may not be applicable in your case.

Regardless, AFAIK there is no way to send a signal to a Webassembly execution (e.g. Linux kill). Therefore, you'll have to wait for the operation to finish in order to complete the cancellation.

I'm attaching a code snippet that may explain this idea.

worker.js:

... init webassembly

onmessage = function(q) {
	// query received from main thread.
	const result = ... call webassembly(q);
	postMessage(result);
}

main.js:

const worker = new Worker("worker.js");
const cancel = false;
const processing = false;

worker.onmessage(function(r) {
	// when worker has finished processing the query.
	// r is the results of the processing.
	processing = false;

	if (cancel === true) {
		// processing is done, but result is not required.
		// instead of showing the results, update that the query was canceled.
		cancel = false;
		... update UI "cancled".
		return;
	}
	
	... update UI "results r".
}

function onCancel() {
	// Occurs when user clicks on the cancel button.
	if (cancel) {
		// sanity test - prevent this in UI.
		throw "already cancelling";
	}
	
	cancel = true;
	
	... update UI "canceling". 
}

function onQuery(q) {
	if (processing === true) {
		// sanity test - prevent this in UI.
		throw "already processing";
	}
	
	processing = true;
	// Send the query to the worker.
	// When the worker receives the message it will process the query via webassembly.
	worker.postMessage(q);
}

An idea from user experience perspective: You may create ~two workers. This will take twice the memory, but will allow you to "cancel" "immediately" once. (it will just mean that in the backend the 2nd worker will run the next query, and when the 1st finishes the cancellation, cancellation will again become immediate).


Shared Thread

Since the worker and the C++ function that it called share the same thread, the worker will also be blocked until the C++ loop is finished, and won't be able to handle any incoming messages. I think the a solid option would minimize the amount of time that the thread is blocked by instead initializing one iteration at a time from the main application.

It would look something like this.

main.js  ->  worker.js  ->  C++ function  ->  worker.js  ->  main.js

Breaking up the Loop

Below, C++ has a variable initialized at 0, which will be incremented at each loop iteration and stored in memory. C++ function then performs one iteration of the loop, increments the variable to keep track of loop position, and immediately breaks.

int x;
x = 0; // initialized counter at 0

std::vector<JSONObject> data
for (size_t i = x; i < data.size(); i++)
{
    process_data(data[i]);

    x++ // increment counter
    break; // stop function until told to iterate again starting at x
}

Then you should be able to post a message to the web worker, which then sends a message to main.js that the thread is no longer blocked.

Canceling the Operation

From this point, main.js knows that the web worker thread is no longer blocked, and can decide whether or not to tell the web worker to execute the C++ function again (with the C++ variable keeping track of the loop increment in memory.)

let continueOperation = true
// here you can set to false at any time since the thread is not blocked here

worker.expensiveThreadBlockingFunction()
// results in one iteration of the loop being iterated until message is received below

worker.onmessage = function(e) {
    if (continueOperation) {
        worker.expensiveThreadBlockingFunction()
        // execute worker function again, ultimately continuing the increment in C++
    } {
        return false
        // or send message to worker to reset C++ counter to prepare for next execution
    }
}

Continuing the Operation

Assuming all is well, and the user has not cancelled the operation, the loop should continue until finished. Keep in mind you should also send a distinct message for whether the loop has completed, or needs to continue, so you don't keep blocking the worker thread.