Skip to content

Commit 7b8d68d

Browse files
committed
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.
1 parent c911bb4 commit 7b8d68d

5 files changed

Lines changed: 45 additions & 14 deletions

File tree

crates/environ/src/component/types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,10 @@ pub struct TypeComponentInstance {
585585
pub struct TypeFunc {
586586
/// Whether or not this is an async function.
587587
pub async_: bool,
588+
/// Whether any parameter (transitively) contains a `borrow` handle,
589+
/// letting the runtime skip borrow-scope tracking when lending is
590+
/// impossible. (Borrows are only valid in parameters, not results.)
591+
pub contains_borrow: bool,
588592
/// Names of parameters.
589593
pub param_names: Vec<String>,
590594
/// Parameters to the function represented as a tuple.

crates/environ/src/component/types_builder.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,16 +230,20 @@ impl ComponentTypesBuilder {
230230
.params
231231
.iter()
232232
.map(|(_name, ty)| self.valtype(types, ty))
233-
.collect::<Result<_>>()?;
233+
.collect::<Result<Vec<_>>>()?;
234234
let results = ty
235235
.result
236236
.iter()
237237
.map(|ty| self.valtype(types, ty))
238238
.collect::<Result<_>>()?;
239-
let params = self.new_tuple_type(params);
239+
let contains_borrow = params
240+
.iter()
241+
.any(|ty| self.ty_contains_borrow_resource(ty));
242+
let params = self.new_tuple_type(params.into());
240243
let results = self.new_tuple_type(results);
241244
let ty = TypeFunc {
242245
async_: ty.async_,
246+
contains_borrow,
243247
param_names,
244248
params,
245249
results,

crates/wasmtime/src/runtime/component/concurrent.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,9 +1812,14 @@ impl StoreOpaque {
18121812
/// relatively expensive table manipulations. This would ideally be
18131813
/// optimized to avoid the full allocation of a `HostTask` in at least some
18141814
/// situations.
1815-
pub(crate) fn host_task_create(&mut self) -> Result<Option<TableId<HostTask>>> {
1815+
pub(crate) fn host_task_create(&mut self, track_scope: bool) -> Result<Option<TableId<HostTask>>> {
18161816
if !self.concurrency_support() {
1817-
self.enter_call_not_concurrent()?;
1817+
// Resource-borrow scope tracking is only needed when the
1818+
// function's parameters can actually contain `borrow` handles;
1819+
// this is precomputed per function type.
1820+
if track_scope {
1821+
self.enter_call_not_concurrent()?;
1822+
}
18181823
return Ok(None);
18191824
}
18201825
let caller = self.current_guest_thread()?;
@@ -1846,14 +1851,20 @@ impl StoreOpaque {
18461851
/// Note that this isn't invoked when the host is invoked asynchronously and
18471852
/// the host isn't complete yet. In that situation the host task persists
18481853
/// and will be cleaned up separately in `subtask_drop`
1849-
pub(crate) fn host_task_delete(&mut self, task: Option<TableId<HostTask>>) -> Result<()> {
1854+
pub(crate) fn host_task_delete(
1855+
&mut self,
1856+
task: Option<TableId<HostTask>>,
1857+
track_scope: bool,
1858+
) -> Result<()> {
18501859
match task {
18511860
Some(task) => {
18521861
log::trace!("delete host task {task:?}");
18531862
self.concurrent_state_mut()?.delete(task)?;
18541863
}
18551864
None => {
1856-
self.exit_call_not_concurrent();
1865+
if track_scope {
1866+
self.exit_call_not_concurrent();
1867+
}
18571868
}
18581869
}
18591870
Ok(())

crates/wasmtime/src/runtime/component/concurrent_disabled.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,16 +162,22 @@ impl StoreOpaque {
162162
Ok(self.exit_call_not_concurrent())
163163
}
164164

165-
pub(crate) fn host_task_create(&mut self) -> Result<()> {
166-
self.enter_call_not_concurrent()
165+
pub(crate) fn host_task_create(&mut self, track_scope: bool) -> Result<()> {
166+
if track_scope {
167+
self.enter_call_not_concurrent()?;
168+
}
169+
Ok(())
167170
}
168171

169172
pub(crate) fn host_task_reenter_caller(&mut self) -> Result<()> {
170173
Ok(())
171174
}
172175

173-
pub(crate) fn host_task_delete(&mut self, (): ()) -> Result<()> {
174-
Ok(self.exit_call_not_concurrent())
176+
pub(crate) fn host_task_delete(&mut self, (): (), track_scope: bool) -> Result<()> {
177+
if track_scope {
178+
self.exit_call_not_concurrent();
179+
}
180+
Ok(())
175181
}
176182

177183
pub(crate) fn check_blocking(&mut self) -> crate::Result<()> {

crates/wasmtime/src/runtime/component/func/host.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,9 @@ where
369369
) -> Result<()> {
370370
let vminstance = instance.id().get(store.0);
371371
let async_ = vminstance.component().env_component().options[options].async_;
372+
// Skip resource-borrow scope tracking when the signature makes
373+
// lending impossible (precomputed at compile time).
374+
let track_scope = vminstance.component().types()[ty].contains_borrow;
372375

373376
// If this is a synchronous-lower of a host-async function, then the
374377
// guest is blocking. Test, in the context of the guest task, if that's
@@ -378,7 +381,7 @@ where
378381
}
379382

380383
// Enter the host by pushing a `HostTask` into the concurrent state.
381-
let host_task = store.0.host_task_create()?;
384+
let host_task = store.0.host_task_create(track_scope)?;
382385

383386
let host_task_complete = if async_ {
384387
#[cfg(feature = "component-model-async")]
@@ -401,7 +404,7 @@ where
401404
// function transitively would have updated the current guest thread to
402405
// the caller of this host function.
403406
if host_task_complete {
404-
store.0.host_task_delete(host_task)?;
407+
store.0.host_task_delete(host_task, track_scope)?;
405408
}
406409

407410
Ok(())
@@ -572,8 +575,11 @@ where
572575
dst: Destination<'_>,
573576
) -> Result<()> {
574577
// Before lowering below semantically ensure that the caller has dropped
575-
// all of its borrows and such.
576-
lower.validate_scope_exit()?;
578+
// all of its borrows and such. Skipped when the signature cannot
579+
// contain borrows (in which case no scope was entered either).
580+
if lower.types[ty].contains_borrow {
581+
lower.validate_scope_exit()?;
582+
}
577583

578584
// At this point we're transitioning back to the caller task which means
579585
// that the current task needs to be updated. This will restore the

0 commit comments

Comments
 (0)