erikrose opened PR #12990 from erikrose:epoch-mmu to bytecodealliance:main:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [ ] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [ ] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [ ] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
github-actions[bot] added the label cranelift on PR #12990.
github-actions[bot] added the label cranelift:area:x64 on PR #12990.
github-actions[bot] added the label cranelift:area:machinst on PR #12990.
github-actions[bot] added the label cranelift:docs on PR #12990.
github-actions[bot] added the label wasmtime:config on PR #12990.
github-actions[bot] added the label wasmtime:api on PR #12990.
github-actions[bot] added the label cranelift:meta on PR #12990.
github-actions[bot] commented on PR #12990:
Label Messager: wasmtime:config
It looks like you are changing Wasmtime's configuration options. Make sure to
complete this check list:
[ ] If you added a new
Configmethod, you wrote extensive documentation for
it.<details>
Our documentation should be of the following form:
```text
Short, simple summary sentence.More details. These details can be multiple paragraphs. There should be
information about not just the method, but its parameters and results as
well.Is this method fallible? If so, when can it return an error?
Can this method panic? If so, when does it panic?
Example
Optional example here.
```</details>
[ ] If you added a new
Configmethod, or modified an existing one, you
ensured that this configuration is exercised by the fuzz targets.<details>
For example, if you expose a new strategy for allocating the next instance
slot inside the pooling allocator, you should ensure that at least one of our
fuzz targets exercises that new strategy.Often, all that is required of you is to ensure that there is a knob for this
configuration option in [wasmtime_fuzzing::Config][fuzzing-config] (or one
of its nestedstructs).Rarely, this may require authoring a new fuzz target to specifically test this
configuration. See [our docs on fuzzing][fuzzing-docs] for more details.</details>
[ ] If you are enabling a configuration option by default, make sure that it
has been fuzzed for at least two weeks before turning it on by default.[fuzzing-config]: https://github.com/bytecodealliance/wasmtime/blob/ca0e8d0a1d8cefc0496dba2f77a670571d8fdcab/crates/fuzzing/src/generators.rs#L182-L194
[fuzzing-docs]: https://docs.wasmtime.dev/contributing-fuzzing.html
<details>
To modify this label's message, edit the <code>.github/label-messager/wasmtime-config.md</code> file.
To add new label messages or remove existing label messages, edit the
<code>.github/label-messager.json</code> configuration file.</details>
erikrose updated PR #12990.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [ ] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [ ] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [ ] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
- [ ] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [ ] Make
wasmtime rundo MMU interrupts at the frequency ofWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [ ] Write about SAFETY everywhere I do unsafe things.
If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
- [ ] Make sure we can't overflow the stack undetected.
- [ ] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [ ] Make
wasmtime rundo MMU interrupts at the frequency ofWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [ ] Write about SAFETY everywhere I do unsafe things.
If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [ ] Make sure we can't overflow the stack undetected.
- [ ] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [ ] Make
wasmtime rundo MMU interrupts at the frequency ofWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [ ] Write about SAFETY everywhere I do unsafe things.
If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [ ] Make sure we can't overflow the stack undetected.
- [ ] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [ ] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [ ] Write about SAFETY everywhere I do unsafe things.
If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose updated PR #12990.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [ ] Make sure we can't overflow the stack undetected.
- [ ] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [x] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [ ] Write about SAFETY everywhere I do unsafe things.
If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [ ] Make sure we can't overflow the stack undetected.
- [ ] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [x] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [ ] Write about SAFETY everywhere I do unsafe things.
- [ ] Make --mmu-interruption report an error on unsupported platforms.
If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [ ] Make sure we can't overflow the stack undetected.
- [ ] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [x] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [ ] Write about SAFETY everywhere I do unsafe things.
- [ ] Make
--epoch-interruption-via-mmureport an error on unsupported platforms.If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose updated PR #12990.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [ ] Make sure we can't overflow the stack undetected.
- [ ] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [x] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [x] Test 2 tasks interrupting each other several times to make sure interruption works more than once.
- [ ] Write about SAFETY everywhere I do unsafe things.
- [ ] Make
--epoch-interruption-via-mmureport an error on unsupported platforms.If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [ ] Make sure we can't overflow the stack undetected.
- [x] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [x] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [x] Test 2 tasks interrupting each other several times to make sure interruption works more than once.
- [ ] Write about SAFETY everywhere I do unsafe things.
- [ ] Make
--epoch-interruption-via-mmureport an error on unsupported platforms.If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose updated PR #12990.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [ ] Make sure we can't overflow the stack undetected.
- [x] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [x] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [x] Test 2 tasks interrupting each other several times to make sure interruption works more than once.
- [x] Write about SAFETY everywhere I do unsafe things.
- [ ] Make
--epoch-interruption-via-mmureport an error on unsupported platforms.If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [ ] Make sure we can't overflow the stack undetected.
- [ ] Call CLI flag (and other mentions of the term in code) "MMU interrupts" rather than "epoch interruption via MMU".
- [x] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [x] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [x] Test 2 tasks interrupting each other several times to make sure interruption works more than once.
- [x] Write about SAFETY everywhere I do unsafe things.
- [ ] Make
--epoch-interruption-via-mmureport an error on unsupported platforms.If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [ ] Make sure we can't overflow the stack undetected.
- [ ] Call CLI flag (and other mentions of the term in code) "MMU interrupts" rather than "epoch interruption via MMU".
- [x] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [x] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [x] Test 2 tasks interrupting each other several times to make sure interruption works more than once.
- [x] Write about SAFETY everywhere I do unsafe things.
- [x] Make
--epoch-interruption-via-mmureport an error on unsupported platforms.If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose updated PR #12990.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [x] Make sure we can't overflow the stack undetected.
- [ ] Call CLI flag (and other mentions of the term in code) "MMU interrupts" rather than "epoch interruption via MMU".
- [x] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [x] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [x] Test 2 tasks interrupting each other several times to make sure interruption works more than once.
- [x] Write about SAFETY everywhere I do unsafe things.
- [x] Make
--epoch-interruption-via-mmureport an error on unsupported platforms.If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [x] Make sure we can't overflow the stack undetected.
- [x] Call CLI flag (and other mentions of the term in code) "MMU interrupts" rather than "epoch interruption via MMU".
- [x] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [x] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [x] Test 2 tasks interrupting each other several times to make sure interruption works more than once.
- [x] Write about SAFETY everywhere I do unsafe things.
- [x] Make
--epoch-interruption-via-mmureport an error on unsupported platforms.If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose edited PR #12990:
This is an implementation of #1749, specifically @cfallin's roadmap, with the goal of reducing the overhead of checking for the end of epochs.
Paul ran some benchmarks on this (broadly agreeing with our real-world experiments) which tell us:
- Current compare-against-a-deadline
-Wepoch-interruption=yis a 14.4% hit versus doing nothing.- Doing only dead loads in function prologues and loop headers (which were all that was implemented in this patch at the time of the bench) brings that down to a 2.8% hit. There will be some additional hit from the signal handler that actually effects the task switch, but that's on the cold path.
The above numbers are from SpiderMonkey, which I deem the most representative benchmark.
Status:
- [X] Add unmapped-on-interrupt page and pointer to it in vmctx.
- [X] Add method for embedder to call to bring an epoch to a close.
- [X] Add
DeadLoadWithContextCranelift instruction, and use it.- [x] Have
DeadLoadWithContextemit metadata into compiled-artifact tables to tell the signal handler this is an interruption-point load.- [x] Add logic to signal handler that, when seeing such a PC, updates state to redirect to the stub, saving the original PC (probably in the scratch register).
- [x] Add that stub, which saves all register state and invokes a hostcall with the recovered vmctx.
Polishing tasks:
- [x] Make sure we can't overflow the stack undetected.
- [x] Call CLI flag (and other mentions of the term in code) "MMU interrupts" rather than "epoch interruption via MMU".
- [x] Make sure nothing happens when
--epoch-interruption-via-mmuis off.- [x] Make
wasmtime rundo MMU interrupts afterWasmOptions::timeout, just as currently happens with classic epoch-interruption.- [x] Test 2 tasks interrupting each other several times to make sure interruption works more than once.
- [x] Write about SAFETY everywhere I do unsafe things.
- [x] Make
--epoch-interruption-via-mmureport an error on unsupported platforms.- [ ] Make sure Config-option docs and fuzzing are up to snuff.
If the TLB shootdown arising from the frobbing of privs on the "interrupt page" proves too expensive, we can try a more indirect load instead, where, instead of messing with page privs, we mess with the address we're dead-loading from so it points to either a (statically) allowed or forbidden page. (Chris floated this idea at the 2026-04-08 Cranelift meeting.) Not many of the other mechanics need change.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose updated PR #12990.
erikrose has marked PR #12990 as ready for review.
erikrose requested alexcrichton for a review on PR #12990.
erikrose requested wasmtime-compiler-reviewers for a review on PR #12990.
erikrose requested wasmtime-wasi-reviewers for a review on PR #12990.
erikrose requested wasmtime-core-reviewers for a review on PR #12990.
erikrose requested wasmtime-default-reviewers for a review on PR #12990.
alexcrichton commented on PR #12990:
@erikrose do you feel this is landable as-is modulo review? I see the review request now and wanted to confirm. If so, before going too deep into this, have you done performance testing in the contexts of where this is expected to provide a benefit?
erikrose commented on PR #12990:
Yes. I'm about to commit a more in-depth comment on
Config::mmu_interruption()as the bot above requested (perhaps post-dinner), and I'll also look into fuzzing, but I think it's landable as it stands.The perf testing is interesting: it should bench out the same as when we did it before on the no-interrupt path (14.4% overhead for epochs, 2.8% for this). The yes-interrupt path is probably going to depend on how fully loaded the machine is: things like TLB shoot-downs interacting with multiple cores, all furiously running Wasm guest code. My thinking was to get it landed upstream, update the version of wasmtime we're using, and then try the new flag in some canary contexts to get real-world numbers.
Of course, I'm happy to do Sightglass benchmarks of the yes-interrupt path at various intervals (every .1ms, 1ms, 10ms) and see how those compare, though it'll require some coding. Sightglass is better than nothing, and I was probably going to do that anyway just in case huge surprises come out of it. If you want to gate landing on that, it's fine with me. But I don't think it needs to hold up review.
I probably should have led with this, but we're having Saúl help us out for a bit, and he's going to take a look over the next day or two before he becomes unavailable for a span. @saulecabrera Maybe you want to claim review? I'll let you guys fight it out. Thanks!
cfallin commented on PR #12990:
I'm happy to look at the Cranelift side of this. (And Alex should definitely look at the runtime side...)
On first skim, I think the overall shape of the "dead load" seems fine, but a few style and design comments:
It'd be great not to refer to bits of Wasmtime in the Cranelift instruction definition. Cranelift should stand on its own;
dead_load_with_contextcan have a description that maybe says something like "Useful when implementing virtual memory-triggered interrupts, with context in a fixed register available to the interrupt handler."I also see a TODO and a question in the description; we should resolve these.
The register choices are more or less a function of ABI, so let's (i) assert that the instruction occurs in a function with a signature that has an ABI we control (let's say just
tail) and then in the doc-comment on theTailarm of theCallConvenum, alongside the comments defining which registers are used for exception payload, let's add a note about registers used for dead-load-with-context payload.I really don't like the yet-another-kind-of-metadata on
MachBuffer(mmu_interrupt) -- again it's a weird Wasmtime-specific thing, and adds complexity and maintenance burden. Is there a reason we can't attach a trap code to the load, like we do other loads, and distinguish this mechanism based on that? That also avoids the need to add a whole new metadata section to the compiled artifact.At least from my point of view, I think we should get an implementation on another architecture (let's say aarch64) as well; this shows generality, and we have quite a few aarch64 users too. I think we should also probably have perf results showing that the interruption behavior is not catastrophic (due to TLB shootdowns as you mention): the result of that experiment could change the mechanism (use an indirect approach instead), and/or could indicate that the whole thing is not worthwhile.
cfallin edited a comment on PR #12990:
I'm happy to look at the Cranelift side of this. (And Alex should definitely look at the runtime side...)
On first skim, I think the overall shape of the "dead load" seems fine, but a few style and design comments:
It'd be great not to refer to bits of Wasmtime in the Cranelift instruction definition. Cranelift should stand on its own;
dead_load_with_contextcan have a description that maybe says something like "Useful when implementing virtual memory-triggered interrupts, with context in a fixed register available to the interrupt handler."I also see a TODO and a question in the description; we should resolve these.
The register choices are more or less a function of ABI, so let's (i) assert that the instruction occurs in a function with a signature that has an ABI we control (let's say just
tail) and then (ii) in the doc-comment on theTailarm of theCallConvenum, alongside the comments defining which registers are used for exception payload, let's add a note about registers used for dead-load-with-context payload.I really don't like the yet-another-kind-of-metadata on
MachBuffer(mmu_interrupt) -- again it's a weird Wasmtime-specific thing, and adds complexity and maintenance burden. Is there a reason we can't attach a trap code to the load, like we do other loads, and distinguish this mechanism based on that? That also avoids the need to add a whole new metadata section to the compiled artifact.At least from my point of view, I think we should get an implementation on another architecture (let's say aarch64) as well; this shows generality, and we have quite a few aarch64 users too. I think we should also probably have perf results showing that the interruption behavior is not catastrophic (due to TLB shootdowns as you mention): the result of that experiment could change the mechanism (use an indirect approach instead), and/or could indicate that the whole thing is not worthwhile.
cfallin edited a comment on PR #12990:
I'm happy to look at the Cranelift side of this. (And Alex should definitely look at the runtime side...)
On first skim, I think the overall shape of the "dead load" seems fine, but a few style and design comments:
It'd be great not to refer to bits of Wasmtime in the Cranelift instruction definition. Cranelift should stand on its own;
dead_load_with_contextcan have a description that maybe says something like "Useful when implementing virtual memory-triggered interrupts, with context in a fixed register available to the interrupt handler."I also see a TODO and a question in the description; we should resolve these.
The register choices are more or less a function of ABI, so let's (i) assert that the instruction occurs in a function with a signature that has an ABI we control (let's say just
tail) and then (ii) in the doc-comment on theTailarm of theCallConvenum, alongside the comments defining which registers are used for exception payload, let's add a note about registers used for dead-load-with-context payload.I really don't like the yet-another-kind-of-metadata on
MachBuffer(mmu_interrupt) -- again it's a weird Wasmtime-specific thing, and adds complexity and maintenance burden. Is there a reason we can't attach a trap code to the load, like we do other loads, and distinguish this mechanism based on that? That also avoids the need to add a whole new metadata section to the compiled artifact.At least from my point of view, I think we should get an implementation on another architecture (let's say aarch64) as well; this shows generality, and we have quite a few aarch64 users too. It also shows that the mechanism works on an ISA with another style of call linkage (namely, the link register rather than the stack).
I think we should also probably have perf results showing that the interruption behavior is not catastrophic (due to TLB shootdowns as you mention): the result of that experiment could change the mechanism (use an indirect approach instead), and/or could indicate that the whole thing is not worthwhile.
erikrose updated PR #12990.
:memo: saulecabrera submitted PR review.
:speech_balloon: saulecabrera created PR review comment:
I think this approach has the added advantage that it checks for async support, which my previous comment states, but does not check at the engine level.
:speech_balloon: saulecabrera created PR review comment:
I just realized that you had introduced
has_mmu_interruption, I believe this can be replaced withif !cfg!(has_mmu_interruption) { ... }This totally my fault for not digging deeper here.
With this change, I _think_ we can get rid of the
host.architecturecheck.
erikrose commented on PR #12990:
Is there a reason we can't attach a trap code to the load, like we do other loads, and distinguish this mechanism based on that?
@cfallin Oh, sorry; I misinterpreted your "we can add to the module metadata, produced alongside e.g. trap codes" to mean a new piece of metadata was needed. However, it may yet be so: it turned out I needed not only to flag the interruption-check instruction offsets but also their lengths so I could compute where to resume after the interruption. The x64
movinstructions can be either 3 bytes or 4, depending on the source register—R12 (and RSP) make it 4.So I see a couple of possibilities:
- Keep as is.
- Switch to using trap codes. Just note that I'd have to eat 2 separate ones: one signifying "3 bytes long" and another "4 bytes long".
- Maybe there's something I can say to regalloc to assert I don't want my source reg to be R12, thus sparing one of the trap codes. I'm not sure how precious trap codes are. My intuition is that they're less precious than regalloc degrees of freedom.
What's your favorite? Numbers 2 and 3 have the advantage of glomming onto the LEB and delta-compression fanciness of trap codes, which I wasn't aware of until just now. My leaning is toward 2.
:speech_balloon: erikrose created PR review comment:
Nope, I introduced that after your commit! But thanks for noticing; I'll have a look.
:memo: erikrose submitted PR review.
erikrose edited a comment on PR #12990:
Is there a reason we can't attach a trap code to the load, like we do other loads, and distinguish this mechanism based on that?
@cfallin Oh, sorry; I misinterpreted your "we can add to the module metadata, produced alongside e.g. trap codes" to mean a new piece of metadata was needed. However, it may yet be so: it turned out I needed not only to flag the interruption-check instruction offsets but also their lengths so I could compute where to resume after the interruption. The x64
movinstructions can be either 3 bytes or 4, depending on the source register—R12 (and RSP) make it 4.So I see a few possibilities:
- Keep as is.
- Switch to using trap codes. Just note that I'd have to eat 2 separate ones: one signifying "3 bytes long" and another "4 bytes long".
- Maybe there's something I can say to regalloc to assert I don't want my source reg to be R12, thus sparing one of the trap codes. I'm not sure how precious trap codes are. My intuition is that they're less precious than regalloc degrees of freedom.
What's your favorite? Numbers 2 and 3 have the advantage of glomming onto the LEB and delta-compression fanciness of trap codes, which I wasn't aware of until just now. My leaning is toward 2.
erikrose edited a comment on PR #12990:
Is there a reason we can't attach a trap code to the load, like we do other loads, and distinguish this mechanism based on that?
@cfallin Oh, sorry; I misinterpreted your "we can add to the module metadata, produced alongside e.g. trap codes" to mean a new piece of metadata was needed. However, it may yet be so: it turned out I needed not only to flag the interruption-check instruction offsets but also their lengths so I could compute where to resume after the interruption. The x64
movinstructions can be either 3 bytes or 4, depending on the source register—R12 (and RSP) make it 4.So I see a few possibilities:
- Keep as is.
- Switch to using trap codes. Just note that I'd have to eat 2 separate ones: one signifying "3 bytes long" and another "4 bytes long".
- Maybe there's something I can say to regalloc to assert I don't want my source reg to be R12, thus sparing one of the trap codes. I'm not sure how precious trap codes are. My intuition is that they're less precious than regalloc degrees of freedom. [Ed: Looks like we have only 3 of them so far and pack them in a u8, so likely not precious at all.]
What's your favorite? Numbers 2 and 3 have the advantage of glomming onto the LEB and delta-compression fanciness of trap codes, which I wasn't aware of until just now. My leaning is toward 2.
cfallin commented on PR #12990:
I don't like any of those options actually -- and it gets at my general concern with the way that this change conflates abstractions from a number of logically-separate places in a way that makes things really brittle. The proposed solutions are (i) x64-specific, and (ii) combine distributed knowledge from instruction emission and regalloc into an invariant encoded in the trap table and then used by the signal handler (!!). The alternative is to have a very specific and weird "MMU interruption" concept in Cranelift. A compiler should not know what MMU interruption is; it understands loads, stores, and instruction metadata.
Let me ask more deeply: why do we need to know about the instruction encoding at all, to advance past the instruction? Why can't we resume into the guest and let it redo the load, with the page mapped back in? Unless we burn a virtual address on every interruption and never reuse it, it will have to be mapped back in eventually, so I don't think that's an issue?
If we do need to resume past the load, then we should not bake assumptions about possible lengths into the compiler. So "one trap code for 3 bytes, one for 4 bytes" is a bad idea: what happens when we get a new architecture that requires 5 bytes? Or add an optimization to the x64 backend to encode it in 2 bytes? Or ...
Instead what you're really trying to build is the concept of resumable traps. So in the trap table, we have a kind of entry that indicates "resumable" and gives the PC-offset to correct the captured PC by. One could encode that with a sparse array alongside trap codes. That might end up looking something like your current custom section, but named in a much less confusing and specific way, and reusable for any other purpose that requires resumable traps in the future (e.g. hardware debug-break opcodes, or missing-instruction emulation, or ...).
So: I prefer that we don't build a distributed, brittle invariant that is ISA-specific like this; but if we have to, let's reify the concept as a more fundamental thing.
alexcrichton commented on PR #12990:
Personally I think it would be best to prioritize performance numbers for this. I agree with everything @cfallin is saying review-wise, but I think it would be best to get on the same page about the overall shape of this feature first as well since that will inform the implementation which will have knock-on effects on what will be reviewed. Of the two possibilities here -- flipping virtual memory permissions vs having an indirect load that is dynamically switched to null -- there's still quite a lot of shared implementation/mechanisms between the two, but I also feel they're different enough we should try to settle on one before landing.
I personally have a strong hunch that what's implemented here, flipping virtual memory permissions, will be a significant performance regression over today's implementation of epochs, specifically because there are more TLB shootdowns (repeatedly unmapping previously-mapped pages). I understand that getting performance numbers on this isn't easy, and I also understand that it would be easiest to land everything here, gated, and then get performance numbers. This is a significant change, however, and I believe we're going to want more confidence before landing it, even gated.
The performance numbers that I'm specifically interested in is the performance of a multi-threaded program with wasm execution in a lot of threads. I'd like to see the performance effect of flipping pages being accessible/inaccessible for the running wasms. This would end-to-end exercise the Cranelift bits here, how the page flipping is implemented, how the list of pages to flip are managed, etc. In-repo the closest equivalent to this is benchmarking
wasmtime serveunder heavy load. While that's not a perfect benchmark it would at least give me personally more confidence in this change one way or another.One thing I'll also explicitly say is that I'm specifically not too interested in the single-threaded overhead of epochs nor the impact of virtual mapping changes in a single-threaded program. I understand some benchmarks were done, but Sightglass does not exercise anything related to TLB shootdowns so it's the wrong benchmark corpus for what I'm interested in. While it's good to provie that this has lower overhead than epochs that's also pretty naturally expected given the reduction in the size of the generated code. The specific concerns I have lie in the TLB shootdown behavior, which surface only in a multithreaded environment.
erikrose commented on PR #12990:
@cfallin First, thank you for bringing your wide and lengthy wasmtime perspective to bear on this!
Why can't we resume into the guest and let it redo the load, with the page mapped back in?
We could, if we don't mind making resumption 1 instruction more expensive. It's the cold path, but I'd be lying to claim knowledge of how significant this would be. Actually, it should be pretty easy to get performance numbers—decent ones, if I make
wasmtime serveMMU-interrupt-savvy like Alex suggests above. If it's much <1%, I'm inclined to just eat it and enjoy the simplicity.There remains, however, a pathological case which troubles me:
- The load is hit.
- The interrupt happens.
- The fiber-yielding routine runs and unprotects the page.
- The interrupter thread (whose existence I'm positing in an embedder) re-protects the page.
- The wasm code resumes, redoing the load and not making any progress.
This loop could happen indefinitely, hopefully unlikely but conceivably not, depending on how many fibers are competing, their scheduling algorithm, and how quickly the interrupter thread (in which I envision some adjustable sleep interval) spins. The epoch-deadline approach doesn't have this problem; it always makes forward progress.
So "one trap code for 3 bytes, one for 4 bytes" is a bad idea: what happens when we get a new architecture that requires 5 bytes? Or add an optimization to the x64 backend to encode it in 2 bytes? Or
That's only if we want to represent MMU interrupt locations as trap codes, of course. Otherwise, we have complete freedom to express instruction length in any kind of forward-compatible, ISA-agnostic way. Your suggestion to get aarch64 going before merging has the advantage of ferreting out these sorts of ISA-specific assumptions. So chalk up one in its column.
One could encode that with a sparse array alongside trap codes. That might end up looking something like your current custom section, but named in a much less confusing and specific way…
I appreciate your reimagining of this as a future-looking resumable-traps mechanism, and I'm intrigued. To clarify, are you proposing to add the sparse array of lengths to the trap section or to bail out into a separate section after all to avoid further complicating traps?
How do you and @alexcrichton feel about this course of action?
- Make
wasmtime serveoptionally use the MMU interrupt mechanism likewasmtime rundoes. (I punted on this initially because it's going to be much harder thanrunwas.)- Measure perf on that, as heavy as I can load it.
- If shootdowns consume the whole performance gain, try the additional-indirection approach.
- If they don't, bench again with the repeated-load approach if we can assuage my pathological no-forward-progress concern above.
(In this plan, I'm leaving a few of your earlier comments on the table until we have an idea of multithreaded numbers.)
alexcrichton commented on PR #12990:
That sounds good to me. Personally I wouldn't be too concerned about live-locking of sorts with resuming and always faulting at the load-that-traps. I also don't think that the performance of executing a load twice will be noticable at all. Which is to say, I would agree with @cfallin that the initial state here is to run the load again. In the absolute worst case if live-locking is a problem in practice I'd prefer to go the route of rewriting the address the dead-load instruction loads from to be known-valid during resumption since that's easier to do cross-platform than skipping instructions.
cfallin commented on PR #12990:
We could, if we don't mind making resumption 1 instruction more expensive. It's the cold path, but I'd be lying to claim knowledge of how significant this would be. Actually, it should be pretty easy to get performance numbers—decent ones, if I make wasmtime serve MMU-interrupt-savvy like Alex suggests above. If it's much <1%, I'm inclined to just eat it and enjoy the simplicity.
Stated this way I think it's pretty clear that it should be negligible: one more machine instruction, on the cold path, after a resume, which happens once a millisecond or so, is no big deal to any computer from the past 40 years (order-of-magnitude: modern core runs ~1B insts/second; a millisecond is a million instructions).
There remains, however, a pathological case which troubles me:
This only happens if interruption occurs so frequently that the guest cannot even be scheduled back onto the CPU before the next interruption. Arguably, the mechanism is even working correctly in this case: the interrupter has asserted the interrupt signal again, so we yield again. In other words, it's a faithful mechanism to the purpose. It's up to the overall system not to interrupt too frequently.
Given all this, I feel pretty strongly that let's not try to do any instruction-skipping magic; hence, no extra metadata sections at all.
Strong +1 to: let's get data from a full multithreaded benchmark with frequent interrupts (wasmtime serving wasi-http should be fine, with a concurrent load generator) to see how the TLB shootdowns impact perf.
Last updated: Jul 29 2026 at 05:03 UTC