From 13a065d9e296e350d95db5f098addcd2442d72aa Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Mon, 13 Jul 2026 17:19:44 +0200 Subject: [PATCH 1/8] Split current_thread into an inline fast path and a cold slow path Splitting current_thread into an #[inline] fast path plus #[cold] outlined deferred-promotion slow path reduces call overhead by about 20% (measured in a Linux VM on an M5 Max MBP): sync calls go from about 83ns to about 65ns, immediately ready async calls from 142ns to 117ns. --- crates/wasmtime/src/runtime/component/concurrent.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index a5a34e481557..6f6c2737eda5 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1601,6 +1601,7 @@ impl StoreContextMut<'_, T> { impl StoreOpaque { /// Returns the currently-running thread, promoting any deferred lazy thread /// into a fully-materialized `CurrentThread`. + #[inline] pub(crate) fn current_thread(&mut self) -> Result { // Without concurrency support there is nothing to force. if !self.concurrency_support() { @@ -1619,6 +1620,14 @@ impl StoreOpaque { .unforced_current_thread); } + self.force_deferred_current_thread() + } + + /// Slow path of [`Self::current_thread`]: promote the deferred lazy + /// thread into a fully-materialized `CurrentThread`. + #[cold] + fn force_deferred_current_thread(&mut self) -> Result { + // The component instance whose adapters pushed the deferred frames; all // frames in a guest-to-guest, sync-to-sync call chain of fused adapters // live within a single `wasmtime::component::Instance` (because From 3336665e1c890aa23cfdb114ad0a75bf400a21ac Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Mon, 13 Jul 2026 18:10:10 +0200 Subject: [PATCH 2/8] Skip resource-borrow scope tracking for borrow-free signatures Precompute `TypeFunc::contains_borrow` at compile time and use it in the hostcall entrypoint to skip `CallContext` scope push/pop and `validate_scope_exit` when lending is statically impossible. Relative to the previous commit, this reduces call overhead by about 29% for sync calls, and about 9% for async calls (both measured in a Linux VM on an M5 Max MBP): sync calls go from about 65ns to about 46ns, immediately ready async calls from 117ns to 106ns. I'm not entirely sure why the win is so much less for async calls, but there are more wins in the next commits. --- crates/environ/src/component/types.rs | 4 ++++ crates/environ/src/component/types_builder.rs | 8 ++++++-- .../src/runtime/component/concurrent.rs | 19 +++++++++++++++---- .../runtime/component/concurrent_disabled.rs | 14 ++++++++++---- .../src/runtime/component/func/host.rs | 14 ++++++++++---- 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index 56c2874fa8d6..9aefd48d8d75 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -585,6 +585,10 @@ pub struct TypeComponentInstance { pub struct TypeFunc { /// Whether or not this is an async function. pub async_: bool, + /// Whether any parameter (transitively) contains a `borrow` handle, + /// letting the runtime skip borrow-scope tracking when lending is + /// impossible. (Borrows are only valid in parameters, not results.) + pub contains_borrow: bool, /// Names of parameters. pub param_names: Vec, /// Parameters to the function represented as a tuple. diff --git a/crates/environ/src/component/types_builder.rs b/crates/environ/src/component/types_builder.rs index a9822ea1afdc..6a5ebf2cd50a 100644 --- a/crates/environ/src/component/types_builder.rs +++ b/crates/environ/src/component/types_builder.rs @@ -230,16 +230,20 @@ impl ComponentTypesBuilder { .params .iter() .map(|(_name, ty)| self.valtype(types, ty)) - .collect::>()?; + .collect::>>()?; let results = ty .result .iter() .map(|ty| self.valtype(types, ty)) .collect::>()?; - let params = self.new_tuple_type(params); + let contains_borrow = params + .iter() + .any(|ty| self.ty_contains_borrow_resource(ty)); + let params = self.new_tuple_type(params.into()); let results = self.new_tuple_type(results); let ty = TypeFunc { async_: ty.async_, + contains_borrow, param_names, params, results, diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 6f6c2737eda5..353ad789917f 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1821,9 +1821,14 @@ impl StoreOpaque { /// relatively expensive table manipulations. This would ideally be /// optimized to avoid the full allocation of a `HostTask` in at least some /// situations. - pub(crate) fn host_task_create(&mut self) -> Result>> { + pub(crate) fn host_task_create(&mut self, track_scope: bool) -> Result>> { if !self.concurrency_support() { - self.enter_call_not_concurrent()?; + // Resource-borrow scope tracking is only needed when the + // function's parameters can actually contain `borrow` handles; + // this is precomputed per function type. + if track_scope { + self.enter_call_not_concurrent()?; + } return Ok(None); } let caller = self.current_guest_thread()?; @@ -1855,14 +1860,20 @@ impl StoreOpaque { /// Note that this isn't invoked when the host is invoked asynchronously and /// the host isn't complete yet. In that situation the host task persists /// and will be cleaned up separately in `subtask_drop` - pub(crate) fn host_task_delete(&mut self, task: Option>) -> Result<()> { + pub(crate) fn host_task_delete( + &mut self, + task: Option>, + track_scope: bool, + ) -> Result<()> { match task { Some(task) => { log::trace!("delete host task {task:?}"); self.concurrent_state_mut()?.delete(task)?; } None => { - self.exit_call_not_concurrent(); + if track_scope { + self.exit_call_not_concurrent(); + } } } Ok(()) diff --git a/crates/wasmtime/src/runtime/component/concurrent_disabled.rs b/crates/wasmtime/src/runtime/component/concurrent_disabled.rs index a476bfb869ad..da10e1a0b1be 100644 --- a/crates/wasmtime/src/runtime/component/concurrent_disabled.rs +++ b/crates/wasmtime/src/runtime/component/concurrent_disabled.rs @@ -162,16 +162,22 @@ impl StoreOpaque { Ok(self.exit_call_not_concurrent()) } - pub(crate) fn host_task_create(&mut self) -> Result<()> { - self.enter_call_not_concurrent() + pub(crate) fn host_task_create(&mut self, track_scope: bool) -> Result<()> { + if track_scope { + self.enter_call_not_concurrent()?; + } + Ok(()) } pub(crate) fn host_task_reenter_caller(&mut self) -> Result<()> { Ok(()) } - pub(crate) fn host_task_delete(&mut self, (): ()) -> Result<()> { - Ok(self.exit_call_not_concurrent()) + pub(crate) fn host_task_delete(&mut self, (): (), track_scope: bool) -> Result<()> { + if track_scope { + self.exit_call_not_concurrent(); + } + Ok(()) } pub(crate) fn check_blocking(&mut self) -> crate::Result<()> { diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index b1d8a931f8bd..c5e2db30f0fa 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -369,6 +369,9 @@ where ) -> Result<()> { let vminstance = instance.id().get(store.0); let async_ = vminstance.component().env_component().options[options].async_; + // Skip resource-borrow scope tracking when the signature makes + // lending impossible (precomputed at compile time). + let track_scope = vminstance.component().types()[ty].contains_borrow; // If this is a synchronous-lower of a host-async function, then the // guest is blocking. Test, in the context of the guest task, if that's @@ -378,7 +381,7 @@ where } // Enter the host by pushing a `HostTask` into the concurrent state. - let host_task = store.0.host_task_create()?; + let host_task = store.0.host_task_create(track_scope)?; let host_task_complete = if async_ { #[cfg(feature = "component-model-async")] @@ -401,7 +404,7 @@ where // function transitively would have updated the current guest thread to // the caller of this host function. if host_task_complete { - store.0.host_task_delete(host_task)?; + store.0.host_task_delete(host_task, track_scope)?; } Ok(()) @@ -572,8 +575,11 @@ where dst: Destination<'_>, ) -> Result<()> { // Before lowering below semantically ensure that the caller has dropped - // all of its borrows and such. - lower.validate_scope_exit()?; + // all of its borrows and such. Skipped when the signature cannot + // contain borrows (in which case no scope was entered either). + if lower.types[ty].contains_borrow { + lower.validate_scope_exit()?; + } // At this point we're transitioning back to the caller task which means // that the current task needs to be updated. This will restore the From 4d50079897e26c87fdeeb13629ac472866e4b4b8 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Mon, 13 Jul 2026 18:26:03 +0200 Subject: [PATCH 3/8] Defer HostTask creation for sync-lowered borrow-free host calls Such calls now run as the caller's guest thread without creating a pending future at all: `poll_and_block` polls the host future once before materializing anything and returns ready results directly. `host_task_reenter_caller` becomes a no-op when no task was created. Async-lowered calls keep the eager task for subtask status/cancellation), and borrow-ful calls keep it as scope identity. Relative to the previous commit, this reduces call overhead by almost another 60% (46ns -> 19ns) for sync calls, with async calls, expectedly, stay unchanged. --- .../src/runtime/component/concurrent.rs | 59 +++++++++++++++++-- .../runtime/component/concurrent_disabled.rs | 2 +- .../src/runtime/component/func/host.rs | 7 ++- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 353ad789917f..b2e77818a59d 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -834,7 +834,27 @@ pub(crate) fn poll_and_block( store: &mut dyn VMStore, future: impl Future> + Send + 'static, ) -> Result { - let task = store.current_host_thread()?; + // Fast path: poll the future once before materializing any `HostTask`. + // Most host implementations complete immediately, in which case the + // result is returned without any task-table traffic or result boxing. + let mut inner = Box::pin(future); + let poll = tls::set(store, || { + inner + .as_mut() + .poll(&mut Context::from_waker(&Waker::noop())) + }); + if let Poll::Ready(result) = poll { + return result; + } + + // Slow path: the future is genuinely pending. Materialize the host task + // now if the entrypoint deferred it (in which case this function also + // owns its cleanup below); otherwise use the already-current task. + let (task, materialized) = match store.current_thread()? { + CurrentThread::Host(task) => (task, false), + _ => (store.materialize_host_task()?, true), + }; + let future = inner; // Wrap the future in a closure which will take care of stashing the result // in `GuestTask::result` and resuming this fiber when the host task @@ -898,13 +918,21 @@ pub(crate) fn poll_and_block( // Retrieve and return the result. let host_state = &mut store.concurrent_state_mut()?.get_mut(task)?.state; - match mem::replace(host_state, HostTaskState::CalleeDone { cancelled: false }) { + let result = match mem::replace(host_state, HostTaskState::CalleeDone { cancelled: false }) { HostTaskState::CalleeFinished(result) => Ok(match result.downcast() { Ok(result) => *result, Err(_) => bail_bug!("host task finished with wrong type of result"), }), _ => bail_bug!("unexpected host task state after completion"), + }; + + // A task materialized here (rather than by the entrypoint) is also + // cleaned up here, mirroring `host_task_delete` for the eager case. + if materialized { + store.host_task_reenter_caller()?; + store.concurrent_state_mut()?.delete(task)?; } + result } /// Execute the specified guest call. @@ -1821,7 +1849,11 @@ impl StoreOpaque { /// relatively expensive table manipulations. This would ideally be /// optimized to avoid the full allocation of a `HostTask` in at least some /// situations. - pub(crate) fn host_task_create(&mut self, track_scope: bool) -> Result>> { + pub(crate) fn host_task_create( + &mut self, + track_scope: bool, + defer: bool, + ) -> Result>> { if !self.concurrency_support() { // Resource-borrow scope tracking is only needed when the // function's parameters can actually contain `borrow` handles; @@ -1831,12 +1863,25 @@ impl StoreOpaque { } return Ok(None); } + // Sync-lowered, borrow-free host calls don't need a `HostTask` unless + // the host implementation actually suspends: they run as the caller's + // thread and `poll_and_block` materializes (and cleans up) a task + // on-demand via `materialize_host_task` if the future is pending. + if defer { + return Ok(None); + } + Ok(Some(self.materialize_host_task()?)) + } + + /// Push a new `HostTask` for a host call made by the current guest + /// thread, and make it the current thread. + pub(crate) fn materialize_host_task(&mut self) -> Result> { let caller = self.current_guest_thread()?; let state = self.concurrent_state_mut()?; let task = state.push(HostTask::new(caller, HostTaskState::CalleeStarted))?; log::trace!("new host task {task:?}"); self.set_thread(task)?; - Ok(Some(task)) + Ok(task) } /// Invoked before lowering the results of a host task to the guest. @@ -1848,7 +1893,11 @@ impl StoreOpaque { if !self.concurrency_support() { return Ok(()); } - let task = self.current_host_thread()?; + // For deferred (task-less) host calls the current thread already is + // the caller; nothing to restore. + let CurrentThread::Host(task) = self.current_thread()? else { + return Ok(()); + }; let caller = self.concurrent_state_mut()?.get_mut(task)?.caller; self.set_thread(caller)?; Ok(()) diff --git a/crates/wasmtime/src/runtime/component/concurrent_disabled.rs b/crates/wasmtime/src/runtime/component/concurrent_disabled.rs index da10e1a0b1be..2b4473f10910 100644 --- a/crates/wasmtime/src/runtime/component/concurrent_disabled.rs +++ b/crates/wasmtime/src/runtime/component/concurrent_disabled.rs @@ -162,7 +162,7 @@ impl StoreOpaque { Ok(self.exit_call_not_concurrent()) } - pub(crate) fn host_task_create(&mut self, track_scope: bool) -> Result<()> { + pub(crate) fn host_task_create(&mut self, track_scope: bool, _defer: bool) -> Result<()> { if track_scope { self.enter_call_not_concurrent()?; } diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index c5e2db30f0fa..e267d5dcc688 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -381,7 +381,12 @@ where } // Enter the host by pushing a `HostTask` into the concurrent state. - let host_task = store.0.host_task_create(track_scope)?; + // Sync-lowered borrow-free calls defer task creation entirely (see + // `host_task_create`); async-lowered calls need the task for subtask + // status/cancellation semantics, and borrow-ful calls need it as the + // resource scope identity. + let defer_task = !async_ && !track_scope; + let host_task = store.0.host_task_create(track_scope, defer_task)?; let host_task_complete = if async_ { #[cfg(feature = "component-model-async")] From e4cfb5f25fba9bddb2bbff13d8058ad1aecd3040 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Mon, 13 Jul 2026 18:42:43 +0200 Subject: [PATCH 4/8] Skip scope resolution in LiftContext if possible Specifically, if no borrows need to be tracked, no scope needs to be resolved. Plus, inline a bunch of single-instantiation generics. Relative to the previous commit, this reduces call overhead by another 31% for sync calls (19ns to 13ns), with async calls unchanged (both measured in a Linux VM on an M5 Max MBP). --- .../src/runtime/component/concurrent.rs | 4 +++ .../src/runtime/component/func/host.rs | 13 +++++++-- .../src/runtime/component/func/options.rs | 28 ++++++++++++++++++- .../wasmtime/src/runtime/component/store.rs | 6 ++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index b2e77818a59d..7fb01142193d 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1849,6 +1849,7 @@ impl StoreOpaque { /// relatively expensive table manipulations. This would ideally be /// optimized to avoid the full allocation of a `HostTask` in at least some /// situations. + #[inline] pub(crate) fn host_task_create( &mut self, track_scope: bool, @@ -1889,6 +1890,7 @@ impl StoreOpaque { /// This is used to update the current thread annotations within the store /// to ensure that it reflects the guest task, not the host task, since /// lowering may execute guest code. + #[inline] pub fn host_task_reenter_caller(&mut self) -> Result<()> { if !self.concurrency_support() { return Ok(()); @@ -1909,6 +1911,7 @@ impl StoreOpaque { /// Note that this isn't invoked when the host is invoked asynchronously and /// the host isn't complete yet. In that situation the host task persists /// and will be cleaned up separately in `subtask_drop` + #[inline] pub(crate) fn host_task_delete( &mut self, task: Option>, @@ -2410,6 +2413,7 @@ impl StoreOpaque { /// Used by `ResourceTables` to record the scope of a borrow to get undone /// in the future. + #[inline] pub(crate) fn current_scope_id(&mut self) -> Result> { if !self.concurrency_support() { return self.current_scope_id_not_concurrent(); diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index e267d5dcc688..060fe76af60e 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -359,6 +359,7 @@ where /// "Rust" entrypoint after panic-handling infrastructure is set up and raw /// arguments are translated to Rust types. + #[inline] fn entrypoint( &self, mut store: StoreContextMut<'_, T>, @@ -399,7 +400,7 @@ where when `component-model-async` feature disabled" ); } else { - self.call_sync_lower(store.as_context_mut(), instance, ty, options, storage)?; + self.call_sync_lower(store.as_context_mut(), instance, ty, options, storage, track_scope)?; true }; @@ -422,6 +423,7 @@ where /// the `async` option when lowered. Note that the host function itself /// can still be async, in which case this will block here waiting for it /// to finish. + #[inline] fn call_sync_lower( &self, mut store: StoreContextMut<'_, T>, @@ -429,8 +431,13 @@ where ty: TypeFuncIndex, options: OptionsIndex, storage: &mut [MaybeUninit], + track_scope: bool, ) -> Result<()> { - let mut lift = LiftContext::new(store.0.store_opaque_mut(), options, instance)?; + let mut lift = if track_scope { + LiftContext::new(store.0.store_opaque_mut(), options, instance)? + } else { + LiftContext::new_without_scope(store.0.store_opaque_mut(), options, instance)? + }; let (params, rest) = self.load_params(&mut lift, ty, MAX_FLAT_PARAMS, storage)?; let ret = match self.run(store.as_context_mut(), params) { @@ -538,6 +545,7 @@ where /// /// This will internally decide the ABI source of the parameters and use /// `storage` appropriately. + #[inline] fn load_params<'a>( &self, lift: &mut LiftContext<'_>, @@ -573,6 +581,7 @@ where } /// Stores the result `ret` into `dst` which is calculated per the ABI. + #[inline] fn lower_result_and_exit_call( lower: &mut LowerContext<'_, T>, ty: TypeFuncIndex, diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 6538ab0837cf..0bea3eb14a26 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -338,10 +338,36 @@ impl<'a> LiftContext<'a> { store: &'a mut StoreOpaque, options: OptionsIndex, instance_handle: Instance, + ) -> Result> { + Self::new_impl(store, options, instance_handle, true) + } + + /// Same as [`Self::new`] but without resolving the current resource + /// scope, for calls whose signature statically cannot contain `borrow` + /// handles (the only consumers of the scope id). + #[inline] + pub(crate) fn new_without_scope( + store: &'a mut StoreOpaque, + options: OptionsIndex, + instance_handle: Instance, + ) -> Result> { + Self::new_impl(store, options, instance_handle, false) + } + + #[inline] + fn new_impl( + store: &'a mut StoreOpaque, + options: OptionsIndex, + instance_handle: Instance, + want_scope: bool, ) -> Result> { let store_id = store.id(); let hostcall_fuel = store.hostcall_fuel(); - let current_scope_id = store.current_scope_id()?; + let current_scope_id = if want_scope { + store.current_scope_id()? + } else { + None + }; // From `&mut StoreOpaque` provided the goal here is to project out // three different disjoint fields owned by the store: memory, // `CallContexts`, and `HandleTable`. There's no native API for that diff --git a/crates/wasmtime/src/runtime/component/store.rs b/crates/wasmtime/src/runtime/component/store.rs index 2dccbb41d712..6a670aaa259b 100644 --- a/crates/wasmtime/src/runtime/component/store.rs +++ b/crates/wasmtime/src/runtime/component/store.rs @@ -218,6 +218,7 @@ impl StoreComponentInstanceId { /// # Panics /// /// Panics if `self` does not belong to `store`. + #[inline] pub(crate) fn get<'a>(&self, store: &'a StoreOpaque) -> &'a ComponentInstance { self.assert_belongs_to(store.id()); store.component_instance(self.instance) @@ -228,6 +229,7 @@ impl StoreComponentInstanceId { /// # Panics /// /// Panics if `self` does not belong to `store`. + #[inline] pub(crate) fn get_mut<'a>(&self, store: &'a mut StoreOpaque) -> Pin<&'a mut ComponentInstance> { self.from_data_get_mut(store.store_data_mut()) } @@ -356,6 +358,7 @@ impl StoreOpaque { support } + #[inline] pub(crate) fn lift_context_parts( &mut self, instance: Instance, @@ -420,6 +423,7 @@ impl StoreOpaque { )) } + #[inline] pub(crate) fn enter_call_not_concurrent(&mut self) -> Result<()> { let state = match &mut self.component_data_mut().task_state { ComponentTaskState::NotConcurrent(state) => state, @@ -430,6 +434,7 @@ impl StoreOpaque { Ok(()) } + #[inline] pub(crate) fn exit_call_not_concurrent(&mut self) { let state = match &mut self.component_data_mut().task_state { ComponentTaskState::NotConcurrent(state) => state, @@ -459,6 +464,7 @@ impl StoreOpaque { } } + #[inline] pub(crate) fn current_scope_id_not_concurrent(&mut self) -> Result> { match &mut self.component_data_mut().task_state { ComponentTaskState::NotConcurrent(state) => match state.scopes.len().checked_sub(1) { From 0efaf6585f3a39d57e696e2e9c80039f581d2409 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Mon, 13 Jul 2026 18:58:23 +0200 Subject: [PATCH 5/8] Elide HostTask + JoinHandle for ready async-lowered host calls A ready async-lowered call returns Status::Returned with no waitable, so the guest cannot observe the task's absence. first_poll now polls the raw future before creating anything, only pending futures get in `JoinHandle::run` and materialize a task. This only works for borrow-free calls, because for borrows the task is still needed for scope tracking. Relative to the previous commit, sync calls stay unchanged, but async calls are improved by about 45%: 109ns to 60ns. (measured in a Linux VM on an M5 Max MBP) --- .../src/runtime/component/concurrent.rs | 69 ++++++++----------- .../src/runtime/component/func/host.rs | 23 +++++-- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 7fb01142193d..76c89f25c592 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1727,13 +1727,6 @@ impl StoreOpaque { } } - fn current_host_thread(&mut self) -> Result> { - match self.current_thread()?.host() { - Some(id) => Ok(id), - None => bail_bug!("current thread is not a host thread"), - } - } - /// Returns whether there's a pending cancellation on the current guest thread, /// consuming the event if so. fn take_pending_cancellation(&mut self) -> Result { @@ -1864,10 +1857,8 @@ impl StoreOpaque { } return Ok(None); } - // Sync-lowered, borrow-free host calls don't need a `HostTask` unless - // the host implementation actually suspends: they run as the caller's - // thread and `poll_and_block` materializes (and cleans up) a task - // on-demand via `materialize_host_task` if the future is pending. + // Deferred (borrow-free) calls run as the caller's thread; a task is + // only materialized if the host future actually suspends. if defer { return Ok(None); } @@ -3274,35 +3265,27 @@ impl Instance { lower: impl FnOnce(StoreContextMut, Option) -> Result<()> + Send + 'static, ) -> Result> { let token = StoreToken::new(store.as_context_mut()); - let task = store.0.current_host_thread()?; - let state = store.0.concurrent_state_mut()?; - - // Create an abortable future which hooks calls to poll and manages call - // context state for the future. - let (join_handle, future) = JoinHandle::run(future); - { - let state = &mut state.get_mut(task)?.state; - assert!(matches!(state, HostTaskState::CalleeStarted)); - *state = HostTaskState::CalleeRunning(join_handle); - } - let mut future = Box::pin(future); - - // Finally, poll the future. We can use a dummy `Waker` here because - // we'll add the future to `ConcurrentState::futures` and poll it - // automatically from the event loop if it doesn't complete immediately - // here. + // Poll the raw future once before creating anything. Note that no + // `HostTask` (and no abortable `JoinHandle` wrapper) exists yet for + // deferred (borrow-free) calls: the common case is that the future + // completes right here, in which case no task is ever created and + // the guest simply observes `Status::Returned` with no waitable + // handle. We can use a dummy `Waker` because a pending future is + // added to `ConcurrentState::futures` below and polled from the + // event loop. + let mut inner = Box::pin(future); let poll = tls::set(store.0, || { - future + inner .as_mut() .poll(&mut Context::from_waker(&Waker::noop())) }); match poll { - // It finished immediately; lower the result and delete the task. + // It finished immediately; lower the result. Poll::Ready(result) => { - let result = result.transpose()?; - lower(store.as_context_mut(), result)?; + let result = result?; + lower(store.as_context_mut(), Some(result))?; return Ok(None); } @@ -3310,6 +3293,21 @@ impl Instance { Poll::Pending => {} } + // The future is genuinely pending: wrap it in an abortable + // `JoinHandle` (for cancellation) and materialize the `HostTask` now + // if the entrypoint deferred it, or use the already-current one. + let (join_handle, future) = JoinHandle::run(inner); + let future = Box::pin(future); + let task = match store.0.current_thread()? { + CurrentThread::Host(task) => task, + _ => store.0.materialize_host_task()?, + }; + { + let state = &mut store.0.concurrent_state_mut()?.get_mut(task)?.state; + assert!(matches!(state, HostTaskState::CalleeStarted)); + *state = HostTaskState::CalleeRunning(join_handle); + } + // It hasn't finished yet; add the future to // `ConcurrentState::futures` so it will be polled by the event // loop and allocate a waitable handle to return to the guest. @@ -5303,13 +5301,6 @@ impl CurrentThread { } } - fn host(&self) -> Option> { - match self { - Self::Host(id) => Some(*id), - _ => None, - } - } - fn is_none(&self) -> bool { matches!(self, Self::None) } diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 060fe76af60e..714496326031 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -382,17 +382,23 @@ where } // Enter the host by pushing a `HostTask` into the concurrent state. - // Sync-lowered borrow-free calls defer task creation entirely (see - // `host_task_create`); async-lowered calls need the task for subtask - // status/cancellation semantics, and borrow-ful calls need it as the + // Borrow-free calls defer task creation entirely (see + // `host_task_create`); borrow-ful calls keep the eager task as their // resource scope identity. - let defer_task = !async_ && !track_scope; + let defer_task = !track_scope; let host_task = store.0.host_task_create(track_scope, defer_task)?; let host_task_complete = if async_ { #[cfg(feature = "component-model-async")] { - self.call_async_lower(store.as_context_mut(), instance, ty, options, storage)? + self.call_async_lower( + store.as_context_mut(), + instance, + ty, + options, + storage, + track_scope, + )? } #[cfg(not(feature = "component-model-async"))] unreachable!( @@ -478,6 +484,7 @@ where ty: TypeFuncIndex, options: OptionsIndex, storage: &mut [MaybeUninit], + track_scope: bool, ) -> Result { use wasmtime_environ::component::MAX_FLAT_ASYNC_PARAMS; @@ -488,7 +495,11 @@ where // Lift the parameters, either from flat storage or from linear // memory. - let mut lift = LiftContext::new(store.0.store_opaque_mut(), options, instance)?; + let mut lift = if track_scope { + LiftContext::new(store.0.store_opaque_mut(), options, instance)? + } else { + LiftContext::new_without_scope(store.0.store_opaque_mut(), options, instance)? + }; let (params, rest) = self.load_params(&mut lift, ty, MAX_FLAT_ASYNC_PARAMS, storage)?; // Load/validate the return pointer, if present. From 0d618b026d6c9abb6d167c836268bb1292acd9a4 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Mon, 13 Jul 2026 19:15:34 +0200 Subject: [PATCH 6/8] Add fast path for immediately-ready results to `call_async_lower` Polls the result future once in-place and skips creating a `HostTask`, etc if it's ready immediately. Reduces costs of immediately-ready async calls by another 12% (60.5ns to 53ns) --- .../src/runtime/component/concurrent.rs | 77 ++++++------------- .../src/runtime/component/func/host.rs | 21 ++++- 2 files changed, 44 insertions(+), 54 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 76c89f25c592..80506d2fe656 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -823,13 +823,27 @@ pub(crate) enum WaitResult { Completed, } +/// Poll `future` once with a no-op waker in the store's tls context, +/// before any task machinery exists. For pending futures, +/// `Instance::continue_polling` should be used to add them to +/// `ConcurrentState::futures` and have them polled by the event loop. +#[inline] +pub(crate) fn poll_once(store: &mut dyn VMStore, future: &mut F) -> Poll +where + F: Future + Unpin, +{ + tls::set(store, || { + Pin::new(future).poll(&mut Context::from_waker(&Waker::noop())) + }) +} + /// Poll the specified future until it completes on behalf of a guest->host call /// using a sync-lowered import. /// -/// This is similar to `Instance::first_poll` except it's for sync-lowered -/// imports, meaning we don't need to handle cancellation and we can block the -/// caller until the task completes, at which point the caller can handle -/// lowering the result to the guest's stack and linear memory. +/// This is similar to `poll_once` + `Instance::continue_polling` except it's +/// for sync-lowered imports, meaning we don't need to handle cancellation and +/// we can block the caller until the task completes, at which point the caller +/// can handle lowering the result to the guest's stack and linear memory. pub(crate) fn poll_and_block( store: &mut dyn VMStore, future: impl Future> + Send + 'static, @@ -3246,19 +3260,12 @@ impl Instance { Ok(status.pack(waitable)) } - /// Poll the specified future once on behalf of a guest->host call using an - /// async-lowered import. - /// - /// If it returns `Ready`, return `Ok(None)`. Otherwise, if it returns - /// `Pending`, add it to the set of futures to be polled as part of this - /// instance's event loop until it completes, and then return - /// `Ok(Some(handle))` where `handle` is the waitable handle to return. - /// - /// Whether the future returns `Ready` immediately or later, the `lower` - /// function will be used to lower the result, if any, into the guest caller's - /// stack and linear memory. The `lower` function is invoked with `None` if - /// the future is cancelled. - pub(crate) fn first_poll( + /// Continue an async-lowered host call whose future was already given + /// its first poll (see `poll_once`) and is pending: wrap it in an + /// abortable `JoinHandle` (for cancellation), materialize the `HostTask` + /// if the entrypoint deferred it, and register the future with the event + /// loop, returning the waitable handle for the guest. + pub(crate) fn continue_polling( self, mut store: StoreContextMut<'_, T>, future: impl Future> + Send + 'static, @@ -3266,37 +3273,7 @@ impl Instance { ) -> Result> { let token = StoreToken::new(store.as_context_mut()); - // Poll the raw future once before creating anything. Note that no - // `HostTask` (and no abortable `JoinHandle` wrapper) exists yet for - // deferred (borrow-free) calls: the common case is that the future - // completes right here, in which case no task is ever created and - // the guest simply observes `Status::Returned` with no waitable - // handle. We can use a dummy `Waker` because a pending future is - // added to `ConcurrentState::futures` below and polled from the - // event loop. - let mut inner = Box::pin(future); - let poll = tls::set(store.0, || { - inner - .as_mut() - .poll(&mut Context::from_waker(&Waker::noop())) - }); - - match poll { - // It finished immediately; lower the result. - Poll::Ready(result) => { - let result = result?; - lower(store.as_context_mut(), Some(result))?; - return Ok(None); - } - - // Future isn't ready yet, so fall through. - Poll::Pending => {} - } - - // The future is genuinely pending: wrap it in an abortable - // `JoinHandle` (for cancellation) and materialize the `HostTask` now - // if the entrypoint deferred it, or use the already-current one. - let (join_handle, future) = JoinHandle::run(inner); + let (join_handle, future) = JoinHandle::run(future); let future = Box::pin(future); let task = match store.0.current_thread()? { CurrentThread::Host(task) => task, @@ -3308,10 +3285,6 @@ impl Instance { *state = HostTaskState::CalleeRunning(join_handle); } - // It hasn't finished yet; add the future to - // `ConcurrentState::futures` so it will be polled by the event - // loop and allocate a waitable handle to return to the guest. - // Wrap the future in a closure responsible for lowering the result into // the guest's stack and memory, as well as notifying any waiters that // the task returned. diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 714496326031..2840151159b4 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -22,6 +22,7 @@ use core::mem::{self, MaybeUninit}; #[cfg(feature = "async")] use core::pin::Pin; use core::ptr::NonNull; +use core::task::Poll; use wasmtime_environ::component::{ CanonicalAbiInfo, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, TypeFuncIndex, }; @@ -531,8 +532,24 @@ where None } #[cfg(feature = "component-model-async")] - HostResult::Future(future) => { - instance.first_poll(store, future, move |store, ret| { + HostResult::Future(mut future) => { + // Ready results are lowered exactly like the `Done` arm + // above, with no task, no `JoinHandle`, and no re-boxing + // (the future is already heap-pinned, hence `Unpin`). + match concurrent::poll_once(store.0, &mut future) { + Poll::Ready(result) => { + Self::lower_result_and_exit_call( + &mut LowerContext::new(store, options, instance), + ty, + Some(result?), + Destination::Memory(retptr), + )?; + storage[0].write(ValRaw::u32(Status::Returned.pack(None))); + return Ok(true); + } + Poll::Pending => {} + } + instance.continue_polling(store, future, move |store, ret| { Self::lower_result_and_exit_call( &mut LowerContext::new(store, options, instance), ty, From 118e483c1e89a5e6a7a6b3844a83e81e98bc9292 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Tue, 14 Jul 2026 01:15:05 +0200 Subject: [PATCH 7/8] cargo fmt fixes --- crates/environ/src/component/types_builder.rs | 4 +--- crates/wasmtime/src/runtime/component/concurrent.rs | 1 - crates/wasmtime/src/runtime/component/func/host.rs | 9 ++++++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/environ/src/component/types_builder.rs b/crates/environ/src/component/types_builder.rs index 6a5ebf2cd50a..a1b4da20885d 100644 --- a/crates/environ/src/component/types_builder.rs +++ b/crates/environ/src/component/types_builder.rs @@ -236,9 +236,7 @@ impl ComponentTypesBuilder { .iter() .map(|ty| self.valtype(types, ty)) .collect::>()?; - let contains_borrow = params - .iter() - .any(|ty| self.ty_contains_borrow_resource(ty)); + let contains_borrow = params.iter().any(|ty| self.ty_contains_borrow_resource(ty)); let params = self.new_tuple_type(params.into()); let results = self.new_tuple_type(results); let ty = TypeFunc { diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 80506d2fe656..a92e63868d47 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1669,7 +1669,6 @@ impl StoreOpaque { /// thread into a fully-materialized `CurrentThread`. #[cold] fn force_deferred_current_thread(&mut self) -> Result { - // The component instance whose adapters pushed the deferred frames; all // frames in a guest-to-guest, sync-to-sync call chain of fused adapters // live within a single `wasmtime::component::Instance` (because diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 2840151159b4..85fcd7749e53 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -407,7 +407,14 @@ where when `component-model-async` feature disabled" ); } else { - self.call_sync_lower(store.as_context_mut(), instance, ty, options, storage, track_scope)?; + self.call_sync_lower( + store.as_context_mut(), + instance, + ty, + options, + storage, + track_scope, + )?; true }; From 8da58dc64c2e243189b64f8802005e6a81ba2ca9 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Tue, 14 Jul 2026 15:36:00 +0200 Subject: [PATCH 8/8] Fix CI failures --- crates/wasmtime/src/runtime/component/func/host.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 85fcd7749e53..620069c1e113 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -22,6 +22,7 @@ use core::mem::{self, MaybeUninit}; #[cfg(feature = "async")] use core::pin::Pin; use core::ptr::NonNull; +#[cfg(feature = "component-model-async")] use core::task::Poll; use wasmtime_environ::component::{ CanonicalAbiInfo, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, TypeFuncIndex,