Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #19427 +/- ##
=======================================
Coverage 98.51% 98.51%
=======================================
Files 177 177
Lines 22927 22992 +65
=======================================
+ Hits 22586 22651 +65
Misses 341 341 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Code size report: |
676df50 to
e0f217f
Compare
|
This is possibly of interest to @LeaNumworks upon whose work I have no doubt been building. Right now the Numworks simulator has A Bad Time if you're so bold as to use a loop. This is exactly the sort of behaviour this PR fixes successfully for our own simulator.
And since a picture - or a live demo - is worth a thousand words: https://gadgetoid.github.io/upyweb/ |
14f7e4d to
984cdc0
Compare
|
Thanks for tagging me, I forwarded it to our team! |
|
Good to know you've been getting some use out of this webassembly port! It's on my list to look into JSPI. But the first thing there is to see how Pyodide use it and what their user-facing API is with JSPI enabled. See eg https://blog.pyodide.org/posts/jspi/ |
|
They use the same general shape but they have wildly different ambitions to where I was going. I'm maintaining support for Asyncify (even if it's terribly, terribly slow) and adding JSPI on top, both supporting the ability to cooperatively yield to the browser with Pyodide is (afaict) all-in on JSPI and offers real
As for API surface specifically:
Notably there is a lot of overlap, I was solving a bunch of problems (quite unwittingly so) that Pyodide had already folded into their asyncio-first pattern. That said I think Pyodide leans into asyncio and still doesn't solve the problem I was targeting; being able to run Looks like what they offer is true I think we want both approaches, but the road to what Pyodide is doing seems much longer and more involved than getting what we have playing nicer. But also I appreciate you don't want to commit further to an API surface that's likely to dramatically change. |
|
I've walked what's presented here toward Pyodide by borrowing their import js
from jsffi import run_sync, can_run_sync
if can_run_sync():
resp = run_sync(js.fetch("https://example.com/data.json"))
data = run_sync(resp.json()) # blocking, no await needed
print(data.title)
run_sync(js.Promise.resolve(42))This is really useful for burying async calls into synchronous libraries- ie: where you'd normally expect a real device to do a blocking call (unless you're going significantly out of your way to work with asyncio, select, etc) I always use import js
import asyncio
async def main():
resp = await js.fetch("http://127.0.0.1:8000/")
text = await resp.text()
print(text)
await main()Suffice to say Still kinda marvellous that any of this even works - https://gadgetoid.github.io/upyweb/ui.html |
0b7dab4 to
e6cf9c1
Compare
|
I've dropped _jsfetch (redundant) and @micropython.native noop (out of scope) from this PR since it's already a huge burden for anyone to look over and test. I have applied some commit surgery to include an extra function to the asyncify-fast build (which unbreaks some file IO cases), since I'm keen to use this in production (it's significantly faster, but will likely encounter issues until all possible code paths are exercised and the necessary ones opted-in to asyncify's bloat). Note: I'm still having trouble with the fast build. I think this is walking slowly toward a serious proposal. Notably half of the files touched by this PR are tests, but there are still a huge number of changes to main.c and other spicy areas. Edit: After sinking far too much time into trying to catch all the edge cases in asyncify-fast for my purposes I'm tempted to drop it, though as just vanilla MicroPython it should be fine and may be of use to someone? Another, separate PR later perhaps. Edit: Latest push catches a regression in Ctrl+C handling and adds a test to guard for it. Otherwise it is mostly just non-functional cleanup to bring this change more in compliance with existing practises. |
e6cf9c1 to
be119cd
Compare
mp_js_hook (the Node stdin poll for the interrupt character) passed a
spurious argument to mp_hal_get_interrupt_char(), which takes none. Recent
Emscripten builds assert on the argument-count mismatch and abort the VM
mid-execution ("native function mp_hal_get_interrupt_char called with 1
args but expects 0"), which surfaces once the VM hook fires during a
longer-running script. Drop the argument.
Signed-off-by: Phil Howard <[email protected]>
Long-running or self-looping Python (a bare `while True:` game loop, a
busy computation) currently blocks the browser's main thread for as long
as it runs, because the MicroPython entry points execute to completion
synchronously. Add an optional cooperative yield so the VM periodically
hands the JS event loop a turn without the script having to `await`.
mpconfigport.h: MICROPY_ENABLE_VM_YIELD (default off) routes the existing
VM hook to mp_js_yield().
main.c: mp_js_yield() issues a throttled emscripten_sleep() from the hook,
and mp_js_sleep_ms() backs mp_hal_delay_ms(). Both suspend only when the
whole JS->MP call chain is suspend-capable (external_call_depth ==
suspendable_depth), so a synchronous re-entry from JS can't be unwound. A
MP_WEAK mp_js_yield_hook() lets an embedding run work at a safe point.
mphalport.c: mp_hal_delay_ms() sleeps via the event loop when safe.
api.js: an invoke()/settle() indirection issues the entry-point call the
right way for the build's backend (JSPI promising export, Asyncify
ccall({async:true}), or a plain synchronous ccall) and awaits the result
only when it is a Promise. The backend is announced by async_asyncify.js
and async_jspi.js, added to SRC_JS by the variant; a plain build stays
synchronous.
Makefile: make CSTD and SUPPORT_LONGJMP overridable by variants; add a
test_async target that runs the async-only fixtures (which SKIP elsewhere)
against a suspend-capable build.
variants/asyncify: reference Asyncify build with the yield enabled.
variants/jspi: experimental build using Emscripten JSPI (Wasm stack
switching) instead of Asyncify.
tests/ports/webassembly/async_yield.mjs: yield smoke test, skipped unless
the build is suspend-capable.
tools/ci.sh: build the asyncify variant and run `make test_async` on it.
The default standard and pyscript variants are unchanged and remain
synchronous.
Signed-off-by: Phil Howard <[email protected]>
A self-looping program (a bare `while True:` game loop) never returns to the top level, which broke two assumptions of the port: - Stopping meant tearing the whole instance down. Expose interrupt() (mp_sched_keyboard_interrupt) so a host can raise KeyboardInterrupt and stop a running script in place, then reuse the live instance. mp_js_sleep_ms() now slices the sleep and handles pending exceptions between slices, so time.sleep() is promptly interruptible too. - Under MICROPY_GC_SPLIT_HEAP_AUTO, gc_collect() defers collection to the top level, which a forever-loop never reaches, so the heap grew unbounded to the wasm ceiling. Collect mid-execution from the VM yield hook instead. Asyncify scans the stack + registers; JSPI can't (switched stack, no register spill), so it scans the tracked C-stack range with bounds captured at the outermost entry. gc_alloc_threshold bounds the transient garbage held between collects. main.c: interrupt / sliced sleep, and the mid-execution collector (gc_collect_if_pending, gc_collect_cstack, MP_JS_NOTE_CSTACK_TOP, and the MICROPY_GC_SCAN_REGISTERS / MICROPY_GC_TRACK_CSTACK selection). api.js: expose interrupt(). variants/asyncify, variants/jspi: enable the auto split heap (and memory growth) so a looping program is the case the mid-loop collector bounds. tests/ports/webassembly: async_interrupt, async_restart and async_gc fixtures for interrupt, in-place restart and bounded GC. Signed-off-by: Phil Howard <[email protected]>
Asyncify instruments every function that could be on the stack at a suspend. For MicroPython that is almost the whole runtime: indirect calls (the NLR jump callbacks and the print/type-slot vtables) are conservatively assumed to reach a suspend, so even float boxing in a hot loop is instrumented (ASYNCIFY_ADVISE reported ~3200 functions). Add an asyncify-fast variant that turns on ASYNCIFY_IGNORE_INDIRECT and re-adds just the indirect-dispatch sites that genuinely sit above a suspend (the VM call machinery + protocol slots, in asyncify_add.txt); ASYNCIFY_ADD re-roots the analysis so their direct callers are picked up automatically. It also needs the Wasm longjmp backend, as MicroPython's NLR wraps execution in setjmp and the emscripten invoke_* trampolines abort once indirect auto-detection is off. The result runs the arithmetic / alloc / object-op hot path uninstrumented - about 2.5x faster, and 31% smaller wasm here. EXPERIMENTAL: the ADD list must stay complete - a suspend reachable through an un-listed indirect call would corrupt the unwind silently. variants/asyncify-fast: reuse the asyncify variant, add the instrumentation flags and the Wasm exception/longjmp backend. asyncify_add.txt: the curated dispatch-site list. tests/ports/webassembly/asyncify_fast.mjs: force a suspend through every dispatch path and cross-check a checksum against the full build. tools/ci.sh: build the asyncify-fast variant and run its fixture. Signed-off-by: Phil Howard <[email protected]>
_jsfetch shows the pattern - start an async JS operation, then suspend the WASM stack (emscripten_sleep) until it settles - but bakes in fetch(). Generalise it: run_sync(awaitable) blocks the current flow on any JS thenable and returns its resolved value, so Python can await JS without the caller having to. It needs a suspend-capable build and is gated on MICROPY_PY_JS_RUN_SYNC (off by default, on in the asyncify and jspi variants). run_sync refuses with RuntimeError unless mp_js_can_suspend() - the same permission check the cooperative yield uses - so a synchronous re-entry cannot unwind a frame it does not own. A rejection is surfaced like a thrown JS call (JsException), and a pending KeyboardInterrupt breaks the wait. Each in-flight await takes an integer handle into a JS-side Map rather than a single slot, so it stays correct if more than one continuation is ever suspended at once. main.c: make mp_js_can_suspend() non-static so the module can gate on it. modjsffi.c: run_sync()/can_run_sync() and their JS half (start/poll/take). proxy_c.h: declare mp_obj_jsproxy_make_js_exception() for the reject path. mpconfigport.h: MICROPY_PY_JS_RUN_SYNC gate, default off. variants/asyncify, variants/jspi: enable it (fast inherits asyncify). tests/ports/webassembly/async_run_sync.mjs: resolve, reject and type-error paths on a suspend-capable build (skips otherwise). Signed-off-by: Phil Howard <[email protected]>
be119cd to
e2211f0
Compare
The cooperative VM yield took over the VM hook from the plain JS_HOOK path, which quietly dropped the node-stdin Ctrl-C polling (mp_js_hook) during a running script, with no test to catch it. The yield now re-polls mp_js_hook, and this test locks that in: a child spins in Python with Ctrl-C enabled while the parent injects the interrupt byte on its stdin, and the run must raise KeyboardInterrupt. If the hook stops being polled the byte goes unread, the loop never stops, and the test fails rather than the regression going unnoticed. Runs on the asyncify variants, where the stdin hook is compiled in; it skips on the plain synchronous build and on jspi. Signed-off-by: Phil Howard <[email protected]>
e2211f0 to
0490b6f
Compare
Dropping it sounds good to me! I'd go even further and suggest to remove all ASYNCIFY support and just go for JSPI. ASYNCIFY won't be a viable long term option, it's short term if anything. |
|
JSPI is currently supported in Chrome, Edge and Opera: https://caniuse.com/wf-wasm-jspi I'm hoping that changes quickly because Asyncify is slow and unwieldy and indeed it would really simplify this PR. It's useful as a short-term fallback, though once you start adding WebSerial code deployment (which we're considering for our simulator) you're shackled to Chrome anyway. |
|
Hi 👋 Just adding a note of thanks for JSPI efforts and for dropping asyncify. My use of MicroPython in the browser is as a browser based runtime for doing, er, browser-y things via the FFI. I also want to emphasise how important it is that MicroPython's JSPI support has a public API that mirror's Pyodide's implementation. It means Python runtimes can be easily slotted in/out as need arises without having to change project code. Of further note is also that I believe JSPI support is coming to Firefox (it's being tracked via this issue: https://bugzilla.mozilla.org/show_bug.cgi?id=1897981). I start a new gig tomorrow so my spare cycles are going to be limited in the coming few weeks, but if you want someone to test things I'm more than happy to be that crash test dummy. 😄 👍 🚀 |
Testing- yes please. But - vitally more important - your input as another user of MicroPython WASM. I'm pretty new to this corner of the codebase and made changes to suit my needs first and foremost, then tried to shape them to be suitable for upstream second. The points about Pyodide are particularly salient, and I agree. MicroPython reflects CPython. MicroPython WASM ought to reflect Pyodide. Possible talking point along those lines - a Fetch API based mip? May also be worth considering how we handle stuff like urllib and requests, which I currently vendor modified shims for- since they need a deep rewrite from using a raw socket, to just being a wrapper around Fetch. Pyodide has experimental support for sockets when running under node - https://pyodide.org/en/stable/usage/socket.html - which, if it goes anywhere, would mean we'd have two classes of networking library:
The one place I think we differ - for my needs in particular - is the cooperative yield to handle fully synchronous code ( Anyway TLDR; help shaping this would be appreciated. |
|
👋 Apologies for my tardy response time. I started a new gig this week and I've been snowed under with on-boarding activities. So my use for WASM MicroPython is exclusively within the browser. It turns out, if you want Pythonic things in the browser, MicroPython gives you what you need in the blink of an eye. This is in contrast to Pyodide, which brings everything but the kitchen sink, and is relatively slow to start. Both have a place - Pyodide brings the Python ecosystem to the browser, MicroPython brings "Pythonic" to browser based development. They complement each other and I intend to evolve things on from PyScript to a project I'm calling "browser friendly Python". Blocking calls in Python in the browser and the whole sync/async dichotomy have been a pain point. In PyScript we used web workers, atomics and shared-array-buffer, to simulate blocking, but this came at a price of complexity in our code, performance trade-offs, lots of deeply technical research and obscure server configuration related to COOP/COEP headers that are often hard to control and even more difficult to make sense of. Definitely not ideal and a set of work-arounds on top of work-arounds. With the arrival of JSPI pretty much 99% of those problems go away, and it means browser friendly Python can be significantly simpler and easier to use than PyScript ever was. This is why I'm interested in this work and willing to participate as best I can to help get this to land. FWIW @dpgeorge and I have chatted about this over the past three months or so as something affirmative to the WASM build of MicroPython. Damien also tells me there are known locations in MicroPython where the interpreter can yield to the browser's event loop so Anyway... this is a bit brain-dumpy... but I hope you see my view of this particular mountain, and I hope we can compare notes so we arrive at a nice, simple yet effective and flexible solution for JSPI / MicroPython. 👍 |
|
Morning folks. 🌞 Over the weekend (and just recently with fresh eyes), I did a quick analysis (with the help of Claude) of the current state of play based on this PR. Rather than clutter up this PR, I've stuck it all in this gist which contains the LLM's initial draft, heavily edited and changed by me: https://gist.github.com/ntoll/88634f27175c585729e619287d008871 @dpgeorge @Gadgetoid - please take a read. It's relatively short but VERY specific as a proposal to land just JSPI, based on the initial work you (Phil) have already done. My suggestion is this report leads to a new PR for JSPI support only. What I expect is some discussion and questions, but the core thing you need to know is LLM estimates the PR to be around 200 LoC (i.e. relatively small). Now, knowing how Damo works, it should be relatively trivial to create this PR as a "to-throw-away" PoC. What I expect will really happen is Damo will roll his own solution 😉, so this is simply an exercise in creating a coherent, simple and easy to understand context, with some working code, from which Damien can trivially build a solution to his liking. Please let me know what you think. Happy to answer questions. Totally understand if the answer is "nope". FWIW - JSPI is the missing link for my BFP project to land, so I have skin in this game. 🤗 |
|
If you're willing to land that JSPI PR I'm happy to defer. My specific use case (https://try.badgewa.re) wont serve as a very good test for it. I've been meaning to come back and prune this PR, but haven't had the time. Appreciate your insight! |
|
See #19594, and thanks for getting this going @Gadgetoid 🤗. Without your initial work none of this could have been done so quickly. @dpgeorge I imagine you'll have opinions about the implementation in #19594 - please feel free to change anything. 💪 This is the final piece in the BFP jigsaw puzzle and, once landed/released, will allow me to do an alpha release of BFP in a couple of weeks once I've finished off the other 90% of work that needs completing. 😉 |

Summary
I appreciate this is an enormous tangled mess of very complicated, very deep-rooted and very scary changes.
I'm raising this for visibility, rather than as a serious proposal for upstream in its current form, but it does present some useful points.
Chiefly that JSPI is the future (A JSPI build run a Mandelbrot demo about 10x faster than its Asyncify counterpart)
, and we ought to seriously look into a cooperative WebAssembly port. The current setup functionally requires async to be usable beyond trivial things, and absolutely falls apart if you allow users to do something so brazen as
while True:. It's great for what it's great for (folks have built some genuinely amazing stuff) but isn't exactly representative of MicroPython.Additionally there are many challenges to running useful MicroPython code in the browser. This PR includes- Note: I have removed this because it was made pretty much redundant byjsfetchwhich gives MicroPython the ability to fetch URLs. Some simple wrappers around urllib and requests make this more or less an implementation detail and allow our (Pimoroni's) Badgeware code to run unmodified on both real hardware, and in browser, even when it's fetching data from APIs.run_sync.I also include a no-op for- Note: I have removed this because it muddies the waters and is unrelated specifically to wasm.@micropython.nativeand have been investigating doing something similar for Viper, but the latter is rather more difficult to syntax verify but not actually compile. My focus has been on parity between our devices and the web simulator, so little bits of friction like this would otherwise stop a user from just copy-pasting code between the two.Testing
I'm running this setup (shipping both Asyncify and JSPI alongside each other and auto-picking the best one for any given browser) in our prototype Badgeware simulator and it'll also be powering live examples in our documentation.
I'm now also shipping these builds with some basic playgrounds:
Trade-offs and Alternatives
This is the culmination of some months looking in to making MicroPython's WebAssembly port useful for my specific purposes. As such it's a hulking big change that - while it doesn't (afaik) break any existing uses of this port - carries a burden of understanding that I can't shoulder alone. Some input from other port users would be useful, and certainly some insight from new users of this port - I hope to put together a nice turnkey demo you can build on - would be appreciated.
Generative AI
I used generative AI tools when creating this PR, but a human has checked the
code and is responsible for the code and the description above.