Skip to content

Stack switching#3210

Merged
hoodmane merged 155 commits intopyodide:nextfrom
hoodmane:suspender
Jul 31, 2023
Merged

Stack switching#3210
hoodmane merged 155 commits intopyodide:nextfrom
hoodmane:suspender

Conversation

@hoodmane
Copy link
Copy Markdown
Member

@hoodmane hoodmane commented Oct 30, 2022

Uses the JS Promise integration stack switching API to allow blocking for JavaScript promises and PyodideFuture objects.

@hoodmane hoodmane changed the title Work on suspenders Work on stack switching Oct 30, 2022
@hoodmane hoodmane mentioned this pull request Oct 30, 2022
@hoodmane
Copy link
Copy Markdown
Member Author

We can also use this to insert a sleep into the Python main loop, so that the print statements from the Python loop and the Javascript loop are interleaved:

for(let i = 1; i < 100; i++){
  sleep(100*i).then(() => console.log("jsi", i));
}
f = py.runPython(`
def f():
  for i in range(100_000):
    if i % 100 == 0:
      print("pyi", i)
  import pytest # segfaults if pytest hasn't been imported before =(
f
`);
await f.callSyncifying({});

So the stack switching is extremely promising. It also seems likely that we will be able to make a version of Pyodide that uses stack switching if it is available but gracefully falls back if it is not available.

@ryanking13
Copy link
Copy Markdown
Member

Thanks for the investigation @hoodmane! Indeed it looks quite promising.

with node v18.x

Nice. Since node v18 is now in an LTS state, I think it is okay to use new features of v18.

@hoodmane
Copy link
Copy Markdown
Member Author

Apparently they completely rearranged this proposal though. It seems like it now requires wasm reference types to use, and they may have removed the WebAssembly.Suspender API. Wasm reference types have been available in Firefox since v79 July 2020, in Safari since v15.0 released in September 2021, and in Chrome since v96 released November 2021. So it would be nice not to use them. But also, there is currently no way to make C/C++ code that emits wasm reference types, the only documented way to use them is via assembly code.
emscripten-core/emscripten#15878

Both the backwards compatibility problem and the how-do-we-even-emit-code problem could be solved by hand coding the wrapper like what I did for createDyncallWrapper but this will be a pain:
https://github.com/emscripten-core/emscripten/pull/17328/files#diff-8bb085f397f4174fe93a8cf0cda27a12a2647a4afe8ce7116ed740935f5d1188

@hoodmane
Copy link
Copy Markdown
Member Author

I will have to try rewriting this to use the newer approach and see if it fixes the segfaults...

@hoodmane
Copy link
Copy Markdown
Member Author

@hoodmane
Copy link
Copy Markdown
Member Author

Okay, so this works great in node v19 with flags --experimental-wasm-type-reflection and --experimental-wasm-stack-switching or in chrome version 109.0.5384.0 (Official Build) dev with the Experimental WebAssembly Stack Switching flag set to enabled. I have two tests: one shows that we can allow background tasks to occur intermingled with synchronous Python. Another uses an import hook to download and install pytest when an import is attempted.

This feature magically transcends our most annoying limitations.

I think the trickiest design question is how to deal with mixing syncifying calls with non syncifying calls. Syncifying calls always return a promise. Non syncifying calls trap if a promise is syncified. It should be possible to set a flag on entry to the vm indicating whether it was a syncifying entry or a nonsyncifying one. A more sophisticated approach to reentrancy might be needed as well.

@hoodmane
Copy link
Copy Markdown
Member Author

hoodmane commented Nov 1, 2022

We should probably look into using the wasm binary toolkit to convert wat text source files to wasm. Then we can convert the wasm to base64, create javascript files which define the module binaries, and inject these javascript files with --pre-js.

@hoodmane
Copy link
Copy Markdown
Member Author

@rth @ryanking13 this is ready to review. By splitting off a bunch of stuff, I've gotten it down to 408 lines added for features and 326 lines for tests which is more than a 2x decrease in size from before. The current state of this PR leaves out three things:

  • ctypes integration
  • stack state management
  • Python state management

I have dealt with all three of these issues on local branches and will make more followup PRs after this is merged to fix them.

Copy link
Copy Markdown
Member

@ryanking13 ryanking13 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @hodmane! This is awesome.

The changes generally look good to me. I just have a few comments (questions). Probably it would be good to have Roman's review too before merging.

@rth
Copy link
Copy Markdown
Member

rth commented Jul 11, 2023

Thanks for doing this work! I'll review it tomorrow.

Copy link
Copy Markdown
Member

@rth rth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's certainly ambitious work that would solve a lot of Podide's problems! Thanks @hoodmane !

I'm not following all of what's happening on the lower level side, but some more comments are below and +1 from me for merging.

Maybe the major point is to make sure that there are no changes in behavior if JSPI is disabled, so maybe we should still run tests without JSPI (but rather adds CI runs where it's enabled). I'm not sure.

For the documentation, are you planning to add it separately (even if it's experimental). In particular, I would be interested in how this compares, broadly speaking, to ASYNCIFY=2 in Emscripten which also uses JSPI from what I understand.


import { build } from "esbuild";
import { readFileSync } from "node:fs";
import loadWabt from "../../js/node_modules/wabt/index.js";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid the hardcoded assumption as to where node_modules are? It doesn't work with a non relative import?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem is that this directory isn't under node_modules. Most likely there is a better way, like moving this script into src/js or something. But I'm not sure what the best approach is.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to leave fixing this to a followup.

"--enable-experimental-webassembly-features",
]
)
pytest_pyodide.runner.NODE_FLAGS.extend(["--experimental-wasm-stack-switching"])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the idea is that we are still running Firefox tests without JSPI right?

I wonder if it wouldn't be good to still run some tests for chrome (and possibly node) without this flag. Since that's the default configuration where most users are currently.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to run the fpcast tests, the C++ exception handling tests, the ctypes tests, the rust tests, and the recursion limit tests in the core test suite in both configurations. The remaining tests can be run in just one or the other.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix this in a followup PR.

@hoodmane
Copy link
Copy Markdown
Member Author

@rth okay to merge this?

@rth
Copy link
Copy Markdown
Member

rth commented Jul 31, 2023

Sorry for the delay @hoodmane . Yes, no objection on my part. Thanks for doing all this work!

@hoodmane hoodmane merged commit 4b41efb into pyodide:next Jul 31, 2023
@hoodmane hoodmane deleted the suspender branch July 31, 2023 09:27
hoodmane added a commit that referenced this pull request Jul 31, 2023
Uses the JS Promise integration stack switching API to allow blocking for JavaScript promises and
`PyodideFuture` objects. It's a bit complicated...
This doesn't include support for reentrant switching, currently doing that will corrupt the Python VM.
hoodmane added a commit that referenced this pull request Sep 13, 2023
Uses the JS Promise integration stack switching API to allow blocking for JavaScript promises and
`PyodideFuture` objects. It's a bit complicated...
This doesn't include support for reentrant switching, currently doing that will corrupt the Python VM.
hoodmane added a commit to hoodmane/pyodide that referenced this pull request Sep 15, 2023
Uses the JS Promise integration stack switching API to allow blocking for JavaScript promises and
`PyodideFuture` objects. It's a bit complicated...
This doesn't include support for reentrant switching, currently doing that will corrupt the Python VM.
hoodmane added a commit to hoodmane/pyodide that referenced this pull request Oct 19, 2023
Uses the JS Promise integration stack switching API to allow blocking for JavaScript promises and
`PyodideFuture` objects. It's a bit complicated...
This doesn't include support for reentrant switching, currently doing that will corrupt the Python VM.
hoodmane added a commit to hoodmane/pyodide that referenced this pull request Oct 19, 2023
Uses the JS Promise integration stack switching API to allow blocking for JavaScript promises and
`PyodideFuture` objects. It's a bit complicated...
This doesn't include support for reentrant switching, currently doing that will corrupt the Python VM.
hoodmane added a commit that referenced this pull request Oct 21, 2023
Uses the JS Promise integration stack switching API to allow blocking for JavaScript promises and
`PyodideFuture` objects. It's a bit complicated...
This doesn't include support for reentrant switching, currently doing that will corrupt the Python VM.
lesteve pushed a commit to lesteve/pyodide that referenced this pull request Nov 13, 2023
Uses the JS Promise integration stack switching API to allow blocking for JavaScript promises and
`PyodideFuture` objects. It's a bit complicated...
This doesn't include support for reentrant switching, currently doing that will corrupt the Python VM.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants