js-promise-integration icon indicating copy to clipboard operation
js-promise-integration copied to clipboard

Do we really care if JS gets mixed in with wasm when suspending?

Open fgmccabe opened this issue 1 year ago • 1 comments

A significant fraction of the total effort around JSPI has revolved around the necessity of ensuring that we do not suspend JS code when suspending a WebAssembly computation. However, is it actually necessary to worry about this?

Run to completion

One of the foundational tenets of the Web platform is that triggering of callbacks from Promises is only possible when there is no other JS code activity. This is called 'run-to-completion' and results in the architecture we have today: Promise callbacks are triggered only by the (micro-)task runner of the Browser.

In addition, it seemed important that a JS programmer could not 'inadvertently' access coroutining capabilities. JS has coroutining - in the form of yield-style generators and async functions - that have specific syntax associated. It should not be possible for a JS programmer to access coroutining without using this special syntax (or by explicitly manipulating the Promise architecture). One reason for this is that the context of a program can change without its knowledge if the program is suspended and later resumed.

A thought experiment

However, simply calling any function involves suspending the caller. And the world can change across any function call and the careful programmer will not make unwarranted assumptions (such as by trying to cache a global variable by keeping a local copy of it).

Consider the following thought experiment: instead of having JS 'built-in' to the Browser platform, imagine that only WebAssembly were provided natively; and that any JS code in the Web application was compiled into WebAssembly.

In this world, there would be no WebAssembly<->JS boundary that would be accessible to the WebAssembly engine. We would not be able to enforce the restriction that JSPI may not suspend JS code.

However, the previous constraints on Promises would remain: only the Browser's task runner would be able to trigger callbacks. This is enforced by making the Promise callbacks not directly accessible from JS (or WebAssembly) code. And so, we would still have run-to-completion semantics of user code.

Status of implementation (in V8)

The current implementation status of V8 strongly enforces the boundary between JS and WebAssembly. Any attempt to suspend at the JSPI boundary will cause a trap in WebAssembly if there are any JS frames being suspended. Furthermore, we also switch to the 'main' stack if a JSPI coroutine invokes JS. With this implementation it is simply not safe to allow any JS frames to be suspended when a JSPI coroutine suspends.

There are many complications in allowing JS frames to be suspended; mostly relating to non-JS and non-WebAssembly issues. Such as inadvertently having C++ frames being suspended and complications arising from tools that walk the stack -- such as profilers and debuggers. Some of which are properly considered to be the domain of the embedder not the execution engine.

However, it may be that these are simply complications and not essential requirements that must be adopted by the JSPI specification.

fgmccabe avatar Jun 17 '24 19:06 fgmccabe

I'm not yet convinced that suspending JS frames in this way is compatible with (my notion of) run-to-completion. Consider the following JS code:

function foo(someFunc) {
  console.log('one');
  Promise.resolve().then(() => console.log('three'));
  someFunc();
  console.log('two');
}

If foo is called with a non-suspending someFunc, the output is guaranteed to be in the order:

one
two
three

(potentially with some delay before "three" due to the microtask queue). But if someFunc is a Wasm function that suspends using core stack-switching, along with its caller foo, then it's possible to get the interleaving

one
three
two

(where "two" shows up whenever the Wasm someFunc is resumed and returns).

The only way I can see this maintaining the expected interleaving is if the microtask queue itself were suspended as well. But I don't see how that would be compatible with spinning the browser's event loop while someFunc is suspended.

Consider the following thought experiment: instead of having JS 'built-in' to the Browser platform, imagine that only WebAssembly were provided natively; and that any JS code in the Web application was compiled into WebAssembly.

In this world, there would be no WebAssembly<->JS boundary that would be accessible to the WebAssembly engine. We would not be able to enforce the restriction that JSPI may not suspend JS code.

In this world it would be up to the Wasm -> JS system to maintain the appropriate invariants. Certainly it would be possible for a Wasm -> JS compiler to allow suspending JS, but that doesn't mean it would be a run-to-completion-conforming implementation of JS.

ajklein avatar Jun 21 '24 00:06 ajklein

Closing this. It seems that we do care.

fgmccabe avatar Oct 31 '24 22:10 fgmccabe