A political conference and discussion platform, in Rust and Dioxus
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167#!/usr/bin/env nu# Split the debug info out of the built wasm.## The bundle is built WITH DWARF line tables so a crash can be traced back to a# source line. Shipping those sections would add ~20 MB to every visit, so this# writes two files instead:## public/assets/wiki_bg-<hash>.wasm stripped — the one that ships# public/symbols/<hash>.debug.wasm the same module, DWARF intact## Stripping only removes custom sections, so the code section keeps its offset# and its function indices. That is what makes the split safe: a stack frame# means the same thing in both files, so the symbols resolve against exactly the# binary the reader was running.## NOTHING MAY REWRITE THE MODULE AFTER THIS POINT. A `wasm-opt -Oz` pass used to# run here, on the shipped copy only. It inlines, merges and drops functions and# prunes unused imports, so the pair silently stopped corresponding (12148 vs# 10707 functions, 425 vs 416 imports) and every report came back resolved to a# real source line in an unrelated crate. It was removed rather than reordered:# binaryen aborts on a module that still holds DWARF, even with `-g`. It cost# 9 KB gzipped (0.5% of what a reader downloads), which is not worth a crash# report that lies. `assert_pair` below fails the build if it happens again.## The pair is keyed by dx's own content hash, already in the filename and already# in the wasm URL the browser reports, so no build-id section is needed. The# backend fetches /symbols/<hash>.debug.wasm when a report arrives; see# backend/src/symbolicate.rs.
const PUBLIC = "target/dx/wiki/release/web/public"
# How many builds' worth of symbols to keep.## dx never removes a superseded asset, so without this the directory gains ~26 MB# per build forever and every deploy uploads the lot.## Six, not three. Three assumed a reader reloads within a deploy or two, and a# day of thirteen deploys disproved it: a crash arrived from a tab whose build# was four deploys old, its symbols already pruned, and came back as raw offsets.# What bounds this is how long a tab stays open, not how often we deploy. Six# costs about 160 MB on the site and roughly a minute of upload.## Losing an older one is not a failure: the backend fetches the sidecar, gets the# site's SPA fallback (HTML, not a wasm module) instead, recognises it by the# missing magic bytes and leaves the raw offsets alone. Unresolved beats wrong,# which is why an older build is never resolved against a newer one's symbols —# the offsets would land in whatever function now occupies that address.const KEEP = 6
# Drop all but the newest KEEP sidecars, never the one this build just produced.## What it removes is printed. A prune that stayed quiet would read as "everything# is still there", which is the one thing it is not.def prune_symbols [symbols_dir: string, current: string] { let sidecars = ( ls $symbols_dir | where name =~ '\.debug\.wasm$' | sort-by modified --reverse ) if ($sidecars | length) <= $KEEP { return } for stale in ($sidecars | skip $KEEP | where name != $current) { rm $stale.name print $"pruned ($stale.name) ($stale.size)" }}
# The wasm this build actually serves, followed from index.html.## NOT the newest file matching a glob: dx never removes superseded assets, so# that directory accumulates wasm files from every previous build and the wrong# one would be split — leaving the shipped binary fat and the sidecar useless.def current_wasm [] { let entry = ( open $"($PUBLIC)/index.html" | parse --regex 'wiki-(?<h>[0-9a-z]+)\.js' | get h.0 ) let name = ( open $"($PUBLIC)/assets/wiki-($entry).js" | parse --regex 'wiki_bg-(?<h>[0-9a-z]+)\.wasm' | get h.0 ) { hash: $name, path: $"($PUBLIC)/assets/wiki_bg-($name).wasm" }}
# The imports and code sections as objdump reports them: offsets, sizes, counts.def section_shape [path: string] { ^wasm-tools objdump $path | lines | each {|l| $l | str replace --all --regex '\s+' ' ' | str trim } | where {|l| ($l | str starts-with "code ") or ($l | str starts-with "imports ") } | sort | str join " // "}
# A frame carries a byte offset (Chrome, Firefox) or a function index (Safari).# Both are positions in the code section, and both are meaningless against a# module whose code section moved. Refuse to publish a pair that cannot resolve.def assert_pair [shipped: string, sidecar: string] { let a = (section_shape $shipped) let b = (section_shape $sidecar) if $a == $b { return } print "ABORT: the shipped wasm and its sidecar are different modules." print $" shipped ($a)" print $" sidecar ($b)" print "A reported frame would resolve to whatever function now sits at that" print "address. Something rewrote the module after the split; move it before." exit 1}
def main [] { let target = (current_wasm) if not ($target.path | path exists) { print $"referenced wasm missing: ($target.path)" exit 1 }
let symbols_dir = $"($PUBLIC)/symbols" mkdir $symbols_dir let sidecar = $"($symbols_dir)/($target.hash).debug.wasm"
# This rewrites the build output in place, and dx reuses an unchanged wasm # rather than regenerating it — so a second `just build` finds the binary # already stripped. That is success, not failure, as long as the sidecar from # the first run is still there. Without this check the split would overwrite # a good sidecar with a copy of the stripped binary, quietly destroying the # symbols it exists to keep. let has_debug = (^wasm-tools objdump $target.path | str contains ".debug_") if not $has_debug { if ($sidecar | path exists) { # Check the pair we are keeping, not just the one we just made: a # rebuilt-but-unchanged wasm takes this path every time. assert_pair $target.path $sidecar print $"already split ($sidecar)" prune_symbols $symbols_dir $sidecar return } print "shipped wasm has no debug sections and no sidecar exists —" print "was it built without --debug-symbols?" exit 1 }
cp $target.path $sidecar
# `-d` takes a regex over section names. The DWARF sections are the only ones # worth megabytes; everything else stays, so nothing else about the module # changes. ^wasm-tools strip -d '^\.debug_' $sidecar -o $target.path
assert_pair $target.path $sidecar
let symbols_size = (ls $sidecar | get size.0) let shipped_size = (ls $target.path | get size.0) print $"symbols ($sidecar) ($symbols_size)" print $"shipped ($target.path) ($shipped_size)" if $shipped_size >= $symbols_size { print "WARNING: stripping freed nothing — was the build made without debug info?" exit 1 }
prune_symbols $symbols_dir $sidecar}