mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
integ: merge merge/mesa-cold (Mesa cold split) 3b803040
This commit is contained in:
commit
f3689d12cd
20 files changed
+9890
-2527
No files matched your search
@@ -0,0 +1,519 @@
|
||||
# Mesa's wasm size: what runs, what ships
|
||||
|
||||
Stream M. Branch `merge/mesa-cold`, worktree `wt/mesa-cold`; Mesa changes on
|
||||
the local branch `m/cold` of third_party/mesa (off the wink pin 60202246).
|
||||
|
||||
The page's OpenGL is Mesa built to wasm (`gex-wgpu-replay.wasm`,
|
||||
web/build-mesa-wasm.sh). On master (07f1ed19) it is 20.6 MB with names and
|
||||
18.9 MB as the dists ship it (names stripped): 16.0 MB of code in 18,864
|
||||
functions, 2.8 MB of data, 1.7 MB of names. The goal: cut what a page
|
||||
downloads and compiles, without breaking a workload.
|
||||
|
||||
## 1. Baseline
|
||||
|
||||
The shipped files (web/build-all.sh strips custom sections for the dists):
|
||||
|
||||
| flavour | raw | zstd -19 |
|
||||
| --- | --- | --- |
|
||||
| MP (mt) | 18,898,007 | 3,305,617 |
|
||||
| SP (st) | 18,669,577 | 3,300,261 |
|
||||
|
||||
Code by component (names; mt, master):
|
||||
|
||||
| component | functions | code |
|
||||
| --- | --- | --- |
|
||||
| Mesa core, util, gallium aux (`_mesa_`, `util_`, `save_`, `vbo_`, ...) | 6,207 | 3.25 MB |
|
||||
| Tint (SPIR-V reader, IR, WGSL writer and reader) | 2,966 | 2.67 MB |
|
||||
| GLSL compiler and IR | 658 | 2.16 MB |
|
||||
| other (unprefixed C helpers, lambdas) | 5,691 | 2.14 MB |
|
||||
| gex's wire decoder, `one()` (a single function) | 1 | 1.23 MB |
|
||||
| NIR | 371 | 1.06 MB |
|
||||
| SPIRV-Tools (Tint's input validation) | 1,308 | 0.98 MB |
|
||||
| gex_wgpu driver, emdawnwebgpu | 288 | 0.96 MB |
|
||||
| st/mesa | 773 | 0.78 MB |
|
||||
| libc, libc++ | 433 | 0.42 MB |
|
||||
| spirv_to_nir (vtn) | 168 | 0.37 MB |
|
||||
|
||||
## 2. Measuring what runs
|
||||
|
||||
A profiling build is `GEX_MESA_PROFILE=1 web/build-mesa-wasm.sh` (add
|
||||
`GEX_MESA_THREADS=0` for SP): the release link, then
|
||||
`wasm-split --instrument --in-memory` on its plain module, shipped whole.
|
||||
Each function's entry stores 1 at byte `funcIndex` of linear memory, which
|
||||
is free: the link puts the main stack first (`-Wl,--stack-first`, see
|
||||
section 3), so address 0 up is the far end of a 16 MB stack nothing reaches.
|
||||
The module exports `__write_profile`; the glue (web/gex-wgpu-jspi.js) finds
|
||||
it and, every 2 s while the set grows, prints
|
||||
`[gex-wgpu] mesa-profile N HITS BASE64` (bit i = defined function i) to the
|
||||
console, where every page harness already keeps `gex-wgpu` warnings. Other
|
||||
builds export no such function.
|
||||
|
||||
```
|
||||
GEX_MESA_PROFILE=1 web/build-mesa-wasm.sh
|
||||
# swap target/web-build/mesa-mt/build/src/gallium/targets/gex-wasm/split/gex-wgpu-replay.{js,wasm}
|
||||
# into a page tree, run the workload, keep its console log
|
||||
tools/mesa-hot.py target/web-build/mesa-mt/build/src/gallium/targets/gex-wasm/gex-wgpu-replay.wasm.orig \
|
||||
LOG... --also web/mesa-hot.txt -o web/mesa-hot.txt
|
||||
```
|
||||
|
||||
`--also` keeps what an earlier list held, by name, so a new workload adds
|
||||
to the hot set rather than replacing it. (The M1 numbers below came from an
|
||||
earlier form of this: a link with `-sGLOBAL_BASE=65536` and no ASSERTIONS,
|
||||
which wrote a cookie at address 0.)
|
||||
|
||||
### Hot sets (M1)
|
||||
|
||||
Profiling build of the master pin (18,829 functions, 15.59 MB of code; no
|
||||
ASSERTIONS, so 35 fewer functions than the shipped one). Runs on edger,
|
||||
Chromium, real GPU:
|
||||
|
||||
| workload | how | functions run | their code |
|
||||
| --- | --- | --- | --- |
|
||||
| Muck, to its menu (10 min) | muck-page.sh on a copy of edger's wt/unity | 3,313 (17.6%) | 8.26 MB (53.0%) |
|
||||
| Undertale on W3 (10 min, 2,220 frames) | W's run-utpage.sh, tree D with this Mesa | 2,968 (15.8%) | 7.47 MB (47.9%) |
|
||||
| Hollow Knight, to its menu (12 min, interpreter) | hk-page.sh on the same copy | 3,095 (16.4%) | 7.99 MB (51.2%) |
|
||||
| Noita, menu then 150 s of world (63.2 world fps) | scratch/GL/np/gex-page.sh on a copy of wt/glpath | 3,006 (16.0%) | 7.97 MB (51.1%) |
|
||||
| **union** | | **3,605 (19.1%)** | **8.55 MB (54.8%)** |
|
||||
| margin: direct callees of the union up to 1 KB | tools/mesa-hot.py | +848 | +0.31 MB |
|
||||
|
||||
Four different engines agree closely: the union is only 292 functions
|
||||
more than Muck alone. Copies of edger's trees (`cp -al`, Mesa swapped by
|
||||
`rm` + copy, never through the hard links) kept the other streams' trees
|
||||
untouched.
|
||||
|
||||
Most functions never run, but most of the code bytes do: the big functions
|
||||
all run. The wire decoder's `one()` (1.18 MB), GLSL's builtin-function
|
||||
initialiser (0.55 MB), `_mesa_swizzle_and_convert` (0.44 MB) and
|
||||
`nir_eval_const_opcode` (0.38 MB) are a quarter of the code by themselves,
|
||||
and each runs in every workload.
|
||||
|
||||
The Undertale run used master's Mesa in place of tree D's (W's MSAA patch
|
||||
68c5ed1d); it drew its 2,220 frames, but WebGPU reported a CopyDst usage
|
||||
error that tree D's Mesa does not, and utpage.mjs's frame counter, which
|
||||
stops at the page's first GL failure, stayed at 0.
|
||||
|
||||
Cold code by area (Muck and Undertale union): Mesa core 2.44 MB (display
|
||||
lists `save_*` 0.7 MB, `_hw_select`, ARB programs, format packing for
|
||||
formats not used), Tint 1.50 MB (`ReadIR`, the IR path used only when the
|
||||
AST reader fails, and the WGSL reader), spirv_to_nir 0.35 MB, SPIRV-Tools
|
||||
0.35 MB. None of it is a whole subsystem a workload can do without: display
|
||||
lists, GL_SELECT and ARB programs are GL features some application uses.
|
||||
Hence the split (section 4), not deletion.
|
||||
|
||||
## 3. Trimming the build (M2)
|
||||
|
||||
Sizes are of the module as a dist ships it (names and DWARF stripped by
|
||||
web/build-all.sh), mt unless said; zstd at -19. Mesa CPU ms per frame is
|
||||
the replay's `submit_ms` (scratch/M/edger/replay-run.mjs on edger, Chromium on
|
||||
its GPU, the last 600 frames of Noita's world trace and of HK's menu
|
||||
trace, counters off), two passes in ABBA order; lower is faster.
|
||||
|
||||
### ASSERTIONS=2 out, the main stack first
|
||||
|
||||
ASSERTIONS=2 implies STACK_OVERFLOW_CHECK=2: a call to a limit check at
|
||||
each of the 15,695 places the stack pointer moves, and a cookie at address
|
||||
0; the rest of it is checks in the JS glue. What it caught that matters is a
|
||||
stack overflow (a long Unity run hit one). Mesa runs on the module's main
|
||||
stack in every realm (nothing here uses glthread or another pthread), and
|
||||
the replay now links with `-Wl,--stack-first`: that stack sits at the bottom
|
||||
of memory, so overflowing it wraps below address 0 and the next store traps,
|
||||
as the check did, instead of silently overwriting data -- without a call per
|
||||
function. (A pthread's stack would have no such guard; none runs Mesa.
|
||||
With STACK_FIRST, emcc also stops passing Binaryen `--low-memory-unused`; no
|
||||
measurable cost below.)
|
||||
|
||||
| build | stripped | zstd -19 | Noita world, ms/frame | HK menu, ms/frame |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| master (ASSERTIONS=2) | 18,898,007 | 3,305,617 | 4.885, 4.901 / 5.172, 5.091 | 6.253, 6.456 / 5.122, 5.191 |
|
||||
| ASSERTIONS=0 | 18,444,998 | 3,347,542 | 4.980, 5.123 | 5.354, 5.213 |
|
||||
| ASSERTIONS=0, --stack-first | 18,564,629 | 3,360,944 | 5.046, 4.349 | 6.268, 5.175 |
|
||||
| + decoders kept apart (one-caller inlining off) | 18,655,290 | 3,296,522 | 5.099, 5.140 | 6.461, 5.525 |
|
||||
| + Tint's SPIR-V validation off (**M2 build**) | 18,655,830 | 3,296,846 | 5.114, 5.231 | 5.201, 5.161 |
|
||||
| M2 build at -Os (compile and link) | 13,158,052 | 2,798,299 | see below | see below |
|
||||
|
||||
The CPU runs of master's second pair and the last two rows (t2, which ended
|
||||
at 05:25) overlapped another stream's Chromium left running on edger (05:17
|
||||
to 06:14), and were rerun (t2r):
|
||||
|
||||
| build | Noita world, ms/frame | HK menu, ms/frame |
|
||||
| --- | --- | --- |
|
||||
| master (ASSERTIONS=2) | 5.106, 4.984 | 5.263, 5.232 |
|
||||
| + decoders kept apart | 5.365, 5.071 | 6.249, 6.082 |
|
||||
| M2 build | 5.292, 5.118 | 4.918, 6.527 |
|
||||
|
||||
On this trace the HK runs of every build fall near 5.0 or near 6.2 ms/frame
|
||||
(section 4 has twenty of them), so two runs do not separate these builds;
|
||||
Noita's M2 build is 3.0% slower than master's (5.205 against 5.045), about
|
||||
its run-to-run spread.
|
||||
|
||||
Two runs each, one per ABBA pass; master's second pair is from the second
|
||||
job (each job compares its builds against master in the same session). The
|
||||
spread between passes of one build (up to 1.1 ms on HK) is larger than any
|
||||
difference between builds: none of these steps costs measurable Mesa CPU.
|
||||
SP (st) M2 build: 18,442,251 stripped, 3,294,291 zstd, against master's
|
||||
18,669,577 and 3,300,261.
|
||||
|
||||
### -O3 or -Os, and wasm-opt -Oz
|
||||
|
||||
Compiling and linking all of Mesa at -Os (Tint stays -O3) takes the M2
|
||||
build from 18,655,830 to 13,158,052 bytes (29.5% smaller) and from
|
||||
3,296,846 to 2,798,299 compressed (15.1% smaller). It costs Mesa CPU on HK's
|
||||
menu trace; four ABBA runs each, same job:
|
||||
|
||||
| trace | -O3 (M2 build), ms/frame | -Os, ms/frame |
|
||||
| --- | --- | --- |
|
||||
| Noita world | 5.107, 5.048, 5.050, 5.010 (mean 5.054) | 4.976, 4.993, 4.983, 4.721 (mean 4.918) |
|
||||
| HK menu | 4.712, 4.873, 4.980, 4.733 (mean 4.825) | 6.324, 6.141, 6.248, 5.497 (mean 6.053) |
|
||||
|
||||
-Os is 2.7% faster on Noita and 25.5% slower on HK (1.23 ms more per frame,
|
||||
all four runs), so the build stays -O3. Running only Binaryen at -Oz on the
|
||||
-O3 objects (`-Oz` on the link) saves 364 KB raw (18,444,998 to
|
||||
18,080,546 with ASSERTIONS=0) and 2 KB compressed: not worth a measurement.
|
||||
|
||||
### The wire decoder's `one()`
|
||||
|
||||
`one()` was 1.18 MB because tools/gen_gl_compat.py's 2,520 per-command
|
||||
decoders (web/mesa/gl-compat-generated-decoders.inc) are each called from
|
||||
one `case` of its switch, and Binaryen inlines any function with a single
|
||||
caller whatever its size (`--one-caller-inline-max-function-size`, unlimited
|
||||
by default) -- LLVM's `noinline` does not reach it. The generator now marks
|
||||
them `noinline`, and the replay links with
|
||||
`-sBINARYEN_EXTRA_PASSES=--one-caller-inline-max-function-size=0`: `one()`
|
||||
is 166,537 bytes, the decoders 2,522 functions of 1.04 MB (35 over 1 KB),
|
||||
GLSL's builtin initialiser comes apart the same way (`create_builtins`
|
||||
374,567 and `create_intrinsics`). 91 KB more raw, 64 KB less compressed,
|
||||
and no Mesa CPU cost (table above). The decoders are what the split can now
|
||||
place one by one: a workload runs a few hundred of them.
|
||||
|
||||
### Tint's SPIR-V validation
|
||||
|
||||
Tint's SPIR-V reader ran SPIRV-Tools' validator (`spvtools::ValidateBinary`,
|
||||
363 KB of code that every workload ran) on every module Mesa handed it. Mesa
|
||||
wrote that SPIR-V itself, from NIR it had already validated, so in a release
|
||||
the check only finds Mesa bugs. wink/patches/tint-spirv-reader-skip-validation.patch
|
||||
makes it conditional on `TINT_SPIRV_VALIDATE=1` (read once; the replay takes
|
||||
it through `_gex_gl_setenv`, a page through `GEX_NT_PAGE_ENV`/`?env=`). Mesa
|
||||
CPU over the whole trace, where every shader translation counts, two runs
|
||||
each, ms/frame:
|
||||
|
||||
| trace | run | validation off | validation on (`TINT_SPIRV_VALIDATE=1`) | warm WGSL cache |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| HK menu (3,166 frames, 49 translations) | first (t2b) | 4.956, 4.866 | 5.228, 5.769 | 4.768, 4.781 |
|
||||
| | rerun (t2rb) | 4.924, 5.087 | 4.941, 5.029 | 4.872, 4.741 |
|
||||
| Noita world (2,777 frames, 54 translations) | first (t2b) | 4.330, 4.340 | 4.627, 4.626 | 4.794, 4.867 |
|
||||
| | rerun (t2rb) | 4.675, 4.488 | 4.384, 4.496 | 4.910, 4.367 |
|
||||
|
||||
The first runs overlapped another stream's Chromium left running on edger
|
||||
(05:17 to 06:14), and the rerun does not hold them up: with validation off
|
||||
HK is 0.02 ms/frame slower (5.006 against 4.985 on, 0.4%) and Noita 0.14
|
||||
ms/frame slower (4.582 against 4.440, 3.2%), both inside the spread. Over a whole trace the
|
||||
validator's cost is lost in the noise; what it costs is time inside each
|
||||
translation (49 and 54 of them here), which an average over 3,000 frames
|
||||
dilutes. The switch stays (it costs nothing and keeps a debug check), but it
|
||||
is not a measured CPU win. The code stays linked, so the size does not
|
||||
change.
|
||||
|
||||
Per translation, from V8 CPU profiles of the same whole-trace replays
|
||||
(cp2: 200 us sampling, both orders, time under `translate_here` and under
|
||||
SPIRV-Tools' `spvValidateWithOptions`):
|
||||
|
||||
| trace | run | in translation, ms | of it in the validator, ms |
|
||||
| --- | --- | --- | --- |
|
||||
| HK menu | validation off | 414.3, 461.9 | 73.4, 80.2 |
|
||||
| | validation on | 514.3, 465.4 | 136.5, 124.0 |
|
||||
| Noita world | validation off | 682.9, 640.2 | 102.5, 109.7 |
|
||||
| | validation on | 709.9, 689.3 | 177.6, 170.7 |
|
||||
|
||||
So with the switch off the validator still ran: 17.5% of HK's translation
|
||||
time and 16.0% of Noita's. `spvtools::Optimizer::Run`, which Tint's reader
|
||||
uses right after to split combined image-samplers, validates its input
|
||||
first, the same check again. The patch now turns that one off too
|
||||
(`OptimizerOptions::set_run_validator`, under the same switch), and the
|
||||
validator, 475 functions of the hot list (`spvtools::val::`), moves to the
|
||||
cold part (`tools/mesa-hot.py --drop`). With that build (cp3, same method):
|
||||
|
||||
| trace | run | in translation, ms | of it in the validator, ms | per translation, ms |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| HK menu (49 translations) | validation off | 379.2, 339.6 | 0.0, 0.0 | 7.33 |
|
||||
| | validation on | 514.2, 485.6 | 138.4, 135.8 | 10.20 |
|
||||
| Noita world (54) | validation off | 490.3, 529.3 | 0.0, 0.0 | 9.44 |
|
||||
| | validation on | 725.3, 650.5 | 165.4, 162.8 | 12.74 |
|
||||
|
||||
Off is now 28.1% less translation time on HK (359.4 against 499.9 ms, 2.9
|
||||
ms less per translation) and 25.9% less on Noita (509.8 against 687.9 ms,
|
||||
3.3 ms less per translation): the hitch a mid-frame shader compile costs.
|
||||
Over the whole trace it stays inside the noise (Mesa CPU, ms/frame, off
|
||||
then on: HK 5.041, 5.019 against 5.020, 4.750; Noita 4.452, 4.762 against
|
||||
4.950, 4.428), since translation is under 0.5 s of about 15 s of Mesa CPU.
|
||||
The sampling cannot cut the time
|
||||
into single translations (a translation's samples are not contiguous
|
||||
enough), so there is no per-translation maximum; the largest contiguous
|
||||
run under `translate_here` was 47.25 ms with validation on and 36.47 ms
|
||||
off, on Noita.
|
||||
|
||||
### Tint and SPIRV-Tools behind a warm WGSL cache
|
||||
|
||||
With every translation in the WGSL cache (`_gex_wgsl_cache_load` of the blob
|
||||
a first run dumped; a page gets one from the site's gex-wgsl-cache.bin or
|
||||
IndexedDB), the profiling build shows Tint and SPIRV-Tools do not run at
|
||||
all:
|
||||
|
||||
| trace | run | functions run | Tint | SPIRV-Tools |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| HK menu | cold cache | 3,962 | 1,237 (0.87 MB) | 510 (0.51 MB) |
|
||||
| HK menu | warm cache (49 of 49 hits) | 2,035 | 4 (0 MB) | 0 |
|
||||
| Noita world | cold cache | 3,895 | 1,256 (0.88 MB) | 505 (0.51 MB) |
|
||||
| Noita world | warm cache (54 of 54 hits) | 1,959 | 4 (0 MB) | 1 |
|
||||
|
||||
GLSL, NIR and spirv_to_nir still run (Mesa compiles GLSL to SPIR-V before
|
||||
it looks the WGSL up). Mesa CPU with the warm cache (table above, rerun):
|
||||
HK 4.807 ms/frame against 5.006 cold, 4.0% less; Noita 4.639 against 4.582,
|
||||
1.2% more, inside its spread (4.367 to 4.910). So a shipped cache is a load
|
||||
time and stall win more than an average-CPU one, and the phase split it
|
||||
allows (Tint and SPIRV-Tools, 1.38 MB, to the cold part when a cache ships)
|
||||
is in the backlog.
|
||||
|
||||
## 4. The cold part (M3)
|
||||
|
||||
web/build-mesa-wasm.sh links the replay with `-sSPLIT_MODULE=1` and splits
|
||||
its plain module (`gex-wgpu-replay.wasm.orig`) with web/mesa-split.py:
|
||||
the functions in web/mesa-hot.txt stay in `gex-wgpu-replay.wasm`, the rest
|
||||
go to `gex-wgpu-replay.deferred.wasm` beside it. Every publish step that
|
||||
copies the one copies the other (build-all's dists, build-eject-page.sh, the
|
||||
mesa-wgpu-*/gl44 tests). `GEX_MESA_HOT=FILE` splits by another list,
|
||||
`GEX_MESA_HOT=none` (any missing file) ships it whole.
|
||||
|
||||
### Loading (web/gex-wgpu-split.js, a --pre-js)
|
||||
|
||||
- A miss is one load of the whole cold part per instance of the module,
|
||||
however many cold functions run after it.
|
||||
- In a Worker (the desktop's compositor in MP, the system Worker in SP),
|
||||
the cold part is left until a function there first runs. Once the module
|
||||
has presented a frame, the Worker fetches and compiles it in the
|
||||
background (`fetch` + `WebAssembly.compile`); a miss after that only
|
||||
instantiates it. A miss before then fetches it with a synchronous XHR and
|
||||
compiles it synchronously, which a Worker may do at any size.
|
||||
- On a page's main thread (demos.js' gears in MP, the replay and smoke test
|
||||
pages) Chromium refuses a synchronous compile or instantiation of a module
|
||||
over 8 MB, so there the cold part is instantiated with the module,
|
||||
asynchronously, before it starts, and never missed. (`gexColdLazy: true`
|
||||
in the module's options keeps it lazy, for measuring a miss.)
|
||||
|
||||
Two things had to change in what wasm-split and Emscripten's glue do:
|
||||
|
||||
- Features. The module has no target_features section, and with `-all`
|
||||
Binaryen 130 writes the cold part's imports as exact function imports
|
||||
(import kind 0x20), which Chromium refuses ("Invalid import kind 32").
|
||||
With reference types it also puts the placeholders in a second table,
|
||||
while Emscripten's glue (6.0.1) calls the loaded function through the
|
||||
first, `wasmTable.get(index)`: some other function. mesa-split.py passes
|
||||
the link's own feature set.
|
||||
- JSPI. Emscripten's placeholders are JS functions that load the cold part
|
||||
and then call the function from JS. The first call into a cold function
|
||||
that then waits on WebGPU (glReadPixels mapping a buffer) failed: "SuspendError:
|
||||
trying to suspend JS frames". mesa-split.py replaces each placeholder with
|
||||
a wasm trampoline (generated, merged in with wasm-merge) that calls the
|
||||
one import `gexcold.load` and then calls through its own table slot, by
|
||||
then the function itself, so no JS frame is left on the stack.
|
||||
gex-wgpu-split.js supplies `gexcold.load` through Emscripten's
|
||||
`instantiateWasm` hook.
|
||||
|
||||
Nothing in gex's Mesa runs on a pthread (no glthread), so one instance per
|
||||
page or Worker is all there is; a pthread would load its own the same way.
|
||||
|
||||
### The hot list
|
||||
|
||||
web/mesa-hot.txt is made from the profiling build of this branch's M2 build
|
||||
(24,721 functions, 15.79 MB of code; `GEX_MESA_PROFILE=1`), edger,
|
||||
Chromium on its GPU, unless said:
|
||||
|
||||
| workload | functions run | their code |
|
||||
| --- | --- | --- |
|
||||
| Muck page, to its menu (10 min) | 4,219 | 4.91 MB |
|
||||
| HK page, to its menu (12 min, interpreter) | 3,954 | 4.65 MB |
|
||||
| Noita page, menu then 150 s of world | 3,833 | 4.62 MB |
|
||||
| Undertale page on W3 (10 min) | 3,750 | 4.51 MB |
|
||||
| browser-gui x86 --gl (Wine's glwindow.exe; gooner) | 2,364 | 2.63 MB |
|
||||
| gears trace replay (gooner) | 3,243 | 4.85 MB |
|
||||
| HK menu trace replay (3,166 frames) | 3,962 | 5.45 MB |
|
||||
| Noita world trace replay (2,777 frames) | 3,895 | 5.46 MB |
|
||||
| Muck menu trace replay (3,000 frames) | 4,479 | 5.95 MB |
|
||||
| **union** | **4,865 (19.7%)** | **6.22 MB (39.4%)** |
|
||||
| margin: direct callees up to 1 KB, of callers of up to 64 | +1,047 | +0.34 MB |
|
||||
| first list | 5,912 (23.9%) | 6.56 MB (41.5%) |
|
||||
| HK and Muck pages again, printed in full (4,036 and 4,303 run) | +1 (`_dispatch_stub_GetObjectLabelEXT`) | |
|
||||
| margin over the new union | +229 | +0.07 MB |
|
||||
| second list | 6,143 (24.8%) | 6.63 MB (42.0%) |
|
||||
| SPIR-V validator off in both places: `--drop 'spvtools::val::'` and its entry points | -475 | -0.51 MB |
|
||||
| margin over the new union | +64 | +0.02 MB |
|
||||
| names matched without Binaryen's `_N` suffix (twins of listed functions, and three the Tint rebuild renamed) | +86 | +0.05 MB |
|
||||
| final build profiled again (trace replays with no WGSL cache, gears, Muck and HK pages): nothing outside the list; margin over it | +20 | +0.01 MB |
|
||||
| **kept** | **5,844 (23.6%)** | **6.20 MB (39.2%)** |
|
||||
|
||||
The page harnesses cut console lines at about 4,000 characters, and a set
|
||||
of 24,721 functions is 4,120 in base64, so the first page rows above miss
|
||||
the last 937 functions (0.98 MB: dispatch stubs, part of Tint's IR); the
|
||||
trace replays of the same games, which print in full, covered all but one:
|
||||
the HK and Muck pages, run again with the profile printed in pieces, each
|
||||
ran exactly one function outside the first list, the GL_EXT_debug_label
|
||||
dispatch stub, which was each page's one miss. The glue now
|
||||
prints the set in 2 KiB pieces (`mesa-profile N HITS @OFFSET BASE64`), and
|
||||
tools/mesa-hot.py reads both forms (and as much of a cut line as there is).
|
||||
|
||||
The margin leaves out the callees of a dispatcher (a caller of more than 64
|
||||
functions): the wire decoder `one()` calls all 2,522 per-command decoders,
|
||||
which would otherwise all be kept.
|
||||
|
||||
### Sizes (M3)
|
||||
|
||||
As the dists ship them (stripped), zstd at -19:
|
||||
|
||||
| flavour | file | raw | zstd -19 |
|
||||
| --- | --- | --- | --- |
|
||||
| MP | master, whole | 18,898,007 | 3,305,617 |
|
||||
| MP | M3: gex-wgpu-replay.wasm | 9,593,437 | 1,929,678 |
|
||||
| MP | M3: gex-wgpu-replay.deferred.wasm | 9,953,763 | 1,586,817 |
|
||||
| SP | master, whole | 18,669,577 | 3,300,261 |
|
||||
| SP | M3: gex-wgpu-replay.wasm | 9,392,394 | 1,929,097 |
|
||||
| SP | M3: gex-wgpu-replay.deferred.wasm | 9,948,024 | 1,580,830 |
|
||||
|
||||
What a page fetches and compiles before Mesa starts is 49.2% smaller raw
|
||||
than master's (9.59 against 18.90 MB) and 41.6% smaller compressed (1.93
|
||||
against 3.31 MB) in MP; 49.7% and 41.5% in SP. The two parts together are
|
||||
0.89 MB more raw than the unsplit M2 build (19,547,200 against 18,655,830):
|
||||
the 17,173 trampolines and the exports the cold part imports through.
|
||||
(With the second list, before the validator went cold: MP 10,031,571 /
|
||||
2,032,243 and 9,503,551 / 1,493,059.)
|
||||
Binaryen at -Oz on the cold part alone takes it to 8,640,842 bytes but
|
||||
makes it 15 KB bigger compressed, so it is left at -O3.
|
||||
|
||||
### Speed and misses (M3, edger)
|
||||
|
||||
The final list (6,143 functions), pages against master's Mesa, ABBA
|
||||
(j7), last-minute presents/s for each of the last four minutes:
|
||||
|
||||
| page | run | build | presents/s | cold part |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Muck | 1 | master's | 11.1 11.0 10.9 10.4 | none |
|
||||
| | 2 | split | 11.0 11.4 11.4 10.8 | prefetched (62.4 ms), never missed |
|
||||
| | 3 | split | 10.9 11.1 11.1 10.5 | prefetched (113.0 ms), never missed |
|
||||
| | 4 | master's | 11.0 11.0 10.9 10.6 | none |
|
||||
| HK | 1 | master's | 14.2 14.4 14.3 14.5 | none |
|
||||
| | 2 | split | (42.3: HK went into a NullReferenceException loop in Unity's TickRealtimeProbes; excluded) | prefetched (90.7 ms), never missed |
|
||||
| | 3 | split | 14.7 14.7 14.6 14.6 | prefetched (121.3 ms), never missed |
|
||||
| | 4 | master's | 13.6 13.5 13.7 13.9 | none |
|
||||
| HK (j8, for run 2) | 5 | split | 14.2 14.5 14.7 15.1 | prefetched (115.4 ms), never missed |
|
||||
| | 6 | master's | 13.4 13.7 13.9 13.8 | none |
|
||||
|
||||
Muck's split is 1.5% faster (11.03 against 10.86). HK's split is 5.2%
|
||||
faster (14.64 against 13.91, two runs against three), about the spread
|
||||
of master's own runs (14.35 to 13.68). HK run 2's split never missed, so
|
||||
no cold function ran in it and the Mesa code it ran was the same as in run
|
||||
3: the exception loop is HK's, not the split's. With the final list
|
||||
neither page missed in any run; the prefetch, about 0.1 s of fetch and
|
||||
compile in the Worker after the first frame, was all the cold part cost.
|
||||
|
||||
The runs below are of the first list (5,912 functions), with which each
|
||||
page missed once (`_dispatch_stub_GetObjectLabelEXT`).
|
||||
|
||||
Mesa CPU per frame, by trace replay (the last 600 frames, page main thread,
|
||||
so the cold part is loaded with the module), ms/frame, runs in alternating
|
||||
order:
|
||||
|
||||
| trace | unsplit (M2 build) | split | split, every function kept |
|
||||
| --- | --- | --- | --- |
|
||||
| Noita world | 5.144 5.102 5.207 5.066 | 4.884 4.955 4.905 4.796 | 4.906 4.978 |
|
||||
| HK menu | 4.857 5.078 6.5 5.066 5.088 5.049 4.949 4.815 6.322 6.109 | 6.191 6.237 6.345 5.249 5.491 5.112 6.284 5.159 5.488 4.89 | 5.068 5.106 |
|
||||
|
||||
The split is 4.8% faster on Noita's trace (mean 4.885 against 5.130) and
|
||||
4.9% slower on HK's (mean 5.645 against 5.383). The HK trace's runs fall
|
||||
into two groups, about 5.0 and about 6.2 ms, in every build; three of the
|
||||
ten unsplit runs land high and four of the ten split ones. A V8 CPU profile
|
||||
of the split's measured HK window has no sample in the cold part (fin
|
||||
5.583, split 5.503 ms/frame with the profiler on), so the difference is
|
||||
not cold code running: it is where the code sits, about the size of the
|
||||
spread.
|
||||
|
||||
Pages (last-minute presents/s, each minute of the last four):
|
||||
|
||||
| page | build | presents/s | cold part |
|
||||
| --- | --- | --- | --- |
|
||||
| Muck | master's Mesa | 10.8 11.0 11.1 10.4 | none |
|
||||
| Muck | split | 11.2 11.2 11.1 10.5 | one miss, prefetched: 5.7 ms |
|
||||
| HK (j4) | split, then master's | 13.6 12.8 12.9 13.3 / 14.9 14.9 14.8 14.6 | one miss, prefetched: 9.4 ms |
|
||||
| HK (j5, ABBA) | master's | 15.0 14.9 15.1 15.3 | none |
|
||||
| | split | 13.6 13.2 13.3 13.5 | one miss, prefetched: 8.9 ms |
|
||||
| | split | 14.6 14.7 14.8 14.7 | one miss, prefetched: 6.2 ms |
|
||||
| | master's | 14.0 13.7 13.6 13.9 | none |
|
||||
|
||||
Muck's split is 1.6% faster (11.0 against 10.8). HK's ABBA is 2.7% slower
|
||||
split (14.05 against 14.44), inside the spread of master's own two runs
|
||||
(15.08 and 13.80).
|
||||
|
||||
A miss's stall, edger:
|
||||
|
||||
| where | how | stall |
|
||||
| --- | --- | --- |
|
||||
| Muck and HK pages, compositor Worker | prefetched, then instantiated | 5.7 to 9.4 ms |
|
||||
| Muck page, compositor Worker, forced early miss (`_mesa_Viewport` cold) | synchronous fetch and compile, 9,577,491 bytes | 304.5 ms |
|
||||
| HK trace replay, main thread (lazy), forced late miss, no prefetch | synchronous fetch and compile | 261.7 ms |
|
||||
| the same, early miss | synchronous fetch and compile | 239.9 ms |
|
||||
| the same, late miss, prefetch still compiling | synchronous fetch and compile | 306.9 ms |
|
||||
|
||||
On worldmachine (RX 6500 XT, AMD; never compared with edger's numbers),
|
||||
the final build, HK menu trace, page main thread:
|
||||
|
||||
| run | build | Mesa CPU, ms/frame | cold part |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | whole (fin3) | 2.630 | none |
|
||||
| 1 | split (finsplit3) | 2.648 | loaded with the module in 60.8 ms |
|
||||
| 2 | split | 2.623 | loaded with the module in 70.0 ms |
|
||||
| 2 | whole | 2.625 | none |
|
||||
| forced miss, first frame (`_mesa_Viewport` cold, lazy, no prefetch) | split | 3.414 | fetched and compiled synchronously, 10,009,314 bytes, in 233.4 ms |
|
||||
| the same again | split | 3.256 | 232.1 ms |
|
||||
|
||||
The split is 0.3% slower there (2.636 against 2.628 ms/frame), and loads
|
||||
cleanly on a second GPU vendor. The same runs showed each of the module's
|
||||
four pthreads prefetching its own copy of the cold part (fetch and compile,
|
||||
129 to 368 ms each, on Workers), even where the main thread had loaded it
|
||||
with the module and even with `gexNoColdPrefetch` (a pthread does not get
|
||||
the module's options). Mesa runs no code on them; the prefetch now skips
|
||||
pthreads.
|
||||
|
||||
A miss while the prefetch is still compiling does not wait for it (a
|
||||
synchronous call cannot): it fetches and compiles again.
|
||||
|
||||
A miss now says where it came from (`..., from CALLER <- ...`, the wasm
|
||||
frames under the trampoline). One Muck page run of the build before the
|
||||
last list (j10) missed once before its first frame (241.0 ms, synchronous,
|
||||
in the compositor Worker), during a shader translation with no WGSL cache
|
||||
entry; three more runs of it (j11, j12) and the final build's profiles did
|
||||
not show it again, so which function it was is not known.
|
||||
|
||||
The final build on the pages (j14, edger): Muck 11.4 11.2 11.3 10.7
|
||||
presents/s, HK 14.5 14.6 14.7 14.6; each prefetched the cold part (83.6 and
|
||||
84.6 ms) and never missed.
|
||||
|
||||
### Testing it
|
||||
|
||||
The gate leg, after `web/build-all.sh --both`, on any host (SwiftShader):
|
||||
`tests/mesa-cold/gate.sh` runs the two below on the mt and st builds and
|
||||
the SP dist, and prints `ok mesa-cold gate: 3/3`. MP gears needs a GPU
|
||||
(`web/test/site.mjs gears` on web/dist, on the bench box: `ok site-gears`).
|
||||
|
||||
`node tests/mesa-cold/run.mjs [TARGET_DIR]` (GEX_WEBGPU_SOFTWARE=1 on a host
|
||||
without a GPU) replays the gears trace through the build's Mesa whole, split,
|
||||
and with glReadPixels forced cold (`web/mesa-split.py --cold`) and missed,
|
||||
with and without the prefetch, and checks every read-back hashes the same.
|
||||
On gooner, mt and st, all four legs pass with digest 9e7d5cb9/307200.
|
||||
|
||||
The realms, on gooner (SwiftShader; correctness only):
|
||||
|
||||
| realm | test | result |
|
||||
| --- | --- | --- |
|
||||
| MP page main thread | tests/mesa-cold (split leg) | cold part loaded with the module |
|
||||
| MP compositor Worker | tests/nt/browser-gui.mjs --arch x86 --gl, x64 --gl | 12/12 ok each, no miss |
|
||||
| MP compositor Worker, direct present | the same with GEX_NT_PAGE_FLAGS='&direct=1' | 12/12 ok each, no miss |
|
||||
| MP compositor Worker, forced miss | browser-gui x86 --gl with a hot list missing glwindow's functions | 12/12 ok, one synchronous load |
|
||||
| SP system Worker | web/test/site.mjs gears on web/dist-sp | ok |
|
||||
| eject page | web/build-eject-page.sh | carries gex-wgpu-replay.deferred.wasm beside the module; Mesa runs in the same compositor Worker |
|
||||
@@ -32,6 +32,9 @@ case ${GEX_CTS_BACKEND:-mesa} in
|
||||
mkdir -p "$tree/gex"
|
||||
cp "$MESA_BUILD/gex-wgpu-replay.js" \
|
||||
"$MESA_BUILD/gex-wgpu-replay.wasm" "$tree/gex/"
|
||||
rm -f "$tree/gex/gex-wgpu-replay.deferred.wasm"
|
||||
[ ! -f "$MESA_BUILD/gex-wgpu-replay.deferred.wasm" ] ||
|
||||
cp "$MESA_BUILD/gex-wgpu-replay.deferred.wasm" "$tree/gex/"
|
||||
done
|
||||
;;
|
||||
*) echo "unknown GEX_CTS_BACKEND: ${GEX_CTS_BACKEND}" >&2; exit 2 ;;
|
||||
|
||||
@@ -70,6 +70,9 @@ if [ -f "$MESA_BUILD/gex-wgpu-replay.wasm" ]; then
|
||||
mkdir -p "$tree/gex"
|
||||
cp "$MESA_BUILD/gex-wgpu-replay.js" \
|
||||
"$MESA_BUILD/gex-wgpu-replay.wasm" "$tree/gex/"
|
||||
rm -f "$tree/gex/gex-wgpu-replay.deferred.wasm"
|
||||
[ ! -f "$MESA_BUILD/gex-wgpu-replay.deferred.wasm" ] ||
|
||||
cp "$MESA_BUILD/gex-wgpu-replay.deferred.wasm" "$tree/gex/"
|
||||
done
|
||||
else
|
||||
echo "warning: no gex-wgpu-replay in $MESA_BUILD; testing whatever host" \
|
||||
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# tests/mesa-cold/gate.sh: the gate leg for Mesa's cold part (docs/mesa-wasm-size.md), after
|
||||
# `web/build-all.sh --both`. Correctness only (SwiftShader, any host); timing is the bench box's.
|
||||
# 1. tests/mesa-cold/run.mjs on the mt and the st build: whole, split, a forced miss, a prefetched miss
|
||||
# 2. web/test/site.mjs gears on the SP dist (Mesa in the system Worker, split, lazy)
|
||||
# Prints `ok mesa-cold gate: 3/3` when every leg passes; exits non-zero otherwise.
|
||||
# MP gears needs a GPU (it fails on SwiftShader on master too): run it on the bench box.
|
||||
set -u
|
||||
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
cd "$ROOT"
|
||||
# Chromium's singleton socket path must stay short.
|
||||
export TMPDIR=$(mktemp -d /tmp/mcold.XXXX)
|
||||
trap 'sleep 1; rm -rf "$TMPDIR" 2>/dev/null' EXIT
|
||||
export GEX_WEBGPU_SOFTWARE=1
|
||||
pass=0
|
||||
for v in mt st; do
|
||||
out=$(node tests/mesa-cold/run.mjs "target/web-build/mesa-$v/build/src/gallium/targets/gex-wasm" 2>&1)
|
||||
echo "$out" | sed "s/^/[$v] /"
|
||||
echo "$out" | grep -q '^ok mesa-cold: 4/4' && pass=$((pass + 1))
|
||||
done
|
||||
out=$(GEX_NO_COI=1 GEX_DIST="$ROOT/web/dist-sp" node web/test/site.mjs gears 2>&1)
|
||||
echo "$out" | tail -2 | sed 's/^/[sp] /'
|
||||
echo "$out" | grep -q '^ok site-gears' && pass=$((pass + 1))
|
||||
if [ $pass = 3 ]; then echo "ok mesa-cold gate: 3/3"; else echo "FAIL mesa-cold gate: $pass/3"; exit 1; fi
|
||||
@@ -0,0 +1,105 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Mesa cold part</title>
|
||||
<pre id="out">loading</pre>
|
||||
<script type="module">
|
||||
// tests/mesa-cold/run.mjs's page: replays a GL trace through one build of
|
||||
// Mesa's replay module on WebGPU, then reads the last frame back (READ_PIXELS,
|
||||
// which waits on a buffer map: a JSPI suspension) and hashes it.
|
||||
// ?mesa=URL of gex-wgpu-replay.js &trace=URL
|
||||
// &lazy=1 leave the cold part for a miss on this main thread too
|
||||
// &noprefetch=1 and do not prefetch it after the first frame
|
||||
// &settle=MS wait this long before the read-back (a prefetch's time)
|
||||
// Result in globalThis.__result.
|
||||
const q = new URLSearchParams(location.search);
|
||||
const out = document.getElementById('out');
|
||||
const log = (s) => { out.textContent += '\n' + s; console.log(s); };
|
||||
function textureCanvas() {
|
||||
const canvas = new OffscreenCanvas(16, 16);
|
||||
let config = null, texture = null;
|
||||
const context = {
|
||||
canvas,
|
||||
configure(c) { config = c; },
|
||||
unconfigure() { config = null; },
|
||||
getConfiguration() { return config; },
|
||||
getCurrentTexture() {
|
||||
if (!texture || texture.width !== canvas.width || texture.height !== canvas.height) {
|
||||
texture?.destroy();
|
||||
texture = config.device.createTexture({
|
||||
size: [canvas.width, canvas.height], format: config.format, viewFormats: config.viewFormats ?? [],
|
||||
usage: (config.usage ?? GPUTextureUsage.RENDER_ATTACHMENT) | GPUTextureUsage.TEXTURE_BINDING |
|
||||
GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
|
||||
});
|
||||
}
|
||||
return texture;
|
||||
},
|
||||
};
|
||||
const own = canvas.getContext.bind(canvas);
|
||||
canvas.getContext = (t, o) => (t === 'webgpu' ? context : own(t, o));
|
||||
return canvas;
|
||||
}
|
||||
function hasPresent(b) {
|
||||
const v = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
||||
for (let at = 0; at + 8 <= b.length;) {
|
||||
const op = v.getUint32(at, true), size = v.getUint32(at + 4, true);
|
||||
if (op === 4) return true;
|
||||
if (size < 8) break;
|
||||
at += size;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
if (!adapter) throw new Error('no WebGPU adapter');
|
||||
const optional = ['texture-compression-bc', 'depth32float-stencil8', 'rg11b10ufloat-renderable',
|
||||
'depth-clip-control', 'float32-filterable', 'float32-blendable', 'clip-distances', 'dual-source-blending'];
|
||||
const device = await adapter.requestDevice({ requiredFeatures: optional.filter((f) => adapter.features.has(f)) });
|
||||
let validation = 0;
|
||||
device.addEventListener('uncapturederror', (e) => { if (validation++ < 5) console.error('WebGPU validation: ' + e.error?.message); });
|
||||
const glCanvas = textureCanvas();
|
||||
const qs = document.querySelector.bind(document);
|
||||
document.querySelector = (s) => (String(s).includes('gex-gl') ? glCanvas : qs(s));
|
||||
const mesa = new URL(q.get('mesa'), location.href).href;
|
||||
const { default: create } = await import(mesa);
|
||||
const m = await create({
|
||||
preinitializedWebGPUDevice: device, mainScriptUrlOrBlob: mesa,
|
||||
locateFile: (f) => new URL(f, mesa).href,
|
||||
gexColdLazy: q.get('lazy') === '1', gexNoColdPrefetch: q.get('noprefetch') === '1',
|
||||
print: (l) => console.log('[mesa] ' + l), printErr: (l) => console.warn('[mesa] ' + l),
|
||||
});
|
||||
const submit = WebAssembly.promising(m.gexGlSubmitRaw);
|
||||
const bytes = new Uint8Array(await (await fetch(q.get('trace'))).arrayBuffer());
|
||||
const dv = new DataView(bytes.buffer);
|
||||
const W = dv.getUint32(16, true), H = dv.getUint32(20, true);
|
||||
glCanvas.width = W; glCanvas.height = H;
|
||||
if (m._gex_gl_init(W, H) !== 0) throw new Error('init failed');
|
||||
let ptr = m._malloc(1 << 20), cap = 1 << 20, frames = 0, rejects = 0;
|
||||
for (let at = 24; at < bytes.length;) {
|
||||
const len = dv.getUint32(at, true);
|
||||
const b = bytes.subarray(at + 8, at + 8 + len);
|
||||
at += 8 + len + ((4 - (len & 3)) & 3);
|
||||
if (b.length > cap) { m._free(ptr); cap = b.length; ptr = m._malloc(cap); }
|
||||
m.HEAPU8.set(b, ptr);
|
||||
if ((await submit(ptr, b.length, 0)) !== 0) rejects++;
|
||||
if (hasPresent(b)) {
|
||||
m._gex_gl_display();
|
||||
if (++frames % 32 === 0) await device.queue.onSubmittedWorkDone();
|
||||
}
|
||||
}
|
||||
await device.queue.onSubmittedWorkDone();
|
||||
await new Promise((r) => setTimeout(r, Number(q.get('settle') || 0)));
|
||||
// READ_PIXELS of the default framebuffer, RGBA/UNSIGNED_BYTE.
|
||||
const cmd = new Uint8Array(44); const cv = new DataView(cmd.buffer);
|
||||
[128, cmd.length, 0, 0, W, H, 0x1908, 0x1401, 0, 0, 0].forEach((w, i) => cv.setUint32(i * 4, w, true));
|
||||
m.HEAPU8.set(cmd, ptr);
|
||||
if ((await submit(ptr, cmd.length, 0)) !== 0) rejects++;
|
||||
const rlen = m._gex_gl_reply_len(), rat = m._gex_gl_reply_data();
|
||||
const px = m.HEAPU8.slice(rat + 8, rat + rlen);
|
||||
let h = 0x811c9dc5;
|
||||
for (let i = 0; i < px.length; i++) { h ^= px[i]; h = Math.imul(h, 0x01000193) >>> 0; }
|
||||
globalThis.__result = { ok: rejects === 0 && validation === 0, digest: `${h.toString(16)}/${px.length}`, frames, rejects, validation };
|
||||
} catch (e) {
|
||||
globalThis.__result = { ok: false, error: String(e?.stack || e) };
|
||||
}
|
||||
log(JSON.stringify(globalThis.__result));
|
||||
</script>
|
||||
Executable
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env node
|
||||
// Mesa's cold part (docs/mesa-wasm-size.md): does the split replay draw what
|
||||
// the whole one does, and does a miss load the cold part and carry on?
|
||||
//
|
||||
// node tests/mesa-cold/run.mjs [TARGET_DIR]
|
||||
//
|
||||
// TARGET_DIR is a Mesa build's gex-wasm target directory, after
|
||||
// web/build-mesa-wasm.sh (default target/web-build/mesa-mt/build/src/gallium/
|
||||
// targets/gex-wasm; the st one works the same). Replays
|
||||
// tests/gl-traces/gles-gears-frames.gexgltr in Chromium (GEX_WEBGPU_SOFTWARE=1:
|
||||
// on SwiftShader), reads the last frame back and hashes it, through:
|
||||
//
|
||||
// whole the module unsplit (the reference)
|
||||
// split the build's own split, cold part loaded with the module
|
||||
// (as a page's main thread does)
|
||||
// miss every function kept but glReadPixels' (web/mesa-split.py
|
||||
// --cold), left for a miss: the cold part is fetched and
|
||||
// compiled synchronously, and the read-back then waits on
|
||||
// WebGPU (JSPI) inside a function it brought
|
||||
// prefetch the same after the background prefetch: instantiated only
|
||||
//
|
||||
// Prints `ok mesa-cold <leg>: ...` per leg and exits 0 when every digest
|
||||
// matches the whole module's and each miss was taken as described.
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { createServer } from 'node:http';
|
||||
import { copyFileSync, createReadStream, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, extname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Cdp, browserWsUrl } from '../../web/test/cdp.mjs';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const TGT = resolve(process.argv[2] || join(ROOT, 'target/web-build/mesa-mt/build/src/gallium/targets/gex-wasm'));
|
||||
const TRACE = join(ROOT, 'tests/gl-traces/gles-gears-frames.gexgltr');
|
||||
const CHROME = process.env.CHROME || '/usr/bin/chromium';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const fail = (msg) => { console.log(`FAIL mesa-cold: ${msg}`); process.exitCode = 1; };
|
||||
|
||||
for (const f of ['gex-wgpu-replay.wasm.orig', 'gex-wgpu-replay.js', 'split/gex-wgpu-replay.wasm']) {
|
||||
if (!existsSync(join(TGT, f))) { console.log(`skip mesa-cold: no ${f} in ${TGT} (web/build-mesa-wasm.sh)`); process.exit(0); }
|
||||
}
|
||||
const WORK = mkdtempSync(join(process.env.TMPDIR || tmpdir(), 'mesa-cold-'));
|
||||
const glue = readFileSync(join(TGT, 'gex-wgpu-replay.js'), 'utf8');
|
||||
const marked = (split) => glue.replace(/(gexMesaColdSplit *= *["'])GEX_MESA_COLD_[A-Z]*/,
|
||||
`$1GEX_MESA_COLD_${split ? 'YES' : 'NO'}`);
|
||||
const variant = (name, files, split) => {
|
||||
spawnSync('mkdir', ['-p', join(WORK, name)]);
|
||||
for (const [to, from] of Object.entries(files)) copyFileSync(from, join(WORK, name, to));
|
||||
writeFileSync(join(WORK, name, 'gex-wgpu-replay.js'), marked(split));
|
||||
};
|
||||
variant('whole', { 'gex-wgpu-replay.wasm': join(TGT, 'gex-wgpu-replay.wasm.orig') }, false);
|
||||
const cold = join(TGT, 'split/gex-wgpu-replay.deferred.wasm');
|
||||
variant('split', { 'gex-wgpu-replay.wasm': join(TGT, 'split/gex-wgpu-replay.wasm'),
|
||||
...(existsSync(cold) ? { 'gex-wgpu-replay.deferred.wasm': cold } : {}) }, existsSync(cold));
|
||||
const r = spawnSync('python3', [join(ROOT, 'web/mesa-split.py'), join(TGT, 'gex-wgpu-replay.wasm.orig'), '-',
|
||||
join(WORK, 'miss'), '--cold', '[Rr]ead[Pp]ixels'], { encoding: 'utf8' });
|
||||
if (r.status !== 0) { fail(`mesa-split.py: ${r.stderr || r.stdout}`); process.exit(1); }
|
||||
writeFileSync(join(WORK, 'miss', 'gex-wgpu-replay.js'), marked(true));
|
||||
console.log(` ${r.stdout.trim()}`);
|
||||
|
||||
const types = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.wasm': 'application/wasm' };
|
||||
const server = createServer((req, res) => {
|
||||
const p = decodeURIComponent(new URL(req.url, 'http://x').pathname);
|
||||
if (p.includes('..')) { res.writeHead(403).end(); return; }
|
||||
const file = p === '/' ? join(ROOT, 'tests/mesa-cold/page.html') : p === '/trace' ? TRACE : join(WORK, p);
|
||||
let st;
|
||||
try { st = statSync(file); } catch { res.writeHead(404).end(); return; }
|
||||
res.writeHead(200, { 'Content-Type': types[extname(file)] ?? 'application/octet-stream', 'Content-Length': st.size,
|
||||
'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp' });
|
||||
createReadStream(file).pipe(res);
|
||||
});
|
||||
await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
|
||||
const port = server.address().port;
|
||||
|
||||
async function leg(name, dir, query) {
|
||||
const user = mkdtempSync(join(WORK, 'chrome-'));
|
||||
const cdpPort = Number(process.env.GEX_MESA_COLD_CDP_PORT || 9667);
|
||||
const chrome = spawn(CHROME, ['--headless', '--no-sandbox', '--disable-gpu-sandbox', '--enable-unsafe-webgpu',
|
||||
'--enable-unsafe-swiftshader',
|
||||
...(process.env.GEX_WEBGPU_SOFTWARE === '1' ? ['--use-angle=swiftshader-webgl', '--use-webgpu-adapter=swiftshader'] : []),
|
||||
'--no-first-run', '--remote-allow-origins=*', `--remote-debugging-port=${cdpPort}`, `--user-data-dir=${user}`,
|
||||
'about:blank'], { stdio: 'ignore' });
|
||||
try {
|
||||
let ws = null;
|
||||
for (let i = 0; i < 80 && !ws; i++) { ws = await browserWsUrl(cdpPort); if (!ws) await sleep(250); }
|
||||
if (!ws) throw new Error('no browser');
|
||||
const browser = await Cdp.connect(ws);
|
||||
const { targetId } = await browser.send('Target.createTarget', { url: 'about:blank' });
|
||||
const { sessionId: s } = await browser.send('Target.attachToTarget', { targetId, flatten: true });
|
||||
await browser.send('Runtime.enable', {}, s);
|
||||
const lines = [];
|
||||
browser.on('Runtime.consoleAPICalled', (p, sid) => {
|
||||
if (sid === s) lines.push(p.args.map((a) => a.value ?? a.description ?? '').join(' '));
|
||||
});
|
||||
await browser.send('Page.navigate', { url: `http://127.0.0.1:${port}/?mesa=/${dir}/gex-wgpu-replay.js&trace=/trace${query}` }, s);
|
||||
let res = null;
|
||||
for (let t0 = Date.now(); !res && Date.now() - t0 < 300000;) {
|
||||
await sleep(250);
|
||||
res = (await browser.send('Runtime.evaluate', { expression: 'globalThis.__result ?? null', returnByValue: true }, s)
|
||||
.catch(() => null))?.result?.value ?? null;
|
||||
}
|
||||
browser.close();
|
||||
return { res: res ?? { ok: false, error: 'timeout' }, cold: lines.filter((l) => l.includes('cold part')) };
|
||||
} finally {
|
||||
chrome.kill('SIGKILL');
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
const want = {
|
||||
whole: [],
|
||||
split: existsSync(cold) ? ['loaded with the module'] : [],
|
||||
miss: ['loaded on a miss (fetched and compiled now'],
|
||||
prefetch: ['loaded on a miss (prefetched)'],
|
||||
};
|
||||
const legs = [['whole', 'whole', ''], ['split', 'split', ''], ['miss', 'miss', '&lazy=1&noprefetch=1'],
|
||||
['prefetch', 'miss', '&lazy=1&settle=3000']];
|
||||
let ref = null, passed = 0;
|
||||
for (const [name, dir, query] of legs) {
|
||||
const { res, cold: seen } = await leg(name, dir, query);
|
||||
const how = seen.join(' | ') || 'no cold part loaded';
|
||||
if (!res.ok) { fail(`${name}: ${res.error ?? JSON.stringify(res)}`); continue; }
|
||||
ref ??= res.digest;
|
||||
if (res.digest !== ref) { fail(`${name}: digest ${res.digest}, the whole module's ${ref}`); continue; }
|
||||
const need = want[name];
|
||||
if (need.some((w) => !seen.some((l) => l.includes(w))) || (need.length === 0 && seen.length)) {
|
||||
fail(`${name}: ${how}; wanted ${need.join(', ') || 'none'}`);
|
||||
continue;
|
||||
}
|
||||
passed++;
|
||||
console.log(`ok mesa-cold ${name}: digest ${res.digest}, ${res.frames} frames; ${how}`);
|
||||
}
|
||||
server.close();
|
||||
rmSync(WORK, { recursive: true, force: true });
|
||||
if (passed === legs.length) console.log(`ok mesa-cold: ${passed}/${legs.length}`);
|
||||
process.exit(process.exitCode ?? 0);
|
||||
@@ -31,6 +31,9 @@ for tree in "$ROOT/web/public" "$ROOT/web/dist"; do
|
||||
[ -d "$tree" ] || continue
|
||||
mkdir -p "$tree/gex"
|
||||
cp "$BUILD/gex-wgpu-replay.js" "$BUILD/gex-wgpu-replay.wasm" "$tree/gex/"
|
||||
# The cold part, when the build split one off (docs/mesa-wasm-size.md).
|
||||
rm -f "$tree/gex/gex-wgpu-replay.deferred.wasm"
|
||||
[ ! -f "$BUILD/gex-wgpu-replay.deferred.wasm" ] || cp "$BUILD/gex-wgpu-replay.deferred.wasm" "$tree/gex/"
|
||||
done
|
||||
|
||||
# The guest's own GL driver too, and the glibc build of it, which is the one
|
||||
|
||||
@@ -34,6 +34,9 @@ BUILD=${GEX_MESA_WASM_OUT:-${TMPDIR:-/tmp}/gex-mesa-wasm}/build/src/gallium/targ
|
||||
mkdir -p "$WORK" "$ROOT/web/public/gex" "$ROOT/web/dist/gex"
|
||||
for tree in "$ROOT/web/public" "$ROOT/web/dist"; do
|
||||
cp "$BUILD/gex-wgpu-replay.js" "$BUILD/gex-wgpu-replay.wasm" "$tree/gex/"
|
||||
# The cold part, when the build split one off (docs/mesa-wasm-size.md).
|
||||
rm -f "$tree/gex/gex-wgpu-replay.deferred.wasm"
|
||||
[ ! -f "$BUILD/gex-wgpu-replay.deferred.wasm" ] || cp "$BUILD/gex-wgpu-replay.deferred.wasm" "$tree/gex/"
|
||||
done
|
||||
|
||||
GEX_CHROME_PORT=${GEX_CHROME_PORT:-9335} \
|
||||
|
||||
@@ -30,6 +30,9 @@ REPLAY=$BUILD/gex-mesa-replay.js
|
||||
for tree in "$ROOT/web/public" "$ROOT/web/dist"; do
|
||||
mkdir -p "$tree/gex"
|
||||
cp "$BUILD/gex-wgpu-replay.js" "$BUILD/gex-wgpu-replay.wasm" "$tree/gex/"
|
||||
# The cold part, when the build split one off (docs/mesa-wasm-size.md).
|
||||
rm -f "$tree/gex/gex-wgpu-replay.deferred.wasm"
|
||||
[ ! -f "$BUILD/gex-wgpu-replay.deferred.wasm" ] || cp "$BUILD/gex-wgpu-replay.deferred.wasm" "$tree/gex/"
|
||||
cp "$TRACE" "$tree/gex/replay.gexgltr"
|
||||
done
|
||||
cp "$ROOT/web/public/mesa-wgpu-replay.html" "$ROOT/web/dist/mesa-wgpu-replay.html"
|
||||
|
||||
@@ -34,6 +34,9 @@ node "$ROOT/tests/gl-make-context-trace.mjs" "$WORK/context.gexgltr" "$API" 1 0
|
||||
for tree in "$ROOT/web/public" "$ROOT/web/dist"; do
|
||||
mkdir -p "$tree/gex"
|
||||
cp "$BUILD/gex-wgpu-replay.js" "$BUILD/gex-wgpu-replay.wasm" "$tree/gex/"
|
||||
# The cold part, when the build split one off (docs/mesa-wasm-size.md).
|
||||
rm -f "$tree/gex/gex-wgpu-replay.deferred.wasm"
|
||||
[ ! -f "$BUILD/gex-wgpu-replay.deferred.wasm" ] || cp "$BUILD/gex-wgpu-replay.deferred.wasm" "$tree/gex/"
|
||||
cp "$WORK/context.gexgltr" "$tree/gex/replay.gexgltr"
|
||||
done
|
||||
cp "$ROOT/web/public/mesa-wgpu-replay.html" "$ROOT/web/dist/mesa-wgpu-replay.html"
|
||||
|
||||
Vendored
+1
-1
Submodule third_party/mesa updated: 6020224605...c530e32d21.
@@ -583,7 +583,10 @@ def emit_nt(cmds, plans, opcodes, pname_rust):
|
||||
def host_case(c, opcode, plan):
|
||||
toks = plan.split(" ")
|
||||
r, ptoks = toks[0], toks[1:]
|
||||
L = [f"/* {c.name} */", f"template <typename R> static bool gexgen_{opcode}(R& a) {{"]
|
||||
# Never inlined: each is called from one case of the decoder's switch, and
|
||||
# inlined, all 2,520 made that switch one 1.2 MB function -- slow for V8
|
||||
# to compile, and indivisible by wasm-split (docs/mesa-wasm-size.md).
|
||||
L = [f"/* {c.name} */", f"template <typename R> __attribute__((noinline)) static bool gexgen_{opcode}(R& a) {{"]
|
||||
args = []
|
||||
post = []
|
||||
check = []
|
||||
|
||||
Executable
+195
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Make web/mesa-hot.txt, the functions Mesa's replay keeps out of its cold part.
|
||||
|
||||
tools/mesa-hot.py PLAIN.wasm LOG... [-o web/mesa-hot.txt] [--callee-max BYTES]
|
||||
[--fanout-max N] [--also NAMES.txt]... [--drop REGEX]...
|
||||
|
||||
PLAIN.wasm is the profiling build's module before instrumentation; each LOG
|
||||
holds the `[gex-wgpu] mesa-profile N HITS BASE64` lines the instrumented one
|
||||
printed while a workload ran (docs/mesa-wasm-size.md, "Measuring"). A
|
||||
function is hot if any LOG saw it run. The margin adds what a hot function
|
||||
calls directly when it is at most --callee-max bytes (default 1024): the
|
||||
error and edge paths a hot function reaches on another workload's input.
|
||||
A caller of more than --fanout-max functions (default 64) adds none: that is
|
||||
a dispatcher, like the wire decoder's `one()` over its 2,520 per-command
|
||||
decoders, and its callees are as cold as the profiles say.
|
||||
|
||||
--also adds the names in a list this script wrote before, for another build
|
||||
of the same sources (a page run's profile outlives a change elsewhere in
|
||||
Mesa: names are matched, not indices). --callee-max 0 leaves the margin out.
|
||||
--drop leaves out every function whose name matches REGEX (Python, searched),
|
||||
from the profiles and the margin both: code a workload ran that a later
|
||||
change keeps from running (`spvtools::val::`, the SPIR-V validator, which
|
||||
runs only with TINT_SPIRV_VALIDATE=1).
|
||||
|
||||
Prints the hot set per log, the union and the margin, in functions and code
|
||||
bytes. The list holds Binaryen's names without ThinLTO's `.llvm.<hash>`
|
||||
suffixes (web/mesa-split.py matches them the same way).
|
||||
"""
|
||||
import base64
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
LLVM_SUFFIX = re.compile(r'\.llvm\.\d+')
|
||||
# Binaryen tells functions of the same name apart with a `_N` suffix whose N
|
||||
# moves whenever the module gains or loses one, so a list is matched with
|
||||
# those suffixes off too (a duplicate kept with its twin costs little).
|
||||
DUP_SUFFIX = re.compile(r'_\d+$')
|
||||
|
||||
|
||||
def stable(name):
|
||||
return DUP_SUFFIX.sub('', LLVM_SUFFIX.sub('', name))
|
||||
PROFILE = re.compile(r'mesa-profile (\d+) (\d+) (?:@(\d+) )?([A-Za-z0-9+/=]+)')
|
||||
|
||||
|
||||
def tool(name):
|
||||
try:
|
||||
root = subprocess.run(['em-config', 'BINARYEN_ROOT'], capture_output=True, text=True,
|
||||
check=True).stdout.strip()
|
||||
if os.path.exists(os.path.join(root, 'bin', name)):
|
||||
return os.path.join(root, 'bin', name)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
pass
|
||||
return shutil.which(name) or sys.exit(f'no {name}')
|
||||
|
||||
|
||||
def leb(b, i):
|
||||
r = s = 0
|
||||
while True:
|
||||
x = b[i]
|
||||
i += 1
|
||||
r |= (x & 0x7f) << s
|
||||
s += 7
|
||||
if x < 0x80:
|
||||
return r, i
|
||||
|
||||
|
||||
def body_sizes(path):
|
||||
"""Code bytes of each defined function, in order."""
|
||||
b = open(path, 'rb').read()
|
||||
i = 8
|
||||
while i < len(b):
|
||||
sid = b[i]
|
||||
size, i = leb(b, i + 1)
|
||||
if sid == 10:
|
||||
n, j = leb(b, i)
|
||||
out = []
|
||||
for _ in range(n):
|
||||
s, k = leb(b, j)
|
||||
out.append(s + k - j)
|
||||
j = k + s
|
||||
return out
|
||||
i += size
|
||||
return []
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
out, callee_max, fanout_max, also, drop = None, 1024, 64, [], []
|
||||
while '--drop' in args:
|
||||
k = args.index('--drop')
|
||||
drop.append(re.compile(args[k + 1]))
|
||||
del args[k:k + 2]
|
||||
while '--also' in args:
|
||||
k = args.index('--also')
|
||||
also.append(args[k + 1])
|
||||
del args[k:k + 2]
|
||||
if '-o' in args:
|
||||
k = args.index('-o')
|
||||
out = args[k + 1]
|
||||
del args[k:k + 2]
|
||||
if '--fanout-max' in args:
|
||||
k = args.index('--fanout-max')
|
||||
fanout_max = int(args[k + 1])
|
||||
del args[k:k + 2]
|
||||
if '--callee-max' in args:
|
||||
k = args.index('--callee-max')
|
||||
callee_max = int(args[k + 1])
|
||||
del args[k:k + 2]
|
||||
if len(args) < 1 or (len(args) < 2 and not also):
|
||||
sys.exit(__doc__)
|
||||
plain, logs = args[0], args[1:]
|
||||
fmap = subprocess.run([tool('wasm-opt'), '-all', plain, '--print-function-map', '-o', os.devnull],
|
||||
capture_output=True, text=True, check=True).stdout
|
||||
names = [l.split(':', 1)[1] for l in fmap.splitlines() if ':' in l]
|
||||
sizes = body_sizes(plain)
|
||||
nimp = len(names) - len(sizes)
|
||||
defined = names[nimp:]
|
||||
total = sum(sizes)
|
||||
print(f'{plain}: {len(defined)} functions, {total} code bytes')
|
||||
|
||||
def report(what, idx):
|
||||
b = sum(sizes[i] for i in idx)
|
||||
print(f' {what}: {len(idx)} functions ({100 * len(idx) / len(defined):.1f}%), '
|
||||
f'{b} bytes ({100 * b / total:.1f}%)')
|
||||
|
||||
union = set()
|
||||
for log in logs:
|
||||
seen = set()
|
||||
for line in open(log, errors='replace'):
|
||||
m = PROFILE.search(line)
|
||||
if not m:
|
||||
continue
|
||||
n = int(m.group(1))
|
||||
if n != len(defined):
|
||||
sys.exit(f'{log}: a profile of {n} functions, not this module\'s {len(defined)}')
|
||||
# `@OFFSET`: a piece of the set from that byte on (web/gex-wgpu-jspi.js);
|
||||
# without one, the whole set. A line a harness cut short still
|
||||
# gives the functions it got as far as.
|
||||
text = m.group(4)
|
||||
bits = base64.b64decode(text[:len(text) // 4 * 4])
|
||||
base = int(m.group(3) or 0) * 8
|
||||
seen |= {base + i for i in range(min(len(bits) * 8, n - base)) if bits[i >> 3] >> (i & 7) & 1}
|
||||
report(os.path.basename(log), seen)
|
||||
union |= seen
|
||||
by_name = {}
|
||||
for i, n in enumerate(defined):
|
||||
by_name.setdefault(stable(n), []).append(i)
|
||||
for path in also:
|
||||
names = {l.strip() for l in open(path) if l.strip() and not l.startswith('#')}
|
||||
seen = {i for n in names for i in by_name.get(stable(n), ())}
|
||||
report(f'{os.path.basename(path)} ({len(names)} names, {len({n for n in names if stable(n) not in by_name})} not in this build)', seen)
|
||||
union |= seen
|
||||
unescape = lambda n: re.sub(r'\\([0-9a-f]{2})', lambda m: chr(int(m.group(1), 16)), n)
|
||||
dropped = {i for i in range(len(defined))
|
||||
if any(r.search(defined[i]) or r.search(unescape(defined[i])) for r in drop)}
|
||||
if drop:
|
||||
report('dropped (--drop)', union & dropped)
|
||||
union -= dropped
|
||||
report('union', union)
|
||||
|
||||
# Direct calls, from Binaryen's text: `(func $name` opens a body.
|
||||
index = {n: i for i, n in enumerate(defined)}
|
||||
calls, cur = {}, None
|
||||
head, call = re.compile(r'^ \(func \$(\S+)'), re.compile(r'\(call \$([^\s)]+)')
|
||||
dis = subprocess.Popen([tool('wasm-dis'), plain], stdout=subprocess.PIPE, text=True,
|
||||
errors='replace')
|
||||
for line in dis.stdout:
|
||||
m = head.match(line)
|
||||
if m:
|
||||
cur = index.get(m.group(1))
|
||||
continue
|
||||
if cur in union and '(call $' in line:
|
||||
for c in call.findall(line):
|
||||
j = index.get(c)
|
||||
if j is not None:
|
||||
calls.setdefault(cur, set()).add(j)
|
||||
dis.wait()
|
||||
margin = {j for i in union if len(calls.get(i, ())) <= fanout_max
|
||||
for j in calls.get(i, ()) if j not in union and j not in dropped and sizes[j] <= callee_max}
|
||||
report(f'margin (callees up to {callee_max} bytes of callers of up to {fanout_max})', margin)
|
||||
keep = union | margin
|
||||
report('kept', keep)
|
||||
if out:
|
||||
with open(out, 'w') as f:
|
||||
f.write('# Mesa replay functions kept out of the cold part: tools/mesa-hot.py,\n'
|
||||
'# docs/mesa-wasm-size.md. Binaryen names, without .llvm.<hash>.\n')
|
||||
f.writelines(sorted({LLVM_SUFFIX.sub('', defined[i]) + '\n' for i in keep}))
|
||||
print(f'wrote {out}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -31,4 +31,8 @@ for f in gex-wgpu-replay.js gex-wgpu-replay.wasm; do
|
||||
if [ -f "$WEB/public/gex/$f" ]; then cp "$WEB/public/gex/$f" "$OUT/gex/$f"
|
||||
else echo "build-eject-page: no web/public/gex/$f (web/build-mesa-wasm.sh): the page has no OpenGL" >&2; fi
|
||||
done
|
||||
# Mesa's cold part, when the build split one off (docs/mesa-wasm-size.md).
|
||||
rm -f "$OUT/gex/gex-wgpu-replay.deferred.wasm"
|
||||
[ ! -f "$WEB/public/gex/gex-wgpu-replay.deferred.wasm" ] ||
|
||||
cp "$WEB/public/gex/gex-wgpu-replay.deferred.wasm" "$OUT/gex/"
|
||||
echo "build-eject-page: $OUT"
|
||||
+48
-4
@@ -148,12 +148,56 @@ fi
|
||||
# Where the page loads them from. Left out before, so a rebuilt module sat in
|
||||
# the build directory while the site served whatever had last been copied by
|
||||
# hand -- which looks exactly like a change that did not work.
|
||||
#
|
||||
# The replay links with -sSPLIT_MODULE=1: its .wasm is wasm-split's
|
||||
# instrumented module and never shipped, and .wasm.orig is the plain one,
|
||||
# which web/mesa-split.py splits by web/mesa-hot.txt into what the page loads
|
||||
# and a cold part (gex-wgpu-replay.deferred.wasm) it fetches the first time a
|
||||
# function there runs (docs/mesa-wasm-size.md). GEX_MESA_HOT=none ships it
|
||||
# whole; GEX_MESA_HOT=FILE splits by another list. GEX_MESA_PROFILE=1 ships
|
||||
# it whole and instrumented instead, to measure which functions a workload
|
||||
# runs (wasm-split --instrument --in-memory: a byte per function at address
|
||||
# 0 up, below the main stack, which --stack-first puts first; the glue prints
|
||||
# the set, and tools/mesa-hot.py turns logs of it into web/mesa-hot.txt).
|
||||
TGT=$BUILD/src/gallium/targets/gex-wasm
|
||||
if [ "$SANITIZE" != 1 ] && [ -f "$TGT/gex-wgpu-replay.wasm.orig" ]; then
|
||||
HOT=${GEX_MESA_HOT:-$ROOT/web/mesa-hot.txt}
|
||||
if [ "${GEX_MESA_PROFILE:-0}" = 1 ]; then
|
||||
mkdir -p "$TGT/split"
|
||||
rm -f "$TGT/split/gex-wgpu-replay.deferred.wasm"
|
||||
# The link's features, as web/mesa-split.py names them.
|
||||
"$(em-config BINARYEN_ROOT)/bin/wasm-split" --mvp-features --enable-threads --enable-simd \
|
||||
--enable-bulk-memory --enable-bulk-memory-opt --enable-nontrapping-float-to-int \
|
||||
--enable-sign-ext --enable-mutable-globals --enable-multivalue \
|
||||
--instrument --in-memory -g \
|
||||
"$TGT/gex-wgpu-replay.wasm.orig" -o "$TGT/split/gex-wgpu-replay.wasm"
|
||||
echo " profiling build: tools/mesa-hot.py $TGT/gex-wgpu-replay.wasm.orig LOG..."
|
||||
else
|
||||
python3 "$ROOT/web/mesa-split.py" "$TGT/gex-wgpu-replay.wasm.orig" "$HOT" "$TGT/split"
|
||||
fi
|
||||
# The glue's loader (web/gex-wgpu-split.js) prefetches a cold part only
|
||||
# where there is one.
|
||||
if [ -f "$TGT/split/gex-wgpu-replay.deferred.wasm" ]; then
|
||||
cold=YES
|
||||
else
|
||||
cold=NO
|
||||
fi
|
||||
# The assignment only: the test beside it names YES too.
|
||||
sed -i "s/\\(gexMesaColdSplit *= *[\"']\\)GEX_MESA_COLD_[A-Z]*/\\1GEX_MESA_COLD_$cold/" "$TGT/gex-wgpu-replay.js"
|
||||
cp "$TGT/gex-wgpu-replay.js" "$TGT/split/gex-wgpu-replay.js"
|
||||
# And over the instrumented one, for the tests that copy from here.
|
||||
cp "$TGT/split/gex-wgpu-replay.wasm" "$TGT/gex-wgpu-replay.wasm"
|
||||
rm -f "$TGT/gex-wgpu-replay.deferred.wasm"
|
||||
[ -f "$TGT/split/gex-wgpu-replay.deferred.wasm" ] &&
|
||||
cp "$TGT/split/gex-wgpu-replay.deferred.wasm" "$TGT/gex-wgpu-replay.deferred.wasm"
|
||||
fi
|
||||
PUB="$ROOT/web/public/gex"
|
||||
[ "$THREADS" = 0 ] && [ -d "$PUB" ] && PUB="$PUB/st" && mkdir -p "$PUB"
|
||||
if [ -d "$PUB" ]; then
|
||||
for f in gex-wgpu-replay.js gex-wgpu-replay.wasm; do
|
||||
[ -f "$BUILD/src/gallium/targets/gex-wasm/$f" ] || continue
|
||||
cp "$BUILD/src/gallium/targets/gex-wasm/$f" "$PUB/$f"
|
||||
if [ -d "$PUB" ] && [ -f "$TGT/split/gex-wgpu-replay.wasm" ]; then
|
||||
rm -f "$PUB/gex-wgpu-replay.deferred.wasm"
|
||||
for f in gex-wgpu-replay.js gex-wgpu-replay.wasm gex-wgpu-replay.deferred.wasm; do
|
||||
[ -f "$TGT/split/$f" ] || continue
|
||||
cp "$TGT/split/$f" "$PUB/$f"
|
||||
echo " published $PUB/$f"
|
||||
done
|
||||
fi
|
||||
|
||||
+35
-1
@@ -1,5 +1,38 @@
|
||||
// JSPI bridge for synchronous Gallium readback. The exported smoke/replay
|
||||
// entry must be wrapped with WebAssembly.promising by its JavaScript caller.
|
||||
// A profiling build (wasm-split --instrument --in-memory; docs/mesa-wasm-size.md)
|
||||
// exports __write_profile: a flag per defined function, set the first time it
|
||||
// runs. Every 2 s, when the set has grown, the module prints it as a bitmap --
|
||||
// `[gex-wgpu] mesa-profile N HITS BASE64`, bit i for defined function i -- to
|
||||
// the console, where the test harnesses already keep `gex-wgpu` warnings.
|
||||
// Every other build exports no such function and this is never called.
|
||||
var GEX_PROFILE_JS = `
|
||||
function gexReportProfile(write) {
|
||||
var last = -1;
|
||||
var dump = function () {
|
||||
var need = write(0, 0);
|
||||
var at = _malloc(need);
|
||||
write(at, need);
|
||||
var n = (need - 8) >> 2, bits = new Uint8Array((n + 7) >> 3), hits = 0;
|
||||
if (typeof growMemViews == "function") growMemViews();
|
||||
var words = new Uint32Array(HEAPU8.slice(at + 8, at + 8 + n * 4).buffer);
|
||||
_free(at);
|
||||
for (var i = 0; i < n; i++) if (words[i]) { bits[i >> 3] |= 1 << (i & 7); hits++; }
|
||||
if (hits === last) return;
|
||||
last = hits;
|
||||
// In pieces of 2 KiB (2,732 characters of base64): page harnesses cut
|
||||
// console lines at about 4,000, and a 25,000-function set is 4,120.
|
||||
// Each piece says where it starts, in bytes: \`@OFFSET\`.
|
||||
for (var at0 = 0; at0 < bits.length; at0 += 2048) {
|
||||
var bin = '';
|
||||
for (var j = at0; j < Math.min(bits.length, at0 + 2048); j++) bin += String.fromCharCode(bits[j]);
|
||||
console.warn('[gex-wgpu] mesa-profile ' + n + ' ' + hits + ' @' + at0 + ' ' + btoa(bin));
|
||||
}
|
||||
};
|
||||
setInterval(dump, 2000);
|
||||
Module['gexDumpProfile'] = dump;
|
||||
}
|
||||
`;
|
||||
mergeInto(LibraryManager.library, {
|
||||
gex_wgpu_jspi_wait: function () {
|
||||
// A list, not one slot. WebGPU dispatches its map callbacks back into the
|
||||
@@ -18,7 +51,8 @@ mergeInto(LibraryManager.library, {
|
||||
// Publish whichever the linked module actually exports -- the raw wasm
|
||||
// export, because WebAssembly.promising cannot wrap the JS-side wrapper.
|
||||
gex_wgpu_jspi_wait__postset:
|
||||
'_gex_wgpu_jspi_wait = new WebAssembly.Suspending(_gex_wgpu_jspi_wait); addOnPostRun(function() { if (wasmExports[\'gex_wgpu_smoke\']) Module[\'gexWgpuRaw\'] = wasmExports[\'gex_wgpu_smoke\']; if (wasmExports[\'gex_gl_submit\']) Module[\'gexGlSubmitRaw\'] = wasmExports[\'gex_gl_submit\']; });',
|
||||
'_gex_wgpu_jspi_wait = new WebAssembly.Suspending(_gex_wgpu_jspi_wait); addOnPostRun(function() { if (wasmExports[\'gex_wgpu_smoke\']) Module[\'gexWgpuRaw\'] = wasmExports[\'gex_wgpu_smoke\']; if (wasmExports[\'gex_gl_submit\']) Module[\'gexGlSubmitRaw\'] = wasmExports[\'gex_gl_submit\']; if (wasmExports[\'__write_profile\']) gexReportProfile(wasmExports[\'__write_profile\']); });' +
|
||||
GEX_PROFILE_JS,
|
||||
|
||||
gex_wgpu_jspi_signal: function () {
|
||||
var waiting = Module['__gexWgpuWaiting'];
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// Mesa's cold part (docs/mesa-wasm-size.md): a --pre-js of gex-wgpu-replay.
|
||||
//
|
||||
// The build splits the module with Binaryen's wasm-split (web/mesa-split.py).
|
||||
// The functions the page's workloads run stay in gex-wgpu-replay.wasm; the
|
||||
// rest moves to gex-wgpu-replay.deferred.wasm, and each table slot that would
|
||||
// reach one of them holds a trampoline instead. A trampoline calls the import
|
||||
// `gexcold.load` below, which instantiates the whole cold part over those
|
||||
// slots, and then calls through its slot again: the function itself now. So
|
||||
// a miss costs one load per instance of the module (the page's or Worker's
|
||||
// that made it, and a pthread's own should Mesa run code there), not one per
|
||||
// function, and leaves no JS frame on the stack that JSPI would have to
|
||||
// suspend across. (Emscripten's own placeholders are JS functions that make
|
||||
// the call themselves; web/mesa-split.py replaces them.)
|
||||
//
|
||||
// Two ways in, both here because this file runs in every realm the module
|
||||
// does:
|
||||
//
|
||||
// - Prefetch. Once the module has presented a frame, the realm fetches the
|
||||
// cold part and compiles it asynchronously, in the background. A miss
|
||||
// after that only instantiates it.
|
||||
// - A miss before then loads it synchronously: a synchronous XHR as text in
|
||||
// the x-user-defined charset, compiled synchronously, which a Worker may
|
||||
// do at any size.
|
||||
//
|
||||
// A page's main thread may not (Chromium refuses a synchronous compile or
|
||||
// instantiation over 8 MB there), so on one the cold part is instantiated
|
||||
// with the module, asynchronously, and never missed.
|
||||
//
|
||||
// The build sets the marker below when it split the module
|
||||
// (web/build-mesa-wasm.sh); an unsplit build fetches nothing and changes
|
||||
// nothing.
|
||||
var gexMesaColdSplit = 'GEX_MESA_COLD_NO';
|
||||
var gexMesaCold = function () {
|
||||
return globalThis.__gexMesaCold || (globalThis.__gexMesaCold = {});
|
||||
};
|
||||
var gexMesaColdFile = function () {
|
||||
return wasmBinaryFile.slice(0, -5) + '.deferred.wasm';
|
||||
};
|
||||
var gexMesaColdInstance = null;
|
||||
|
||||
// `gexcold.load`: the cold part, instantiated over this instance's table.
|
||||
var gexMesaColdLoad = function () {
|
||||
if (gexMesaColdInstance) return;
|
||||
var t0 = performance.now();
|
||||
var file = gexMesaColdFile();
|
||||
var cache = gexMesaCold();
|
||||
var module = cache[file];
|
||||
var how = 'prefetched';
|
||||
if (!(module instanceof WebAssembly.Module)) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', file, false);
|
||||
xhr.overrideMimeType('text/plain; charset=x-user-defined');
|
||||
xhr.send(null);
|
||||
if (xhr.status !== 200 && xhr.status !== 0) {
|
||||
throw new Error("Mesa's cold part " + file + ': HTTP ' + xhr.status);
|
||||
}
|
||||
var text = xhr.responseText;
|
||||
var buf = new Uint8Array(text.length);
|
||||
for (var i = 0; i < text.length; i++) buf[i] = text.charCodeAt(i) & 0xff;
|
||||
module = cache[file] = new WebAssembly.Module(buf);
|
||||
how = 'fetched and compiled now, ' + buf.length + ' bytes';
|
||||
}
|
||||
gexMesaColdInstance = new WebAssembly.Instance(module, { primary: wasmRawExports });
|
||||
// Who missed: the frames under the trampoline, for web/mesa-hot.txt.
|
||||
var where = String(new Error().stack).split('\n').filter(function (l) {
|
||||
return l.indexOf('.wasm') >= 0 && l.indexOf('gexcoldtramp') < 0;
|
||||
}).slice(0, 4).map(function (l) { return l.trim().replace(/^at /, '').replace(/ \(.*$/, ''); });
|
||||
console.warn('[gex-wgpu] cold part loaded on a miss (' + how + ') in ' +
|
||||
(performance.now() - t0).toFixed(1) + ' ms, from ' + (where.join(' <- ') || '?'));
|
||||
};
|
||||
|
||||
if (gexMesaColdSplit === 'GEX_MESA_COLD_YES' && !Module['instantiateWasm']) {
|
||||
// The glue's own instantiation, with the trampolines' import added.
|
||||
Module['instantiateWasm'] = function (imports, done) {
|
||||
var cold = { load: gexMesaColdLoad };
|
||||
var all = new Proxy(imports, {
|
||||
get: function (target, ns) { return ns === 'gexcold' ? cold : target[ns]; },
|
||||
});
|
||||
if (typeof ENVIRONMENT_IS_PTHREAD !== 'undefined' && ENVIRONMENT_IS_PTHREAD) {
|
||||
// A pthread instantiates the module the page sent it, and has no file
|
||||
// name of its own for the cold part's to follow.
|
||||
wasmBinaryFile ??= findWasmBinary();
|
||||
done(new WebAssembly.Instance(wasmModule, all), wasmModule);
|
||||
return {};
|
||||
}
|
||||
wasmBinaryFile ??= findWasmBinary();
|
||||
var file = wasmBinaryFile;
|
||||
var slow = function () {
|
||||
return fetch(file, { credentials: 'same-origin' })
|
||||
.then(function (r) { return r.arrayBuffer(); })
|
||||
.then(function (bytes) { return WebAssembly.instantiate(bytes, all); });
|
||||
};
|
||||
// A page's main thread may not compile or instantiate a module over
|
||||
// 8 MB synchronously, so there the cold part is not left for a miss: it
|
||||
// is instantiated now, asynchronously, before the module starts. (The
|
||||
// pages that run Mesa there are demos and test harnesses; the desktop's
|
||||
// compositor and SP's system Worker are Workers, and load it lazily.)
|
||||
// `gexColdLazy` keeps it lazy anyway, for measuring a miss.
|
||||
var eager = typeof ENVIRONMENT_IS_WEB !== 'undefined' && ENVIRONMENT_IS_WEB &&
|
||||
!Module['gexColdLazy'];
|
||||
WebAssembly.instantiateStreaming(fetch(file, { credentials: 'same-origin' }), all)
|
||||
.catch(slow)
|
||||
.then(function (r) {
|
||||
if (!eager) return r;
|
||||
var t0 = performance.now();
|
||||
var cold = gexMesaColdFile();
|
||||
var imp = { primary: r.instance.exports };
|
||||
return WebAssembly.instantiateStreaming(fetch(cold, { credentials: 'same-origin' }), imp)
|
||||
.catch(function () {
|
||||
return fetch(cold, { credentials: 'same-origin' })
|
||||
.then(function (x) { return x.arrayBuffer(); })
|
||||
.then(function (bytes) { return WebAssembly.instantiate(bytes, imp); });
|
||||
})
|
||||
.then(function (c) {
|
||||
gexMesaColdInstance = c.instance;
|
||||
console.warn('[gex-wgpu] cold part loaded with the module (a page\'s main thread) in ' +
|
||||
(performance.now() - t0).toFixed(1) + ' ms');
|
||||
return r;
|
||||
});
|
||||
})
|
||||
.then(function (r) { done(r.instance, r.module); },
|
||||
function (e) { abort('instantiating Mesa failed: ' + e); });
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
if (gexMesaColdSplit === 'GEX_MESA_COLD_YES' && !Module['gexNoColdPrefetch'] &&
|
||||
!(typeof ENVIRONMENT_IS_PTHREAD !== 'undefined' && ENVIRONMENT_IS_PTHREAD)) {
|
||||
// Wait for the first presented frame, then fetch and compile in the
|
||||
// background. Not in a pthread: the module's pool started each with this
|
||||
// file, Mesa runs no code there, and every one of them fetched and
|
||||
// compiled its own copy (four, on an mt build). A miss on a pthread would
|
||||
// compile the cold part synchronously, which a Worker may do.
|
||||
var gexColdPoll = setInterval(function () {
|
||||
if (typeof _gex_gl_presented !== 'function' || typeof wasmBinaryFile !== 'string') return;
|
||||
var presented = 0;
|
||||
try { presented = _gex_gl_presented(); } catch (e) { return; }
|
||||
if (!presented) return;
|
||||
clearInterval(gexColdPoll);
|
||||
if (gexMesaColdInstance) return;
|
||||
var file = gexMesaColdFile();
|
||||
var cache = gexMesaCold();
|
||||
if (cache[file]) return;
|
||||
cache[file] = 'pending';
|
||||
var t0 = performance.now(), size = 0;
|
||||
fetch(file).then(function (r) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.arrayBuffer();
|
||||
}).then(function (bytes) {
|
||||
size = bytes.byteLength;
|
||||
return WebAssembly.compile(bytes);
|
||||
}).then(function (module) {
|
||||
if (!(cache[file] instanceof WebAssembly.Module)) cache[file] = module;
|
||||
console.warn('[gex-wgpu] cold part prefetched (' + size + ' bytes) in ' +
|
||||
(performance.now() - t0).toFixed(1) + ' ms');
|
||||
}, function (e) {
|
||||
if (cache[file] === 'pending') delete cache[file];
|
||||
console.warn('[gex-wgpu] cold part prefetch failed: ' + e);
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
+5839
File diff suppressed because it is too large.
Load diff
Executable
+281
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Split Mesa's replay module into what the page runs and a cold part.
|
||||
|
||||
web/mesa-split.py ORIG.wasm HOT.txt OUTDIR [--max-cold BYTES] [--cold REGEX]
|
||||
|
||||
ORIG.wasm is the link's plain module (gex-wgpu-replay.wasm.orig: the target
|
||||
links with -sSPLIT_MODULE=1, so the .wasm beside it is wasm-split's
|
||||
instrumented one and never shipped). HOT.txt lists the functions to keep in
|
||||
the primary (web/mesa-hot.txt; docs/mesa-wasm-size.md says how it is made).
|
||||
Writes OUTDIR/gex-wgpu-replay.wasm (names kept) and, when anything is left
|
||||
over, OUTDIR/gex-wgpu-replay.deferred.wasm (names stripped). --max-cold
|
||||
fails the split when the cold part is that big or bigger (default: no
|
||||
limit; a Worker compiles it synchronously at any size, and a page's main
|
||||
thread loads it with the module, web/gex-wgpu-split.js).
|
||||
|
||||
HOT.txt `-` keeps every function. --cold moves the functions whose names
|
||||
match REGEX to the cold part whatever HOT.txt says: a forced miss, for
|
||||
tests/mesa-cold.
|
||||
|
||||
Names are matched as Binaryen prints them, without ThinLTO's `.llvm.<hash>`
|
||||
suffixes, which change whenever their translation unit does. A name in the
|
||||
list that the module lacks is ignored; a function the list lacks is cold and
|
||||
costs one load of the cold part the first time it runs.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
LLVM_SUFFIX = re.compile(r'\.llvm\.\d+')
|
||||
# Binaryen tells functions of the same name apart with a `_N` suffix whose N
|
||||
# moves whenever the module gains or loses one, so a list is matched with
|
||||
# those suffixes off too (a duplicate kept with its twin costs little).
|
||||
DUP_SUFFIX = re.compile(r'_\d+$')
|
||||
|
||||
|
||||
def stable(name):
|
||||
return DUP_SUFFIX.sub('', LLVM_SUFFIX.sub('', name))
|
||||
# The features the link used. The module carries no target_features section
|
||||
# for Binaryen to detect them from, and `-all` is not a substitute: with it,
|
||||
# wasm-split writes the cold part's imports in newer encodings (exact
|
||||
# function imports, kind 0x20) that Chromium refuses to compile. Nor are
|
||||
# reference types here, though the module would validate with them: they let
|
||||
# wasm-split put the placeholders in a second table, and Emscripten's glue
|
||||
# calls the function it loaded through the first (`wasmTable.get(index)`),
|
||||
# which then holds something else at that index.
|
||||
FEATURES = ['--mvp-features', '--enable-threads', '--enable-simd', '--enable-bulk-memory',
|
||||
'--enable-bulk-memory-opt', '--enable-nontrapping-float-to-int', '--enable-sign-ext',
|
||||
'--enable-mutable-globals', '--enable-multivalue']
|
||||
|
||||
|
||||
def binaryen(tool):
|
||||
"""Binaryen from the Emscripten that linked the module."""
|
||||
try:
|
||||
root = subprocess.run(['em-config', 'BINARYEN_ROOT'], capture_output=True, text=True,
|
||||
check=True).stdout.strip()
|
||||
path = os.path.join(root, 'bin', tool)
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
pass
|
||||
return shutil.which(tool) or sys.exit(f'mesa-split: no {tool}')
|
||||
|
||||
|
||||
# -- Trampolines ---------------------------------------------------------------
|
||||
#
|
||||
# wasm-split leaves each table slot that reaches a cold function holding an
|
||||
# imported placeholder, and Emscripten's glue makes every placeholder a JS
|
||||
# function that loads the cold part and then calls the function from JS. A
|
||||
# JS frame in the middle of the stack is what JSPI cannot suspend across:
|
||||
# the first call into a cold function that then waits on WebGPU (a glReadPixels,
|
||||
# a buffer map) failed with "SuspendError: trying to suspend JS frames". So
|
||||
# the placeholders are wasm instead: a trampoline per slot that calls the one
|
||||
# import `gexcold.load` (web/gex-wgpu-split.js: loads the cold part, which
|
||||
# overwrites the slots) and then calls through its own slot again, now the
|
||||
# function itself, with no JS frame left on the stack.
|
||||
|
||||
PLACEHOLDER = 'placeholder.deferred'
|
||||
TRAMP = 'gexcoldtramp'
|
||||
VALTYPES = {0x7f: 'i32', 0x7e: 'i64', 0x7d: 'f32', 0x7c: 'f64', 0x7b: 'v128', 0x70: 'funcref',
|
||||
0x6f: 'externref'}
|
||||
|
||||
|
||||
def leb(b, i):
|
||||
r = s = 0
|
||||
while True:
|
||||
x = b[i]
|
||||
i += 1
|
||||
r |= (x & 0x7f) << s
|
||||
s += 7
|
||||
if x < 0x80:
|
||||
return r, i
|
||||
|
||||
|
||||
def uleb(n):
|
||||
out = bytearray()
|
||||
while True:
|
||||
x = n & 0x7f
|
||||
n >>= 7
|
||||
out.append(x | (0x80 if n else 0))
|
||||
if not n:
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def sections(b):
|
||||
i, out = 8, []
|
||||
while i < len(b):
|
||||
sid = b[i]
|
||||
size, j = leb(b, i + 1)
|
||||
out.append((sid, i, j, j + size))
|
||||
i = j + size
|
||||
return out
|
||||
|
||||
|
||||
def name(b, i):
|
||||
n, i = leb(b, i)
|
||||
return b[i:i + n].decode(), i + n
|
||||
|
||||
|
||||
def limits(b, i):
|
||||
flags = b[i]
|
||||
lo, i = leb(b, i + 1)
|
||||
hi = None
|
||||
if flags & 1:
|
||||
hi, i = leb(b, i)
|
||||
return (lo, hi), i
|
||||
|
||||
|
||||
def trampolines(primary, out_path, tmp):
|
||||
"""Replace PRIMARY's placeholder imports with wasm trampolines, in place."""
|
||||
b = open(primary, 'rb').read()
|
||||
types, imports, table_export, imp_at = [], [], None, None
|
||||
for sid, start, body, end in sections(b):
|
||||
if sid == 1:
|
||||
n, i = leb(b, body)
|
||||
for _ in range(n):
|
||||
assert b[i] == 0x60, 'mesa-split: a type other than a function type'
|
||||
np, i = leb(b, i + 1)
|
||||
params = [VALTYPES[x] for x in b[i:i + np]]
|
||||
i += np
|
||||
nr, i = leb(b, i)
|
||||
results = [VALTYPES[x] for x in b[i:i + nr]]
|
||||
i += nr
|
||||
types.append((params, results))
|
||||
elif sid == 2:
|
||||
imp_at = (start, end)
|
||||
n, i = leb(b, body)
|
||||
for _ in range(n):
|
||||
at = i
|
||||
mod, i = name(b, i)
|
||||
base, i = name(b, i)
|
||||
kind = b[i]
|
||||
i += 1
|
||||
if kind == 0:
|
||||
t, i = leb(b, i)
|
||||
elif kind == 1:
|
||||
i += 1
|
||||
_, i = limits(b, i)
|
||||
t = None
|
||||
elif kind == 2:
|
||||
_, i = limits(b, i)
|
||||
t = None
|
||||
elif kind == 3:
|
||||
i += 2
|
||||
t = None
|
||||
else:
|
||||
sys.exit(f'mesa-split: import kind {kind}')
|
||||
imports.append((mod, base, kind, t, b[at:i]))
|
||||
elif sid == 7:
|
||||
n, i = leb(b, body)
|
||||
for _ in range(n):
|
||||
ex, i = name(b, i)
|
||||
kind = b[i]
|
||||
idx, i = leb(b, i + 1)
|
||||
if kind == 1 and idx == 0:
|
||||
table_export = ex
|
||||
holders = [(int(base), t) for mod, base, kind, t, _ in imports if mod == PLACEHOLDER]
|
||||
if not holders:
|
||||
return 0
|
||||
if table_export is None:
|
||||
sys.exit('mesa-split: the primary exports no table')
|
||||
# The primary, its placeholders imported from the trampolines' module.
|
||||
entries = bytearray()
|
||||
for mod, base, kind, t, raw in imports:
|
||||
if mod == PLACEHOLDER:
|
||||
m, nm = TRAMP.encode(), f'{TRAMP}_{base}'.encode()
|
||||
raw = uleb(len(m)) + m + uleb(len(nm)) + nm + bytes([0]) + uleb(t)
|
||||
entries += raw
|
||||
body = uleb(len(imports)) + entries
|
||||
start, end = imp_at
|
||||
renamed = os.path.join(tmp, 'renamed.wasm')
|
||||
with open(renamed, 'wb') as f:
|
||||
f.write(b[:start] + bytes([2]) + uleb(len(body)) + body + b[end:])
|
||||
# The trampolines.
|
||||
wat = [f'(module (import "gexcold" "load" (func $load))',
|
||||
f' (import "primary" "{table_export}" (table $t 1 funcref))']
|
||||
for k, t in holders:
|
||||
params, results = types[t]
|
||||
sig = ''.join(f' (param {p})' for p in params) + ''.join(f' (result {r})' for r in results)
|
||||
gets = ''.join(f' (local.get {i})' for i in range(len(params)))
|
||||
wat.append(f' (func (export "{TRAMP}_{k}"){sig} (call $load)'
|
||||
f' (call_indirect $t{sig}{gets} (i32.const {k})))')
|
||||
wat.append(')')
|
||||
wat_path = os.path.join(tmp, 'tramp.wat')
|
||||
tramp = os.path.join(tmp, 'tramp.wasm')
|
||||
with open(wat_path, 'w') as f:
|
||||
f.write('\n'.join(wat))
|
||||
subprocess.run([binaryen('wasm-as'), *FEATURES, wat_path, '-o', tramp], check=True)
|
||||
merged = os.path.join(tmp, 'merged.wasm')
|
||||
subprocess.run([binaryen('wasm-merge'), *FEATURES, renamed, 'primary', tramp, TRAMP, '-g',
|
||||
'-o', merged], check=True)
|
||||
subprocess.run([binaryen('wasm-opt'), *FEATURES, merged, '-g', '--remove-exports',
|
||||
'--pass-arg', f'remove-exports@{TRAMP}_*', '-o', out_path], check=True)
|
||||
return len(holders)
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
max_cold = None
|
||||
cold_re = None
|
||||
if '--cold' in args:
|
||||
i = args.index('--cold')
|
||||
cold_re = re.compile(args[i + 1])
|
||||
del args[i:i + 2]
|
||||
if '--max-cold' in args:
|
||||
i = args.index('--max-cold')
|
||||
max_cold = int(args[i + 1])
|
||||
del args[i:i + 2]
|
||||
if len(args) != 3:
|
||||
sys.exit(__doc__)
|
||||
orig, hot_path, out = args
|
||||
os.makedirs(out, exist_ok=True)
|
||||
primary = os.path.join(out, 'gex-wgpu-replay.wasm')
|
||||
cold = os.path.join(out, 'gex-wgpu-replay.deferred.wasm')
|
||||
if os.path.exists(cold):
|
||||
os.remove(cold)
|
||||
if hot_path != '-' and not os.path.exists(hot_path):
|
||||
shutil.copyfile(orig, primary)
|
||||
print(f'mesa-split: no {hot_path}; not split')
|
||||
return
|
||||
|
||||
hot = None if hot_path == '-' else {l.strip() for l in open(hot_path) if l.strip() and not l.startswith('#')}
|
||||
fmap = subprocess.run([binaryen('wasm-opt'), *FEATURES, orig, '--print-function-map', '-o', os.devnull],
|
||||
capture_output=True, text=True, check=True).stdout
|
||||
# `index:name`, imports included (wasm-split never moves those).
|
||||
names = [l.split(':', 1)[1] for l in fmap.splitlines() if ':' in l]
|
||||
if hot is not None:
|
||||
hot = {stable(n) for n in hot}
|
||||
keep = [n for n in names if (hot is None or stable(n) in hot)
|
||||
and not (cold_re and cold_re.search(n))]
|
||||
with tempfile.NamedTemporaryFile('w', suffix='.txt', delete=False) as f:
|
||||
f.write(''.join(n + '\n' for n in keep))
|
||||
keep_file = f.name
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
raw_cold = os.path.join(tmp, 'cold.wasm')
|
||||
raw_primary = os.path.join(tmp, 'primary.wasm')
|
||||
subprocess.run([binaryen('wasm-split'), orig, *FEATURES, '--split', '-g', '-q',
|
||||
'--keep-funcs', '@' + keep_file, '-o1', raw_primary, '-o2', raw_cold],
|
||||
check=True)
|
||||
slots = trampolines(raw_primary, primary, tmp)
|
||||
if not slots:
|
||||
shutil.copyfile(raw_primary, primary)
|
||||
subprocess.run([binaryen('wasm-opt'), *FEATURES, raw_cold, '--strip-debug', '-o', cold], check=True)
|
||||
os.remove(keep_file)
|
||||
size = os.path.getsize(cold)
|
||||
print(f'mesa-split: {len(keep)} of {len(names)} functions kept; '
|
||||
f'{os.path.getsize(primary)} bytes + {size} cold; {slots} trampolines')
|
||||
if hot is not None:
|
||||
gone = hot - {stable(n) for n in names}
|
||||
if gone:
|
||||
# A listed function this build no longer has under that name (a
|
||||
# rename leaves the new one cold): re-run tools/mesa-hot.py.
|
||||
print(f'mesa-split: {len(gone)} listed names not in this build, e.g. {sorted(gone)[0][:120]}')
|
||||
if max_cold is not None and size >= max_cold:
|
||||
sys.exit(f'mesa-split: the cold part is {size} bytes, not under {max_cold}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+2520
-2520
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user