Don't re-capture backtraces when propagating traps through host frames (#5049)

* Add a benchmark for traps with many Wasm<-->host calls on the stack

* Add a test for expected Wasm stack traces with Wasm<--host calls on the stack when we trap

* Don't re-capture backtraces when propagating traps through host frames

This fixes some accidentally quadratic code where we would re-capture a Wasm
stack trace (takes `O(n)` time) every time we propagated a trap through a host
frame back to Wasm (can happen `O(n)` times). And `O(n) * O(n) = O(n^2)`, of
course. Whoops. After this commit, it trapping with a call stack that is `n`
frames deep of Wasm-to-host-to-Wasm calls just captures a single backtrace and
is therefore just a proper `O(n)` time operation, as it is intended to be.

Now we explicitly track whether we need to capture a Wasm backtrace or not when
raising a trap. This unfortunately isn't as straightforward as one might hope,
however, because of the split between `wasmtime::Trap` and
`wasmtime_runtime::Trap`. We need to decide whether or not to capture a Wasm
backtrace inside `wasmtime_runtime` but in order to determine whether to do that
or not we need to reflect on the `anyhow::Error` and see if it is a
`wasmtime::Trap` that already has a backtrace or not. This can't be done the
straightforward way because it would introduce a cyclic dependency between the
`wasmtime` and `wasmtime-runtime` crates. We can't merge those two `Trap`
types-- at least not without effectively merging the whole `wasmtime` and
`wasmtime-runtime` crates together, which would be a good idea in a perfect
world but would be a *ton* of ocean boiling from where we currently are --
because `wasmtime::Trap` does symbolication of stack traces which relies on
module registration information data that resides inside the `wasmtime` crate
and therefore can't be moved into `wasmtime-runtime`. We resolve this problem by
adding a boolean to `wasmtime_runtime::raise_user_trap` that controls whether we
should capture a Wasm backtrace or not, and then determine whether we need a
backtrace or not at each of that function's call sites, which are in `wasmtime`
and therefore can do the reflection to determine whether the user trap already
has a backtrace or not. Phew!

Fixes #5037

* debug assert that we don't record unnecessary backtraces for traps

* Add assertions around `needs_backtrace`

Unfortunately we can't do

    debug_assert_eq!(needs_backtrace, trap.inner.backtrace.get().is_some());

because `needs_backtrace` doesn't consider whether Wasm backtraces have been
disabled via config.

* Consolidate `needs_backtrace` calculation followed by calling `raise_user_trap` into one place
This commit is contained in:
Nick Fitzgerald
2022-10-13 07:22:46 -07:00
committed by GitHub
parent f96491f333
commit a2f846f124
9 changed files with 221 additions and 25 deletions

View File

@@ -69,6 +69,72 @@ fn test_trap_trace() -> Result<()> {
Ok(())
}
#[test]
fn test_trap_through_host() -> Result<()> {
let wat = r#"
(module $hello_mod
(import "" "" (func $host_func_a))
(import "" "" (func $host_func_b))
(func $a (export "a")
call $host_func_a
)
(func $b (export "b")
call $host_func_b
)
(func $c (export "c")
unreachable
)
)
"#;
let engine = Engine::default();
let module = Module::new(&engine, wat)?;
let mut store = Store::<()>::new(&engine, ());
let host_func_a = Func::new(
&mut store,
FuncType::new(vec![], vec![]),
|mut caller, _args, _results| {
caller
.get_export("b")
.unwrap()
.into_func()
.unwrap()
.call(caller, &[], &mut [])?;
Ok(())
},
);
let host_func_b = Func::new(
&mut store,
FuncType::new(vec![], vec![]),
|mut caller, _args, _results| {
caller
.get_export("c")
.unwrap()
.into_func()
.unwrap()
.call(caller, &[], &mut [])?;
Ok(())
},
);
let instance = Instance::new(
&mut store,
&module,
&[host_func_a.into(), host_func_b.into()],
)?;
let a = instance
.get_typed_func::<(), (), _>(&mut store, "a")
.unwrap();
let err = a.call(&mut store, ()).unwrap_err();
let trace = err.trace().expect("backtrace is available");
assert_eq!(trace.len(), 3);
assert_eq!(trace[0].func_name(), Some("c"));
assert_eq!(trace[1].func_name(), Some("b"));
assert_eq!(trace[2].func_name(), Some("a"));
Ok(())
}
#[test]
#[allow(deprecated)]
fn test_trap_backtrace_disabled() -> Result<()> {