fitzgen opened PR #14270 from fitzgen:sync-adapter-optimizations to bytecodealliance:main:
Today, every sync adapter calls
enter-sync-call, then does its lifting and lowering of arguments and reesults, and then callsexit-sync-callafterwards. The{enter,exit}-sync-callhelpers save and restore the old thread's TLS context and create the new thread's TLS context. For sync-to-sync calls, we inline these helpers and do their work lazily via theVMDeferredThreadmachinery. But even so, creating a lazyVMDeferredThreadcan be pretty expensive if the adapter's callee is just doing like a single load or store or has been boiled away into returning a constant value.Therefore, this commit introduces an analysis to find "thread-transparent" components. These are components that do not
canon lowerany component model intrinsic to access the thread state, and therefore cannot read or write that state. When we are compiling adapters whose callee is thread-transparent, we don't even need to{enter,exit}-sync-callat all because the callee will not read/write its thread state, so we don't need to save and restore the current thread state, we can just leave it in place.<!--
Please make sure you include the following information:
If this work has been discussed elsewhere, please include a link to that
conversation. If it was discussed in an issue, just mention "issue #...".Explain why this change is needed. If the details are in an issue already,
this can be brief.Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.htmlPlease review the Bytecode Alliance's AI tool usage policy at
https://github.com/bytecodealliance/governance/blob/main/AI_TOOL_POLICY.mdPlease ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->
fitzgen requested cfallin for a review on PR #14270.
fitzgen requested wasmtime-compiler-reviewers for a review on PR #14270.
fitzgen requested wasmtime-core-reviewers for a review on PR #14270.
github-actions[bot] added the label wasmtime:api on PR #14270.
fitzgen updated PR #14270.
:memo: cfallin submitted PR review:
Thanks for this optimization! Some thoughts below.
Overall I will rate my review status as "seems plausible" but I haven't been in this code deeply enough to confidently sign off -- probably @alexcrichton should take a look as well?
:speech_balloon: cfallin created PR review comment:
Little nit but these two bools now define an ad-hoc enum with three valid states, not four, because of the implies-relation that you note. Maybe make it an enum with accessors?
:speech_balloon: cfallin created PR review comment:
This is a key bit to the correctness argument too I think. It's implicit in the name ("transparent") but I think I want to see an explicit statement on the "transitively reached function" problem: naively a sync call to a component that doesn't use the thread-state-observing intrinsics might be fine except that that component calls another component that does. The observation is that the nested call itself will do the state-save if needed. So "transparent" really means that we adopt the thread/task identity of our caller (unobservably) and so doesn't have a transitive nature. Can we write that up somewhere?
:speech_balloon: cfallin created PR review comment:
We query the set here and we both insert and remove from it above -- could we add notes to the API doc-comments specifying the order in which actions must occur for correctness (and a correctness/convergence argument in general)? Something like:
- We initially assume every instance is transparent (that's the
push_instanceas we first walk the instance graph).- Then we scan initializers and remove from the transparent set as we discover the
canon lifts that could observe thread state.- Only after that scan is complete, we can call
adapter_is_transparentto query the result during codegen.Basically, I want a "stratification" of the API according to its expected usage pattern.
fitzgen requested alexcrichton for a review on PR #14270.
:memo: alexcrichton submitted PR review:
Overall this seems generally correct to me, but I'm left with a general feeling that the inlining pass isn't the best place to handle this. One example of my unease is that
ComponentInstanceDef::Intrinsicsis claimed as "yes, this is transparent", but that's not actually true for the context get/set intrinsics. This is later handled infunc_def_is_transparentwhere those are "no, this is not transparent", so I don't think there's a bug here, but I'm also not certain of that.I've been studying this and reading over old code again, and would it be possible to migrate this analysis to the
dfg.rsdata structures? Those I think are generally structured exactly as you'd want for this, and this analysis could be a function ofComponentDfgtoEntitySet<AdapterId>for example (or, possibly, put aOption<bool>onstruct Adapterand fill it in in this analysis pass and then theOption<bool>is unwrapped during adapter generation). This sort of migration would require a relatively major restructuring ofthread_transparency.rs, however, so I'm not certain this is the right thing to do.What I'm roughly thinking is that there'd sets of things that are transparent and they'd be filled in by walking over the order of things in
ComponentDfg. For example anInstanceIdis transparent if all its arguments are, and anOptionsIdwould be transparent if everything it points to is additionally transparent. I think this should be a relatively natural analysis to write in a similar manner to other parts ofdfg.rs, basically an AST-walk of sorts of the data structures and building up a predicate.The benefits of this approach would be to avoid complicating the inlining pass more and not having to worry about abstractions like aliases and such. Instead, in theory, everything would be able to process the exact definition something has, for example not having to deal with
IntrinsicsImportas well and only dealing with "is this exact unsafe intrinsic transparent".Does that sound ok to you to rearchitect this? Or do you feel that this pass as-is is the right location to do this?
Unrelatedly, I'd ideally like to review the tests being added here, but ~4kloc of tests for this feature feels kind of intense and I wouldn't really know where to start. I suspect most of them are LLM-generated, but have you had a chance to review the tests themselves? Do you know if it'd be possible to reduce the size of testing without reducing test coverage?
fitzgen commented on PR #14270:
One example of my unease is that
ComponentInstanceDef::Intrinsicsis claimed as "yes, this is transparent", but that's not actually true for the context get/set intrinsics. This is later handled infunc_def_is_transparentwhere those are "no, this is not transparent", so I don't think there's a bug here, but I'm also not certain of that.The Wasmtime intrinsics instance is transparent because our unsafe intrinsics instance cannot access the task state. The context get/set canon intrinsics aren't actually exported from the unsafe intrinsics instance, and are only implemented as pseudo unsafe intrinsics because we shoe-horned them into that
enumrather than spending the time to define their ownenumand dealing with the type shepherding fallout (and we should really go back and fix that...)I've been studying this and reading over old code again, and would it be possible to migrate this analysis to the
dfg.rsdata structures? Those I think are generally structured exactly as you'd want for this, and this analysis could be a function ofComponentDfgtoEntitySet<AdapterId>for example (or, possibly, put aOption<bool>onstruct Adapterand fill it in in this analysis pass and then theOption<bool>is unwrapped during adapter generation). This sort of migration would require a relatively major restructuring ofthread_transparency.rs, however, so I'm not certain this is the right thing to do.What I'm roughly thinking is that there'd sets of things that are transparent and they'd be filled in by walking over the order of things in
ComponentDfg. For example anInstanceIdis transparent if all its arguments are, and anOptionsIdwould be transparent if everything it points to is additionally transparent. I think this should be a relatively natural analysis to write in a similar manner to other parts ofdfg.rs, basically an AST-walk of sorts of the data structures and building up a predicate.The benefits of this approach would be to avoid complicating the inlining pass more and not having to worry about abstractions like aliases and such. Instead, in theory, everything would be able to process the exact definition something has, for example not having to deal with
IntrinsicsImportas well and only dealing with "is this exact unsafe intrinsic transparent".Does that sound ok to you to rearchitect this? Or do you feel that this pass as-is is the right location to do this?
I can look into this, not married to the current implementation.
Unrelatedly, I'd ideally like to review the tests being added here, but ~4kloc of tests for this feature feels kind of intense and I wouldn't really know where to start. I suspect most of them are LLM-generated, but have you had a chance to review the tests themselves? Do you know if it'd be possible to reduce the size of testing without reducing test coverage?
I did review everything, and probably made things worse by pushing to avoid unit
#[test]s because that required building a bunch of internal data structures by hand that are normally only created by translation which I felt was worse (and also verbose but additionally fairly unreadable), but perhaps I can do better by making some stuff dependency injection ish so that we can write them more concisely. Will look into it.
alexcrichton commented on PR #14270:
Oh right yeah, good point about
ComponentInstanceDef::Intrinsics. My hope is that with the dfg-based approach it'll sort of naturally fall out and we won't even have to consider this, but that's TBD.For tests ok makes sense, and yeah I'd prefer duplication in
*.wasttests over adding newtests/all/*.rstests (as that way we can hopefully share with other runtimes one day).
fitzgen updated PR #14270.
fitzgen requested alexcrichton for a review on PR #14270.
fitzgen commented on PR #14270:
Factored things out a bit to have its own core "vocabulary" so that it is easily unit testable and made it ultimately a
fn(&ComponentDfg) -> EntitySet<AdapterId>and I think things are indeed much cleaner now.
:memo: alexcrichton submitted PR review.
:speech_balloon: alexcrichton created PR review comment:
I think everything in module, except
transparent_adapters, can drop thepubnow? (should help future dead-code lints and such)
:speech_balloon: alexcrichton created PR review comment:
One possible option here, if you feel so inclined, to not pass a
boolparameter would be to pass in the whole&componentand*adapteand have the method internally pluck out these fields.
:speech_balloon: alexcrichton created PR review comment:
Huh this seems like we should probably unify the type for adapter and normal options... Anyway for a future PR
:speech_balloon: alexcrichton created PR review comment:
Naively I'd expect this test to fail because if something can end up going out to the host I'd expect it transitively to be considered opaque. Is this something where the analysis could change the order of how it processes things or inherit instance-to-instance to detect what's opaque?
If this test fails, I also don't think that would de-optimize the use case you're thinking of, right? For compile-time builtins those components generally won't actually import anything from the host I'd expect.
:speech_balloon: alexcrichton created PR review comment:
I'm a bit surprised by these, all of these I'd expect would deal with thread/task state. These are all synthesized by fact modules though so I could also imagine that they don't show up at all since the fact modules haven't been translated yet.
Perhaps these could be
unreachable!()and/or move to above?
:memo: fitzgen submitted PR review.
:speech_balloon: fitzgen created PR review comment:
For compile-time builtins those components generally won't actually import anything from the host I'd expect.
Actually, I would expect compile-time builtins to import things from the host because for everything except the simplest getters, there would be a fast/simple inline path and then a slow path that calls into the host in order to e.g. grow a buffer or whatever that is too complicated to do inline.
It seems unfortunate to make things transitive when it isn't necessary for correctness and would only mean skipping optimizations.
:memo: alexcrichton submitted PR review.
:speech_balloon: alexcrichton created PR review comment:
Ah ok, in that case yeah that seems fine, but how are you thinking that would look like? Right now wouldn't a compile-time-builtin component importing something from the host poison the entire compile-time-import's component instance to be opaque? you'd have to import something from some other component which then imports from the host I'd imagine
fitzgen updated PR #14270.
:memo: fitzgen submitted PR review.
:speech_balloon: fitzgen created PR review comment:
Done
:memo: fitzgen submitted PR review.
:speech_balloon: fitzgen created PR review comment:
Done
:memo: fitzgen submitted PR review.
:speech_balloon: fitzgen created PR review comment:
Done
:memo: fitzgen submitted PR review.
:speech_balloon: fitzgen created PR review comment:
Yeah, it would require keeping the host calls in their own component that re-exports that imported host functions, so that the thread state save/restore is delayed until crossing that component's boundary. I think this should be fine though.
:thumbs_up: alexcrichton submitted PR review.
alexcrichton added PR #14270 Skip {enter,exit}-sync-call for "thread-transparent" adapters to the merge queue.
:check: alexcrichton merged PR #14270.
alexcrichton removed PR #14270 Skip {enter,exit}-sync-call for "thread-transparent" adapters from the merge queue.
fitzgen commented on PR #14270:
Thanks for the review!
Last updated: Sep 20 2026 at 18:08 UTC