perf: remove Linux unwind tables from release artifacts (#36265)
## Summary
- rebuild `std` and the Linux release crates with frame pointers, while
disabling newly generated unwind tables
- use the additive frame-pointer panic-trace feature from
`denoland/panic` for Linux x86_64 and aarch64 release builds
- remove `.eh_frame` and `.eh_frame_hdr` from `deno`, `denort`, and
`libdenort.so`, and fail packaging if either a section or
`PT_GNU_EH_FRAME` remains
- preserve the existing `denort --strip-debug` behavior required by
libsui relocation rewriting
No rusty_v8 change is required: its Linux build already preserves frame
pointers, and an end-to-end panic trace crossed Rust std, Deno, V8 C++,
and back into Rust/Tokio after the unwind sections were removed.
## Size impact
The `deno` row is a paired x86_64 Linux build from the same source
revision. The `denort` and `libdenort.so` rows are exact section-removal
projections from the official v2.9.1 artifacts; their eventual CI sizes
may differ slightly because this change also affects code generation by
forcing frame pointers.
| artifact | measurement | before | after | reduction |
| --- | --- | ---: | ---: | ---: |
| `deno` | raw | 114,490,288 | 104,278,808 | 10,211,480 bytes (9.74 MiB,
8.92%) |
| `deno` | zip | 45,459,265 | 42,248,545 | 3,210,720 bytes (3.06 MiB) |
| `denort` | raw projection | 108,341,376 | 101,910,464 | 6,430,912
bytes (6.13 MiB) |
| `denort` | zip projection | 36,527,614 | 34,509,638 | 2,017,976 bytes
(1.92 MiB) |
| `libdenort.so` | raw projection | 85,556,984 | 79,089,248 | 6,467,736
bytes (6.17 MiB) |
| `libdenort.so` | zip projection | 33,358,112 | 31,323,429 | 2,034,683
bytes (1.94 MiB) |
In the paired `deno` baseline, `.eh_frame_hdr` was 1,342,324 bytes and
`.eh_frame` was 8,659,384 bytes. Rebuilding Rust std and dependencies
with unwind tables disabled reduced `.eh_frame` to 3,184,596 bytes;
final section removal accounts for the remainder. The small
`.gcc_except_table` section is retained.
## Tradeoffs
- `-Zbuild-std` is an unstable Cargo interface. This uses
`RUSTC_BOOTSTRAP=1` only in the two Linux release jobs, retaining Deno's
pinned stable toolchain instead of adding a separately pinned nightly
release toolchain. It is still an unsupported escape hatch whose
behavior can change with Rust/Cargo updates.
- Rebuilding `core`, `alloc`, `std`, `proc_macro`, and `panic_abort`
adds release build time, cache entries, and disk pressure. The separate
`denort_desktop` invocation uses the same build-std configuration and
should reuse those artifacts.
- Frame pointers can modestly affect code size and generated-code
performance, particularly on x86_64 where a general-purpose register
becomes unavailable. A paired 150-run startup smoke test showed no
regression (30.097 ms baseline median versus 28.791 ms new median), but
this is not a comprehensive performance result.
- Stack walking now depends on an intact frame-pointer chain. Rust std
is rebuilt, Rust crates are forced to retain frame pointers, and V8
already retains them, but another native library built without frame
pointers can truncate a trace. The walker stops safely when it cannot
read or validate the next frame.
- Removing DWARF CFI means profilers, crash reporters, and other
external tools that require CFI cannot unwind the packaged images;
frame-pointer-aware tools can. This also assumes no linked native
component needs CFI for runtime language-exception unwinding. Rust uses
`panic=abort` here and V8 is built without C++ exceptions, but this
remains a compatibility boundary for future native dependencies,
especially in `libdenort.so`.
- Symcache generation still happens before stripping. Deno's built-in
panic report uses the frame-pointer walker; `denort` and `libdenort.so`
do not gain the same built-in panic URL, though external frame-pointer
walking remains possible.
- `denort` and `libdenort.so` lose their remaining backtrace path, not
just the panic URL. `deno_panic` is wired into the `deno` crate only
(`cli/lib.rs`); `denort` installs no panic hook and `libdenort.so`'s
hook reports the panic message and `panic_info.location()` without
walking the stack. Previously `strip --strip-debug ./denort` kept both
`.eh_frame` and `.symtab`, so `RUST_BACKTRACE=1` on a `deno compile`
binary produced a symbolized backtrace; with the unwind sections removed
libunwind has nothing to walk, and the default Rust hook's `note: run
with RUST_BACKTRACE=1 …` line is now a dead end on Linux. This is
accepted deliberately: we have no panic reports originating from
compiled binaries, so the ~6 MiB shrink on every one of them is worth
more than a backtrace path nobody is using. Restoring it later means
adding a frame-pointer panic hook to `denort` plus per-binary symcaches
— `tools/release/create_symcache.ts` only symbolicates
`Deno.execPath()`, and panic.deno.com resolves by version and target
alone
(`https://dl.deno.land/${type}/${version}/deno-${target}.symcache`), so
a `denort` trace URL would currently be symbolicated against `deno`'s
symcache.
- The `deno_panic` dependency is the published crates.io `0.2.1`
release; the sources are identical to the `frame-pointer` branch commit
`4ce17fe3cc2dc9557e53013620132e997fdda06b` this PR originally pinned.
- Scope is limited to Linux x86_64 and aarch64 release artifacts. Other
build profiles and operating systems retain the existing libunwind path
and unwind metadata.
## Validation
- `./tools/format.js`
- `./tools/lint.js`
- `Cargo.lock` resolves `deno_panic` 0.2.1 from crates.io; its sources
are byte-identical to the branch commit previously pinned, and the
`frame-pointer` cfg gating in `cli/lib.rs` matches the crate's own
gating for `trace_frame_pointer`
- `specs::test::recursive_permissions_pledge` — its `err.out` pinned the
old panic-banner wording, which the Linux release build no longer emits;
the sentence is now wildcarded and both wordings were checked against
`wildcard_match_detailed`
- full x86_64 Linux Deno release build with rebuilt std, forced frame
pointers, and disabled unwind tables
- native aarch64 Linux frame-pointer walker tests after stripping unwind
sections
- stripped Deno panic through a N-API fixture produced a 28-frame trace
that symbolicated through Deno's panic hook, Rust std, the N-API call
site, V8, `JsRuntime`, `MainWorker`, Tokio, and `main`
- stripped `denort` compiled and ran a standalone program
(`denort-fp-ok`)
- stripped `libdenort.so` retained all 611 exports, loaded with
`dlopen`, and was embedded successfully by the raw desktop backend; the
embedded copy also loaded and contained no unwind sections
- `readelf` checks confirm that `.eh_frame`, `.eh_frame_hdr`, and
`PT_GNU_EH_FRAME` are absent from the packaged Linux artifacts