Turbopack: omit Exit+Enter trace row pairs with (almost) no gap (#99780)
## Summary
When a span is exited and then entered again on the same thread almost
immediately, the writer drops both rows. Async spans do this on every
poll. It cuts the number of Enter/Exit rows almost in half.
A pair is only omitted when all of these hold:
- same span, same thread, and the `Exit` is still the last row in the
thread-local buffer (not sent to the writer thread yet)
- nothing was (de)allocated between the Exit and the Enter (otherwise
the Enter would need to report it for whatever ran in between)
- the gap is **≤ 1µs**, or **≤ 0.1%** of the time between the last
`Enter` row written on this thread (of any span; omitted `Enter` rows
don't count) and the Exit. For nested spans the last `Enter` can be a
nested span's, which only shrinks the allowance. Memory samples are
still taken when the `Enter` row is omitted.
Allocations reported with the dropped Exit aren't lost. The baseline is
rolled back, so the next row of the thread reports them, while the span
is on top of the stack again.
The viewer then shows the span as entered during the gap. Its self time
can be slightly too large, by at most 1µs or 0.1% per gap. This is
documented in `contributing/turbopack/tracing.md`.
Implementation:
- `TraceWriter`'s `WriteGuard` can mark exactly what a callback writes
(`mark(marker, |guard| ...)`) and remove that row again with
`remove_last_row(marker)`, as long as it still ends the thread local
buffer. Any other write, or sending the buffer, invalidates it. `Exit`
rows are marked with the span id.
- `RawTraceLayer` keeps per-thread state (last Enter timestamp, last
exit, allocation baseline). It is reset when the thread id changes. The
allowed gap is only computed in `on_enter`, after the cheaper checks
pass. Allocations made by this bookkeeping are excluded from the
counters.
- The clock and the allocation counters can be injected in tests.
Stack (on top of #99765):
1. #99777: `no-memory` flag
2. #99778: Merge allocation counters into Enter/Exit rows
3. #99779: Delta-encode allocation counters, skip unchanged ones
4. #99780: **This PR**: omit Exit+Enter pairs with (almost) no gap
5. #99781: Bump the trace header to `TRACEv1`
## Measurements
`next build` of `bench/heavy-npm-deps`, release native binding built
from source:
| | trace size | Enter rows |
| --- | --- | --- |
| PR 3 | 31.6 MiB | 1,138,850 |
| same-timestamp pairs only | 27.2 MiB | 875,258 |
| this PR (≤ 1µs or ≤ 0.1%) | 22.9 MiB | 626,956 |
A dev build of the binding has almost no same-µs pairs (it's slower), so
measure this with a release build.
## Verification
- Unit tests for each condition: same/different span, thread and
timestamp; allocations in between; an intervening row; a sent buffer;
thread slot reuse; the 1µs fast path; exactly at and just over the 0.1%
bound; chained omissions; allocation totals preserved
- `TraceWriter` tests for marking and removing rows
- Captured a trace with a locally built release binding and loaded it in
the trace server
<!-- NEXT_JS_LLM -->
<!-- fleet 4a52842b-f1a2-43cb-9779-3655caada6a0 -->
---------
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>