From 1fc502c6294cac011dd95d85e02ba6ac7ae93597 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Mon, 13 Jul 2026 18:10:10 +0200 Subject: [PATCH 1/2] 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 #13887, 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 future commits. --- crates/environ/src/component/types.rs | 4 ++++ crates/environ/src/component/types_builder.rs | 6 +++-- .../src/runtime/component/concurrent.rs | 22 +++++++++++++++---- .../runtime/component/concurrent_disabled.rs | 14 ++++++++---- .../src/runtime/component/func/host.rs | 14 ++++++++---- 5 files changed, 46 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..a1b4da20885d 100644 --- a/crates/environ/src/component/types_builder.rs +++ b/crates/environ/src/component/types_builder.rs @@ -230,16 +230,18 @@ 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 a5a34e481557..26e66262f933 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1812,9 +1812,17 @@ 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()?; @@ -1846,14 +1854,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 abc6aabadf7d1673e4f4e9b22a40addda420945c Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Mon, 13 Jul 2026 18:42:43 +0200 Subject: [PATCH 2/2] Skip scope resolution in LiftContext if possible Extends skipping scope resolution introduced in the previous commit to also cover the LiftContext used in sync calls. Plus, inline a bunch of single-instantiation generics. Relative to the previous commit, this reduces call overhead by another 31% for sync calls (46ns to 32ns), 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 | 20 +++++++++++-- .../src/runtime/component/func/options.rs | 28 ++++++++++++++++++- .../wasmtime/src/runtime/component/store.rs | 6 ++++ 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 26e66262f933..f2096ddcd38e 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1812,6 +1812,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, @@ -1838,6 +1839,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(()); @@ -1854,6 +1856,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>, @@ -2355,6 +2358,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 c5e2db30f0fa..426c3827d818 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>, @@ -394,7 +395,14 @@ 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 }; @@ -417,6 +425,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>, @@ -424,8 +433,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) { @@ -533,6 +547,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<'_>, @@ -568,6 +583,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) {