mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
Merge 2ddb25ce into integ
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01PeRutwFgZ66XpxjasKrPn1 # Conflicts: # shims/browser/js/worker.mjs # web/src/runtime.js
This commit is contained in:
commit
b7fead079d
8 files changed
+135
-6
No files matched your search
@@ -568,7 +568,7 @@ pub fn build(dir: &Path, project: &Project, config: &Config) -> Result<PeReport>
|
||||
// The web build's units, when the project has a browser runtime.
|
||||
if dir.join(WEB_NT_DIR).join(WEB_NT_WASM[0]).exists() {
|
||||
let stateful: Vec<(String, PathBuf)> = config.providers.iter().map(|p| (p.library.clone(), provider_wasm(dir, p))).collect();
|
||||
build_web(dir, &ntss, project, &units, &web_inputs, &stateful, class32, jobs, &mut report)?;
|
||||
build_web(dir, &ntss, project, config.args.as_deref(), &units, &web_inputs, &stateful, class32, jobs, &mut report)?;
|
||||
std::env::set_var("GEX_CGEN_CACHE", &units);
|
||||
}
|
||||
let manifest = BuildManifest {
|
||||
@@ -616,6 +616,8 @@ fn build_web(
|
||||
dir: &Path,
|
||||
ntss: &NtSession,
|
||||
project: &Project,
|
||||
// user/eject.json's "args", over the recorded ones (as `eject run`).
|
||||
args: Option<&[String]>,
|
||||
native: &Path,
|
||||
inputs: &[(String, u64, Vec<u8>, Vec<u64>)],
|
||||
stateful: &[(String, PathBuf)],
|
||||
@@ -734,7 +736,7 @@ fn build_web(
|
||||
("exe", s(&ntss.exe)),
|
||||
("prefix", s(&ntss.prefix)),
|
||||
("cwd", s(project.program.cwd.as_deref().unwrap_or("/"))),
|
||||
("args", strs(&ntss.args)),
|
||||
("args", strs(args.unwrap_or(&ntss.args))),
|
||||
("env", strs(&ntss.env)),
|
||||
("settings", strs(&settings)),
|
||||
("gl", b(ntss.gl)),
|
||||
|
||||
@@ -831,6 +831,72 @@ and the compositor. Paths: project root `~/gex-sandbox`, scratch
|
||||
multi-threaded loading. Then the compositor items only if a profile
|
||||
puts the worker back on the critical path.
|
||||
|
||||
- **Post-merge check (site19, master 5ab54ab2 merged, keys unchanged: 38
|
||||
units present, 0 built; p-noita17's user/eject.json now has `"args": []`,
|
||||
which the web build honours since this commit's sibling):** ABBA on edger
|
||||
ejected 57.2, 58.6 vs pewasm 46.0, 45.0 = **1.27x**, world-up 49 s after
|
||||
the process start. Multi-threaded loading is the project's setting
|
||||
(user/eject.json `args`), not an eject-site default: eject-site is
|
||||
generic, and `--args` stays the override.
|
||||
- Start-up profiles from the process start for both runtimes:
|
||||
`$S/pw-prof-start.sh TAG AFTER SECS` (pewasm, F's browser-prof.mjs) and
|
||||
`prof-edger.sh SITE TAG 1 60` (page-run now profiles Workers that start
|
||||
during the window too).
|
||||
|
||||
- **Queued (user decision, after the start-up work): no WebGL in the
|
||||
browser.** Port the Wayland compositor (web/src/compositor-worker.js, and
|
||||
dom-compositor.js if still live) from WebGL2 to WebGPU on the GPUDevice
|
||||
Mesa uses: GL windows become textures the compositor samples directly
|
||||
(no cross-API copy, no second context, no webgl2 start-up retry, no
|
||||
overlay canvas), wl_shm rows uploaded with queue.writeTexture from guest
|
||||
memory; keep the frame-skip semantics (no present when nothing changed).
|
||||
web/src/gl.js and the demos' WebGL path are being removed on master by
|
||||
the coordinator.
|
||||
- **Start-up profile from the process start (site19, prof-gst):** every
|
||||
guest thread's Worker spent 2.5-3.7 s in `loadFile` (synchronous
|
||||
requests through the service worker) plus 0.5-1.5 s compiling modules:
|
||||
`loadUnit` fetched each unit's c.wasm (131 MiB for noita.exe) only to
|
||||
read its entries, though the Module came from a sibling. Fix 065139bb:
|
||||
the bytes are fetched lazily, only by a Worker that compiles. A/B
|
||||
site19/site20 queued (`$S/edger-wab3.txt`).
|
||||
|
||||
- **Start-up: threads recompiled every unit (fixed, 35578368).** A
|
||||
per-second timeline of the start-up profile (`$S/profline.mjs`,
|
||||
`$S/profimg.mjs`) showed Noita's loader threads starting one after the
|
||||
other, each busy ~3 s while the rest (main included) waited in the ConcRT
|
||||
condvar provider: each new thread's Worker fetched and compiled all 39
|
||||
units (352 `[unit]` lines in one run). host.mjs's thread start path
|
||||
never sent the process's compiled modules (the `start` closure that did
|
||||
is dead code). With them: 44 unit compiles per run. **World-up after
|
||||
navigation (edger, A/B x3): site19 50.6-51.6 s -> site21 36.5-40.6 s**;
|
||||
pewasm 30.2 s. ABBA (site21): ejected 56.8, 56.8 vs pewasm 48.3, 48.0 =
|
||||
1.18x; world-up 35-36 s after the process start. The lazy c.wasm fetch
|
||||
(065139bb) alone measured nothing; it matters now that threads have the
|
||||
Modules (no bytes fetched at all).
|
||||
- pewasm's own start-up timeline (`$S/wup-pewasm2.sh`, timestamped
|
||||
console): process ~4.3 s, first surface 4.7 s, its input frames 60/120/180
|
||||
at 10.6/11.0/11.3 s, world 29.2 s: about 6 s of init and 18 s of world
|
||||
generation. Ours before the fix: init ~20 s (the serial thread starts),
|
||||
world generation ~16 s.
|
||||
|
||||
- **Unit preload (a2… see log, eject page `?preload=`):** the page
|
||||
compiles every unit with `compileStreaming` while the bundle downloads
|
||||
and hands the Modules to every Worker (`preModules`); no Worker fetches
|
||||
or compiles a unit (0 `[unit]` lines). World-up A/B x3: site21 37.5-38.5
|
||||
s -> site22 35.6-38.8 s (mean 38.2 -> 36.7). pewasm 30.2 s.
|
||||
- **Where the remaining ~6.5 s is (site21, per-second timeline, process
|
||||
time):** boot 0-5 s on the main thread (now mostly gone with the
|
||||
preload: 40% loadFile, 10% compile; 17% gex_key sha256 remains);
|
||||
shader translation in the compositor worker (Tint SPIR-V->WGSL) at
|
||||
9-12 s and 40-43 s, during which Noita's main thread waits on GL
|
||||
(~3 s each), so the menus run at ~40 fps where pewasm's run at ~170
|
||||
(its clicks at frames 60/120/180 land within 0.7 s, ours within ~3 s);
|
||||
world generation 19-40 s on one thread, guest code plus spin-yields
|
||||
(~19k NtYieldExecution per 10 s; 0x1d80 guest 7.6-9.9 s of 10), against
|
||||
~18 s in pewasm. Levers: a persistent SPIR-V->WGSL cache (IndexedDB,
|
||||
helps every load after the first), the shader translation off the
|
||||
critical path, and world generation (F, codegen).
|
||||
|
||||
- Harness: `$S/play.mjs` = page-run.mjs + `--script FILE` (timed clicks,
|
||||
keys, holds, screenshots, evals, `persist` snapshots and reloads through
|
||||
CDP input, as a person would).
|
||||
|
||||
@@ -2421,7 +2421,7 @@ export class Gex {
|
||||
if (mod) return mod;
|
||||
const tl = performance.now();
|
||||
got ??= this.#env.loadUnit?.(key) ?? null;
|
||||
if (!got) throw new Error(`gex-cgen: unit ${key} was published but cannot be loaded here`);
|
||||
if (!got?.wasm) throw new Error(`gex-cgen: unit ${key} was published but cannot be loaded here`);
|
||||
const t0 = performance.now();
|
||||
mod = got.wasm instanceof WebAssembly.Module ? got.wasm : new WebAssembly.Module(got.wasm);
|
||||
this.#compileMs += performance.now() - t0;
|
||||
|
||||
@@ -348,9 +348,21 @@ async function handle(msg) {
|
||||
// with a synchronous request -- allowed in a Worker, and what lets the
|
||||
// nested callback loop install one too. Null when there is none.
|
||||
loadUnit(key) {
|
||||
const wasm = this.loadFile(`${key}/c.wasm`);
|
||||
const entries = wasm && this.loadFile(`${key}/c.entries`);
|
||||
return wasm && entries ? { wasm, entries } : null;
|
||||
// The entries now; the module's bytes only if this Worker compiles it
|
||||
// (most get the Module from a sibling: see `Gex#cgenModule`). Fetching
|
||||
// every unit's bytes in every thread's Worker, only to use a shared
|
||||
// Module, cost each new guest thread seconds at start-up.
|
||||
const entries = this.loadFile(`${key}/c.entries`);
|
||||
if (!entries) return null;
|
||||
const env = this;
|
||||
let wasm;
|
||||
return {
|
||||
entries,
|
||||
get wasm() {
|
||||
wasm ??= env.loadFile(`${key}/c.wasm`);
|
||||
return wasm;
|
||||
},
|
||||
};
|
||||
}
|
||||
// Any file of the unit cache by its relative path (an export
|
||||
// provider's module: `providers/<name>-<fingerprint>.wasm`), or null.
|
||||
@@ -414,6 +426,7 @@ async function handle(msg) {
|
||||
if (msg.cgen?.mode) gex.useCgen(msg.cgen.mode);
|
||||
// Any sibling modules the host already had when this Worker started.
|
||||
for (const [key, mod] of msg.sharedModules ?? []) gex.addSharedModule(key, mod);
|
||||
for (const [key, mod] of msg.preModules ?? []) gex.addSharedModule(key, mod);
|
||||
if (globalThis.GEX_SHARE_TRACE || msg.shareTrace) console.warn(`[share] ${msg.kind} start with ${(msg.sharedModules ?? []).length} shared modules (${(msg.sharedModules ?? []).filter(([k]) => String(k).startsWith('cgen:')).length} units)`);
|
||||
|
||||
// The GL device (gles-gears): the page's GlDevice, reached over a ring in a
|
||||
|
||||
@@ -83,6 +83,7 @@ walk(join(project, 'runtime/web/page'), (p, rel) => place(p, rel));
|
||||
const cgen = nt.cgen;
|
||||
const units = join(project, cgen);
|
||||
let unitBytes = 0;
|
||||
const preload = [];
|
||||
// Only this build's units: a unit cache keeps every key it ever built
|
||||
// (a re-keyed rebuild leaves the old ones). build/build.json names the
|
||||
// images' units; the NT trampolines' unit is not an image, and is kept when
|
||||
@@ -108,8 +109,17 @@ walk(units, (p, rel) => {
|
||||
if (!want) return;
|
||||
unitBytes += statSync(p).size;
|
||||
place(p, join('project', cgen, rel));
|
||||
// What the page compiles while the bundle downloads (nt.html ?preload=):
|
||||
// module names as gex.mjs keys them.
|
||||
const url = `/project/${cgen}/${rel}`;
|
||||
if (name === 'c.wasm') preload.push([`cgen:${rel.slice(0, -'/c.wasm'.length)}`, url]);
|
||||
else if (top === 'providers') preload.push([`cgenfile:${rel}`, url]);
|
||||
});
|
||||
|
||||
// Largest first: noita.exe's unit is most of the compile.
|
||||
preload.sort((a, b) => (files[b[1]]?.size ?? 0) - (files[a[1]]?.size ?? 0));
|
||||
writeFileSync(join(out, 'project', cgen, 'preload.json'), JSON.stringify(preload));
|
||||
|
||||
// The bundle, without what --drop names, with --overlay DIR's files (guest
|
||||
// paths under DIR, as user/files) laid over it.
|
||||
const drops = opts('--drop');
|
||||
|
||||
@@ -77,6 +77,8 @@
|
||||
cgenurl: `/project/${m.cgen}`,
|
||||
size: q.get('size') ?? m.size ?? fit,
|
||||
aot: '1',
|
||||
// Compile the units while the bundle unpacks (eject-site's list).
|
||||
preload: `/project/${m.cgen}/preload.json`,
|
||||
});
|
||||
if (m.cwd && m.cwd !== '/') p.set('cwd', m.cwd);
|
||||
if (m.unguarded) p.set('unguarded', '1');
|
||||
|
||||
@@ -72,6 +72,31 @@ async function fetchShared(url) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Compile the units a preload list names, a few at a time, streaming from
|
||||
* the network (or the service worker's cache); null if the list is gone. */
|
||||
async function preloadUnits(url) {
|
||||
let list;
|
||||
try {
|
||||
list = await (await fetch(url)).json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const out = [];
|
||||
let i = 0;
|
||||
const one = async () => {
|
||||
while (i < list.length) {
|
||||
const [name, href] = list[i++];
|
||||
try {
|
||||
out.push([name, await WebAssembly.compileStreaming(fetch(href))]);
|
||||
} catch (e) {
|
||||
console.warn(`[nt-page] preload ${href}: ${e?.message ?? e}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
await Promise.all([one(), one(), one(), one()]);
|
||||
return out;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// `?trapstack=1`: a trap's wasm frames, which name the shim function it
|
||||
// was in (runtime.js passes the switch to every Worker).
|
||||
@@ -96,6 +121,9 @@ async function main() {
|
||||
canvas.style.height = `${h}px`;
|
||||
state.phase = 'fetching';
|
||||
mark('fetching');
|
||||
// `preload=URL`: the units to compile while the bundle downloads (a JSON
|
||||
// list of [module name, url]); every Worker then starts with them.
|
||||
const preloading = q.get('preload') ? preloadUnits(q.get('preload')) : Promise.resolve(null);
|
||||
// `tar=a.tar,b.tar`: bundles laid over one another, later ones winning --
|
||||
// a small test program over a large cached bundle of its libraries.
|
||||
const trees = [];
|
||||
@@ -142,6 +170,8 @@ async function main() {
|
||||
if (!env.some((e) => e.startsWith('GEX_WINE_PREFIX='))) env.push('GEX_WINE_PREFIX=/wine');
|
||||
if (!env.some((e) => e.startsWith('GEX_NT_STRICT='))) env.push('GEX_NT_STRICT=1');
|
||||
const gl = q.get('gl') === '1';
|
||||
const preModules = await preloading;
|
||||
if (preModules) mark(`compiled ${preModules.length} unit modules ahead`);
|
||||
state.phase = 'running';
|
||||
mark('running');
|
||||
if (persistDir) {
|
||||
@@ -167,6 +197,7 @@ async function main() {
|
||||
wl: canvas,
|
||||
wlInput: canvas,
|
||||
hostX: q.get('hostx') === '1',
|
||||
preModules,
|
||||
});
|
||||
state.status = status;
|
||||
state.phase = 'done';
|
||||
|
||||
@@ -440,6 +440,10 @@ export async function runProgram(
|
||||
// process of the same program starts at its own entry with every
|
||||
// relocation already applied.
|
||||
link = Number(globalThis.GEX_LINK ?? 0),
|
||||
// Function-tier modules compiled ahead of the run (an ejected project's
|
||||
// page compiles its units while the bundle downloads), as [name, Module]
|
||||
// pairs every Worker takes instead of fetching and compiling its own.
|
||||
preModules = null,
|
||||
},
|
||||
) {
|
||||
const { shimBytes, seedBytes } = await loadRuntime();
|
||||
@@ -661,6 +665,7 @@ export async function runProgram(
|
||||
traceMisses: !!globalThis.GEX_TRACE_MISSES,
|
||||
// `?sharetrace=1`: say how compiled modules are shared between a process's Workers.
|
||||
shareTrace: flag('sharetrace') ? 1 : undefined,
|
||||
preModules: preModules ?? undefined,
|
||||
// `?forkskip=N`, in `extra` because the process that acts on it is a
|
||||
// forked child, not the page's own.
|
||||
forkSkip: typeof globalThis.GEX_FORK_SKIP_MIB === 'number'
|
||||
|
||||
Reference in new issue
Block a user