Skip to content

Commit 0cc8442

Browse files
committed
Resolve debug_function_index in O(1) instead of scanning every function
Instance::debug_function_index previously scanned every function in the instance, comparing VMFuncRef pointers, to find the one matching a given Func. That makes a whole-store debug snapshot (which calls this once per captured funcref) quadratic overall, which was raised in review. Both an instance's imported-function table and its defined-function funcref table are contiguous, fixed-stride arrays at statically known VMContext offsets, so a VMFuncRef pointer's position in either array is computable directly via pointer arithmetic instead of a linear scan. Imported functions are indexed by FuncIndex directly. Defined ("escaped") functions are indexed by a compact FuncRefIndex that is *not* assigned in FuncIndex order (slots are handed out in the order functions are discovered to escape during module translation, e.g. export declaration order), so resolving a FuncRefIndex back to a FuncIndex needs an explicit reverse table rather than arithmetic. That table only depends on compiled module metadata, not on any particular Instance, so it's built once, lazily, and cached on Module -- shared by every Instance and every debug snapshot of that module, rather than rebuilt per lookup. Adds a regression test with functions placed into a table out of index order, to pin down that the reverse table is used correctly rather than assuming (incorrectly) that escape order tracks function-index order.
1 parent f84d904 commit 0cc8442

3 files changed

Lines changed: 125 additions & 13 deletions

File tree

crates/wasmtime/src/runtime/debug.rs

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub use wasmtime_environ::ModulePC;
2525
use wasmtime_environ::{
2626
DefinedFuncIndex, EntityIndex, FrameInstPos, FrameStackShape, FrameStateSlot,
2727
FrameStateSlotOffset, FrameTableBreakpointData, FrameTableDescriptorIndex, FrameValType,
28-
FuncIndex, FuncKey, GlobalIndex, MemoryIndex, TableIndex, TagIndex, Trap,
28+
FuncIndex, FuncKey, GlobalIndex, MemoryIndex, PtrSize, TableIndex, TagIndex, Trap,
2929
};
3030
use wasmtime_unwinder::{Frame, FrameCursor};
3131

@@ -321,6 +321,10 @@ impl Instance {
321321
/// This is the inverse of [`Instance::debug_function`]. It is only
322322
/// available when guest debugging is enabled for the store's engine, and
323323
/// returns `None` if `func` is not one of this instance's functions.
324+
///
325+
/// This runs in constant time: it locates `func`'s `VMFuncRef` directly
326+
/// via pointer arithmetic against this instance's `VMContext` layout,
327+
/// rather than scanning every function in the instance.
324328
pub fn debug_function_index(
325329
&self,
326330
mut store: impl AsContextMut,
@@ -331,17 +335,36 @@ impl Instance {
331335
return None;
332336
}
333337

334-
let func_ref = func.vm_func_ref(store);
335-
let module = self._module(store).env_module();
336-
for index in 0..u32::try_from(module.functions.len()).ok()? {
337-
let candidate = self
338-
.debug_export(store, FuncIndex::from_bits(index).into())?
339-
.into_func()?;
340-
if candidate.vm_func_ref(store) == func_ref {
341-
return Some(index);
342-
}
338+
let func_ref = func.vm_func_ref(store).as_ptr() as usize;
339+
let module = self._module(store);
340+
let offsets = module.offsets();
341+
let stride = usize::from(offsets.ptr.vm_func_ref().size());
342+
let vmctx = store.instance(self.id()).vmctx().as_ptr() as usize;
343+
344+
// Imported functions live in this instance's `imported_functions`
345+
// array, indexed directly by `FuncIndex`.
346+
let imports_begin =
347+
vmctx + usize::try_from(offsets.vmctx_imported_functions_begin()).ok()?;
348+
if let Some(index) = array_index_of(
349+
func_ref,
350+
imports_begin,
351+
offsets.num_imported_functions,
352+
stride,
353+
) {
354+
return Some(index);
343355
}
344-
None
356+
357+
// Defined ("escaped") functions live in a separate `func_refs`
358+
// array, indexed by a compact `FuncRefIndex` that isn't in
359+
// `FuncIndex` order; translate it back via the module's cached
360+
// reverse table.
361+
let func_refs_begin = vmctx + usize::try_from(offsets.vmctx_func_refs_begin()).ok()?;
362+
let position =
363+
array_index_of(func_ref, func_refs_begin, offsets.num_escaped_funcs, stride)?;
364+
module
365+
.debug_func_ref_to_func_index()
366+
.get(position as usize)
367+
.map(|index| index.as_u32())
345368
}
346369

347370
/// Get access to a tag within this instance's tag index space.
@@ -384,6 +407,17 @@ impl Instance {
384407
}
385408
}
386409

410+
/// If `ptr` falls within the `len`-element array of `stride`-byte entries
411+
/// starting at `begin`, returns its index within that array.
412+
fn array_index_of(ptr: usize, begin: usize, len: u32, stride: usize) -> Option<u32> {
413+
let offset = ptr.checked_sub(begin)?;
414+
if stride == 0 || offset % stride != 0 {
415+
return None;
416+
}
417+
let index = u32::try_from(offset / stride).ok()?;
418+
(index < len).then_some(index)
419+
}
420+
387421
impl<'a, T> StoreContext<'a, T> {
388422
/// Return all breakpoints.
389423
pub fn breakpoints(self) -> Option<impl Iterator<Item = Breakpoint> + 'a> {

crates/wasmtime/src/runtime/module.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ use core::ptr::NonNull;
1919
use std::{fs::File, path::Path};
2020
use wasmparser::{Parser, ValidPayload, Validator};
2121
use wasmtime_environ::{
22-
CompiledFunctionsTable, CompiledModuleInfo, EntityIndex, FuncKey, HostPtr, ModuleTypes,
23-
ObjectKind, StaticModuleIndex, TypeTrace, VMOffsets, VMSharedTypeIndex, WasmChecksum,
22+
CompiledFunctionsTable, CompiledModuleInfo, EntityIndex, EntityRef, FuncIndex, FuncKey,
23+
HostPtr, ModuleTypes, ObjectKind, StaticModuleIndex, TypeTrace, VMOffsets, VMSharedTypeIndex,
24+
WasmChecksum, packed_option::ReservedValue,
2425
};
2526
mod registry;
2627

@@ -151,6 +152,23 @@ struct ModuleInner {
151152
/// instantiated.
152153
memory_images: OnceLock<Option<ModuleMemoryImages>>,
153154

155+
/// Lazily-built reverse map from a function's `FuncRefIndex` (its
156+
/// position within this module's `func_refs` array) back to its
157+
/// `FuncIndex`.
158+
///
159+
/// This exists solely to support `Instance::debug_function_index`,
160+
/// which needs to invert a `VMFuncRef` pointer back to the function
161+
/// index it came from. `FuncRefIndex`s are *not* assigned in `FuncIndex`
162+
/// order (they're assigned in the order functions are discovered to
163+
/// escape while translating the module, e.g. export declaration order),
164+
/// so that inversion can't be done with arithmetic or a binary search;
165+
/// it needs an explicit table. Building the table is a purely
166+
/// module-level computation (it only depends on compiled module
167+
/// metadata, not on any particular `Instance`), so it's cached here and
168+
/// shared by every `Instance` created from this `Module`, rather than
169+
/// being rebuilt per-instance or per-lookup.
170+
debug_func_ref_to_func_index: OnceLock<Vec<FuncIndex>>,
171+
154172
/// Flag indicating whether this module can be serialized or not.
155173
#[cfg(any(feature = "cranelift", feature = "winch"))]
156174
serializable: bool,
@@ -553,6 +571,7 @@ impl Module {
553571
engine: engine.clone(),
554572
code,
555573
memory_images: OnceLock::new(),
574+
debug_func_ref_to_func_index: OnceLock::new(),
556575
module,
557576
#[cfg(any(feature = "cranelift", feature = "winch"))]
558577
serializable,
@@ -1118,6 +1137,25 @@ impl Module {
11181137
&self.inner.offsets
11191138
}
11201139

1140+
/// Returns a table mapping each escaping function's `FuncRefIndex`
1141+
/// (i.e. its position in the `func_refs` array within an instance's
1142+
/// `VMContext`) back to its `FuncIndex`.
1143+
///
1144+
/// Built lazily on first use and cached for the lifetime of this
1145+
/// `Module`; see the doc comment on `ModuleInner::debug_func_ref_to_func_index`.
1146+
pub(crate) fn debug_func_ref_to_func_index(&self) -> &[FuncIndex] {
1147+
self.inner.debug_func_ref_to_func_index.get_or_init(|| {
1148+
let env_module = self.env_module();
1149+
let mut map = vec![FuncIndex::reserved_value(); env_module.num_escaped_funcs];
1150+
for (func_index, ty) in env_module.functions.iter() {
1151+
if ty.is_escaping() {
1152+
map[ty.func_ref.index()] = func_index;
1153+
}
1154+
}
1155+
map
1156+
})
1157+
}
1158+
11211159
/// Return the unique-within-Engine ID for this module.
11221160
///
11231161
/// Allows distinguishing module identities when introspecting

tests/all/debug.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,46 @@ fn debug_function_index_requires_guest_debug() -> wasmtime::Result<()> {
545545
Ok(())
546546
}
547547

548+
// `debug_function_index` resolves a function's position in its module by
549+
// inverting the pointer arithmetic used to locate a `VMFuncRef` (rather than
550+
// scanning every function looking for a pointer match). Functions are only
551+
// assigned a slot in that array once they "escape" the module (e.g. via
552+
// export, `ref.func`, or table placement), and slots are handed out in the
553+
// order escaping is *discovered* during translation -- not in function-index
554+
// order. This module deliberately declares four private functions but only
555+
// places them into the table in reverse order, so their escape order (and
556+
// thus their position in the funcref array) is the opposite of their
557+
// function index. If the lookup assumed escape order tracked function-index
558+
// order, this would resolve every index but the endpoints incorrectly.
559+
#[test]
560+
fn debug_function_index_with_non_monotonic_escape_order() -> wasmtime::Result<()> {
561+
let (module, mut store) = get_module_and_store(
562+
|_| {},
563+
r#"
564+
(module
565+
(table 4 funcref)
566+
(elem (i32.const 0) $f3 $f2 $f1 $f0)
567+
(func $f0 (result i32) i32.const 0)
568+
(func $f1 (result i32) i32.const 1)
569+
(func $f2 (result i32) i32.const 2)
570+
(func $f3 (result i32) i32.const 3))
571+
"#,
572+
)?;
573+
let instance = Instance::new(&mut store, &module, &[])?;
574+
575+
for index in 0..4u32 {
576+
let f = instance.debug_function(&mut store, index).unwrap();
577+
assert_eq!(
578+
instance.debug_function_index(&mut store, &f),
579+
Some(index),
580+
"function {index} must round-trip through debug_function_index \
581+
even though its funcref slot was assigned out of index order"
582+
);
583+
}
584+
585+
Ok(())
586+
}
587+
548588
#[test]
549589
#[cfg_attr(miri, ignore)]
550590
#[cfg(target_pointer_width = "64")] // Threads not supported on 32-bit systems.

0 commit comments

Comments
 (0)