This repository was archived by the owner on Sep 8, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 8
refactor {Stream,Future}|{Reader,Writer} APIs and internals
#240
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This makes a several changes to how `{Stream,Future}|{Reader,Writer}` work to
make them more efficient and, in some ways, more ergonomic:
- The background tasks have been removed, allowing reads and writes to complete without task context switching. We now only allocate and use oneshot channels lazily when the other end is not yet ready; this improves real world performance benchmarks (e.g. wasi-http request handling) considerably.
- Instances of `{Stream,Future}Reader` can now be lifted and lowered directly; no need for `Host{Stream,Future}` anymore.
- The type parameter for `Stream{Reader,Writer}` no longer refers to the buffer type -- just the payload type (i.e. `StreamReader<u8>` instead of `StreamReader<Vec<u8>>`), meaning any buffer type may be used for a given read or write operation. This also means the compiler needs help with type inference less often when calling `Instance::stream`.
- Instances of `{Stream,Future}|{Reader,Writer}` now require access to the store in order to be disposed of properly. I've added RAII wrapper structs (`WithAccessor[AndValue]`) to help with this, but they can only provide a "best-effort" guarantee since using an `Accessor` from within a `Drop::drop` implementation won't always work.
- In order to ensure that resources containing `{Stream,Future}|{Reader,Writer}` instances are disposed of properly, I've added `LinkerInstance::resource_concurrent` and have updated `wasmtime-wit-bindgen` to use it. This gives resource drop functions access to a `StoreContextMut` via an `Accessor`, allowing the stream and future handles to be disposed of.
- In order to make this work, I had to change `Accessor::instance` from a `Instance` to an `Option<Instance>`, which is awkward but temporary since we're planning to remove `Accessor::instance` entirely once we've moved concurrent state from `ComponentInstance` to `Store`.
That problem of disposal is definitely the most awkward part of all this. In
simple cases, it's easy enough to ensure that read and write handles are
disposed of properly, but both `wasmtime-wasi` and `wasmtime-wasi-http` have
some pretty complicated functions where handles are passed between tasks and/or
stored inside resources, so it can be tricky to ensure proper disposal on all
code paths. I'm open to ideas for improving this, but I suspect we'll need new
Rust language features (e.g. linear types) to make it truly ergonomic, robust,
and efficient.
While testing the above, I discovered an issue with `Instance::poll_until` such
that it would prematurely give up and return a "deadlock" trap error, believing
that there was no further work to do, even though the future passed to it was
ready to resolve the next time it was polled. I've fixed this by polling it one
last time and only trapping if it returns pending.
Note that I've moved a few associated functions from `ConcurrentState` to
`Instance` (e.g. `guest_drop_writable` and others) since they now need access to
the store; they're unchanged otherwise. Apologies for the diff noise.
Finally, I've tweaked how `wasmtime serve` to poll the guest for content before
handing the response to Hyper, which helps performance by ensuring the first
content chunk can be sent with the same TCP packet as the beginning of the
response.
Signed-off-by: Joel Dice <joel.dice@fermyon.com>
05ef07b to
b4e74e7
Compare
Member
alexcrichton
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this looks reasonable to me, but there's some high level points from me:
- I will want to spend some more time with the drop-related things to think through them. Basically as a foundational abstraction I want to make sure we spend some time thinking through the design tradeoffs and explore possible alternatives.
- In the limit I still want to get rid of oneshots entirely in futures/streams, even for writes/reads that can't immediately complete. I think this should still be possible but regardless this is a great step in the right direction towards plumbing the
async-ness further down and removing more of the CPS-ish way of passing around closures/async/etc.
crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs
Outdated
Show resolved
Hide resolved
crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs
Outdated
Show resolved
Hide resolved
This is no longer necessary to avoid 40ms of extra response latency for `wasi:http@0.3.0-draft` components since we now poll the body stream before handing the response to Hyper. I still think the Nagle algorithm is a bad default, but I can make a separate PR to disable it. Signed-off-by: Joel Dice <joel.dice@fermyon.com>
Previously, `WithAccessor[AndValue]` made only "best-effort" guarantees about disposing of handles on `drop` -- i.e. they did nothing if dropped outside the scope of a `tls::set`. Now they will panic instead, but that should never happen since we can guarantee futures closing over `&Accessor` are always dropped within the scope of a `tls::set`. Specifically, we ensure that happens in both `Store::drop` and `Instance::run_concurrent`. Signed-off-by: Joel Dice <joel.dice@fermyon.com>
Signed-off-by: Joel Dice <joel.dice@fermyon.com>
Collaborator
Author
|
@alexcrichton I believe I've addressed your feedback so far. Let me know if there's anything else you'd like me to change. |
Member
|
Nah let's land here and work through any other specifics during upstreaming |
Both our current domains failed in https://github.com/bytecodealliance/wasmtime/actions/runs/16349123310/job/46190913918 so add a few more.
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This makes a several changes to how
{Stream,Future}|{Reader,Writer}work tomake them more efficient and, in some ways, more ergonomic:
The background tasks have been removed, allowing reads and writes to complete without task context switching. We now only allocate and use oneshot channels lazily when the other end is not yet ready; this improves real world performance benchmarks (e.g. wasi-http request handling) considerably.
Instances of
{Stream,Future}Readercan now be lifted and lowered directly; no need forHost{Stream,Future}anymore.The type parameter for
Stream{Reader,Writer}no longer refers to the buffer type -- just the payload type (i.e.StreamReader<u8>instead ofStreamReader<Vec<u8>>), meaning any buffer type may be used for a given read or write operation. This also means the compiler needs help with type inference less often when callingInstance::stream.Instances of
{Stream,Future}|{Reader,Writer}now require access to the store in order to be disposed of properly. I've added RAII wrapper structs (WithAccessor[AndValue]) to help with this, but they can only provide a "best-effort" guarantee since using anAccessorfrom within aDrop::dropimplementation won't always work.In order to ensure that resources containing
{Stream,Future}|{Reader,Writer}instances are disposed of properly, I've addedLinkerInstance::resource_concurrentand have updatedwasmtime-wit-bindgento use it. This gives resource drop functions access to aStoreContextMutvia anAccessor, allowing the stream and future handles to be disposed of.Accessor::instancefrom aInstanceto anOption<Instance>, which is awkward but temporary since we're planning to removeAccessor::instanceentirely once we've moved concurrent state fromComponentInstancetoStore.That problem of disposal is definitely the most awkward part of all this. In
simple cases, it's easy enough to ensure that read and write handles are
disposed of properly, but both
wasmtime-wasiandwasmtime-wasi-httphavesome pretty complicated functions where handles are passed between tasks and/or
stored inside resources, so it can be tricky to ensure proper disposal on all
code paths. I'm open to ideas for improving this, but I suspect we'll need new
Rust language features (e.g. linear types) to make it truly ergonomic, robust,
and efficient.
While testing the above, I discovered an issue with
Instance::poll_untilsuchthat it would prematurely give up and return a "deadlock" trap error, believing
that there was no further work to do, even though the future passed to it was
ready to resolve the next time it was polled. I've fixed this by polling it one
last time and only trapping if it returns pending.
Note that I've moved a few associated functions from
ConcurrentStatetoInstance(e.g.guest_drop_writableand others) since they now need access tothe store; they're unchanged otherwise. Apologies for the diff noise.
Finally, I've tweaked how
wasmtime serveto poll the guest for content beforehanding the response to Hyper, which helps performance by ensuring the first
content chunk can be sent with the same TCP packet as the beginning of the
response.