diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4b439af1..b11341f5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,6 +167,11 @@ jobs: - name: Integration tests (headless Node) run: node --test "crates/bashkit-wasm/__test__/*.test.mjs" + - name: Browser compatibility and transfer-size gates + run: | + node crates/bashkit-wasm/bench/compat.mjs + node crates/bashkit-wasm/bench/artifact.mjs crates/bashkit-wasm/pkg --check + audit: name: Audit runs-on: ubuntu-latest diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index a178a4945..0c0fd99f7 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -72,6 +72,11 @@ jobs: - name: Integration tests (headless Node) run: node --test "crates/bashkit-wasm/__test__/*.test.mjs" + - name: Browser compatibility and transfer-size gates + run: | + node crates/bashkit-wasm/bench/compat.mjs + node crates/bashkit-wasm/bench/artifact.mjs crates/bashkit-wasm/pkg --check + - name: Upload pkg uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: diff --git a/Cargo.toml b/Cargo.toml index 2aad79c80..a93c54942 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -209,6 +209,14 @@ codegen-units = 1 panic = "unwind" strip = "symbols" +# Browser cold start is transfer-bound. Keep native release throughput settings; +# use size-oriented LLVM optimization and whole-program LTO only for the JS-host +# WASM package. Retain release's panic policy and all interpreter safety checks. +[profile.wasm-release] +inherits = "release" +opt-level = "s" +lto = "fat" + # Important decision: separate `bench` profile uses fat LTO for the most # aggressive cross-crate inlining. Criterion benches run rarely and we'd # rather spend extra compile time once than miss inlining opportunities diff --git a/crates/bashkit-wasm/README.md b/crates/bashkit-wasm/README.md index 9a16cba49..eb16047e7 100644 --- a/crates/bashkit-wasm/README.md +++ b/crates/bashkit-wasm/README.md @@ -294,3 +294,29 @@ just build-wasm ## License MIT, part of the [Bashkit](https://github.com/everruns/bashkit) project. + +## Deployment and cold start + +Serve `bashkit_wasm_bg.wasm` as `application/wasm` with Brotli or gzip HTTP +compression. Raw npm file size is not transfer size. Use content-hashed asset +URLs with long-lived immutable caching; keep the JS glue and WASM from the same +build together. `initBashkit()` uses wasm-bindgen's streaming loader when the +server sends the correct MIME type. A slow mobile connection can take more than +15 seconds to download a multi-megabyte bundle, so choose startup deadlines from +your application's measured network conditions. + +The package includes `build-info.json`: source commit, dirty-checkout status, +compiler/tools, Cargo profile/features, and the SHA-256 of the shipped WASM. +Source archives without Git metadata record a null source identity. A source +checkout between releases can have the same package version as an older npm +release; compare the source commit as well as the version. + +Maintainers can run the bounded compatibility and compressed-size gates: + +```sh +node crates/bashkit-wasm/bench/compat.mjs +node crates/bashkit-wasm/bench/artifact.mjs crates/bashkit-wasm/pkg --check +``` + +The [browser benchmark methodology](https://github.com/everruns/bashkit/blob/main/crates/bashkit-wasm/bench/README.md) describes reproducible +cold-start and execution experiments and the measured profile tradeoff. diff --git a/crates/bashkit-wasm/__test__/artifact.test.mjs b/crates/bashkit-wasm/__test__/artifact.test.mjs new file mode 100644 index 000000000..693977ae1 --- /dev/null +++ b/crates/bashkit-wasm/__test__/artifact.test.mjs @@ -0,0 +1,33 @@ +// A tiny valid WASM fixture keeps CLI/budget regression coverage bounded. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; + +test("artifact CLI accepts --check with default package and enforces budgets", () => { + const root = mkdtempSync(join(tmpdir(), "bashkit-artifact-")); + try { + const bench = join(root, "bench"), pkg = join(root, "pkg"); + mkdirSync(bench); mkdirSync(pkg); + const script = join(bench, "artifact.mjs"); + copyFileSync(new URL("../bench/artifact.mjs", import.meta.url), script); + writeFileSync(join(pkg, "index.js"), "export {};"); + writeFileSync(join(pkg, "bashkit_wasm.js"), "export {};"); + writeFileSync(join(pkg, "bashkit_wasm_bg.wasm"), Buffer.from([0, 97, 115, 109, 1, 0, 0, 0])); + const budget = join(bench, "size-budget.json"); + writeFileSync(budget, '{"raw":1000,"gzip":1000,"brotli":1000}'); + const run = args => spawnSync(process.execPath, [script, ...args], { encoding: "utf8", timeout: 10000 }); + const result = run(["--check"]); + assert.equal(result.status, 0, result.stderr); + assert.equal(JSON.parse(result.stdout).total.raw, 28); + const explicit = run([pkg, "--check"]); + assert.equal(explicit.status, 0, explicit.stderr); + assert.deepEqual(JSON.parse(explicit.stdout).total, JSON.parse(result.stdout).total); + writeFileSync(budget, '{"raw":27}'); + const rejected = run(["--check"]); + assert.notEqual(rejected.status, 0); + assert.match(rejected.stderr, /28 exceeds 27 byte budget/); + } finally { rmSync(root, { recursive: true, force: true }); } +}); diff --git a/crates/bashkit-wasm/__test__/package.test.mjs b/crates/bashkit-wasm/__test__/package.test.mjs new file mode 100644 index 000000000..c3d22900e --- /dev/null +++ b/crates/bashkit-wasm/__test__/package.test.mjs @@ -0,0 +1,33 @@ +// Exercise the archive's public contract, not just an unpacked build directory. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { Terminal } from "../pkg/index.js"; +const pkg = new URL("../pkg/", import.meta.url); +const read = name => readFileSync(new URL(name, pkg)); + +test("package records verifiable source and artifact identity", () => { + const info = JSON.parse(read("build-info.json")); + if (info.sourceRevision === null) { + assert.equal(info.sourceDirty, null); + assert.notEqual(process.env.GITHUB_ACTIONS, "true", "CI/release must record Git identity"); + } else { + assert.match(info.sourceRevision, /^[0-9a-f]{40}$/); + assert.equal(typeof info.sourceDirty, "boolean"); + assert.equal(info.sourceRevision, execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim()); + } + assert.equal(info.version, JSON.parse(read("package.json")).version); + assert.equal(info.wasmSha256, createHash("sha256").update(read("bashkit_wasm_bg.wasm")).digest("hex")); + assert.equal(info.profile, "wasm-release"); + assert.equal(info.features.includes("terminal"), Terminal !== undefined); +}); + +test("npm archive contains every runtime file and build identity", () => { + const [archive] = JSON.parse(execFileSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], { + cwd: pkg, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 30000, + })); + const files = new Set(archive.files.map(file => file.path)); + for (const name of ["index.js", "index.d.ts", "bashkit_wasm.js", "bashkit_wasm.d.ts", "bashkit_wasm_bg.wasm", "build-info.json"]) assert.ok(files.has(name), `archive lacks ${name}`); +}); diff --git a/crates/bashkit-wasm/bench/README.md b/crates/bashkit-wasm/bench/README.md new file mode 100644 index 000000000..a15c587b1 --- /dev/null +++ b/crates/bashkit-wasm/bench/README.md @@ -0,0 +1,47 @@ +# Browser package experiments + +These experiments complement the full package integration suite. Run from the +repository root after `just build-wasm`. They require GNU Bash 5+ on `PATH` (on +macOS, `/bin/bash` 3.2 is unsuitable) and Node 22+. + +- `node crates/bashkit-wasm/bench/compat.mjs [pkg ...]`: 24 deterministic, + self-contained shell cases against a fresh GNU Bash oracle and fresh Bashkit + instances. Exact stdout bytes and status; stderr is not scored. Worker lifetime + is capped at 30 seconds; each oracle process at two seconds. One package is a + pass/fail CI gate; multiple packages produce a comparison without failing for + an old release's known gaps. This is a regression sample, not a population + compatibility percentage. No corpus or package is fetched by the gate. +- `node crates/bashkit-wasm/bench/artifact.mjs [pkg] [--check]`: raw, gzip-9, + Brotli-11 bytes for the actual JS + WASM load graph, SHA-256 identities, and + WASM code/data/other sections. `--check` applies `size-budget.json`, with about + 4-5% headroom above the selected shipping profile. Review new measurements + before deliberately raising a budget. No wall-clock threshold in CI. +- `node crates/bashkit-wasm/bench/runtime.mjs pkg`: arithmetic, substitution, + jq, and text pipelines; one warmup and seven measured fresh-instance runs. + Output is checked for status and consistency. Compare on the same host, + without competing builds. Times from a busy development host are exploratory. +- `TRIALS=5 node crates/bashkit-wasm/bench/cold-start.mjs pkg-before pkg-after`: + Chromium, Brotli-11 server, fresh HTTP cache per trial, rotated package order, + loopback plus CDP Fast 4G (1,012,500 B/s, 165 ms) and Slow 4G (180,000 B/s, + 562.5 ms), page CPU throttled 4x. Requires an existing Playwright installation + and Chrome; `PLAYWRIGHT_MODULE` can name its `index.mjs` and `CHROME_CHANNEL` + selects a browser channel. Trials are limited to 1..20. Every visit must + initialize the real package, execute `echo hi`, drive `Terminal` when present, + and paint output over two animation frames. Each navigation/wait is capped at + 60 seconds; browser contexts and the loopback server close on completion/error. + +The cold-start page intentionally has no framework, renderer, shared terminal +WASM, or worker dependency. It measures the package, not brush-web's complete +navigation-to-prompt path. All execution is on the page; CDP CPU throttling +therefore covers this experiment's interpreter (unlike a worker experiment). +Fresh contexts guarantee cold HTTP caches, but browser-process compiled-code +caches may be shared. Network measurements remain representative; do not infer +physical-phone compile time. The JSON records every resource and milestone, +Chromium/Node versions, bytes, hashes, and raw samples. + +Redirect JSON output to retain a run. Historical investigation results are in +`results/`; these specialized package experiments are linked here rather than +mixed with the native bashkit-bench or Criterion result schemas. + +See also: [package guide](../README.md), +[browser design](../../../knowledge/runtimes/browser-package.md). diff --git a/crates/bashkit-wasm/bench/artifact.mjs b/crates/bashkit-wasm/bench/artifact.mjs new file mode 100644 index 000000000..593997361 --- /dev/null +++ b/crates/bashkit-wasm/bench/artifact.mjs @@ -0,0 +1,42 @@ +// Transfer bytes, not raw bytes alone, determine cold-start network cost. +import { readFileSync, existsSync, realpathSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { createHash } from "node:crypto"; +import { gzipSync, brotliCompressSync, constants } from "node:zlib"; + +export function artifact(pkg) { + const files = ["index.js", "bashkit_wasm.js", "bashkit_wasm_bg.wasm"].map(name => { + const bytes = readFileSync(resolve(pkg, name)); + return { name, sha256: createHash("sha256").update(bytes).digest("hex"), raw: bytes.length, + gzip: gzipSync(bytes, { level: 9 }).length, + brotli: brotliCompressSync(bytes, { params: { [constants.BROTLI_PARAM_QUALITY]: 11 } }).length }; + }); + const bytes = readFileSync(resolve(pkg, "bashkit_wasm_bg.wasm")); + let offset = 8; + function leb() { let n = 0, shift = 0, b; do { b = bytes[offset++]; n += (b & 127) * 2 ** shift; shift += 7; } while (b & 128); return n; } + const sections = []; + while (offset < bytes.length) { + const id = bytes[offset++], length = leb(), start = offset; + let name = ({ 10: "code", 11: "data" })[id] ?? `section-${id}`; + if (id === 0) { const n = leb(); name = `custom:${bytes.subarray(offset, offset + n).toString()}`; } + sections.push({ name, raw: length }); + offset = start + length; + } + return { files, total: Object.fromEntries(["raw", "gzip", "brotli"].map(k => [k, files.reduce((n, f) => n + f[k], 0)])), sections }; +} +// Node canonicalizes the module URL, but argv can retain a symlink (e.g. /var +// versus /private/var on macOS). Imports must never run the CLI accidentally. +if (process.argv[1] && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) { + const paths = process.argv.slice(2).filter(arg => arg !== "--check"); + if (paths.length > 1) throw new Error("expected one package directory"); + const pkg = resolve(paths[0] ?? new URL("../pkg/", import.meta.url).pathname); + const report = artifact(pkg); + console.log(JSON.stringify(report, null, 2)); + if (process.argv.includes("--check")) { + const budgets = JSON.parse(readFileSync(new URL("./size-budget.json", import.meta.url))); + for (const [kind, budget] of Object.entries(budgets)) { + if (report.total[kind] > budget) throw new Error(`${kind}: ${report.total[kind]} exceeds ${budget} byte budget`); + } + } +} diff --git a/crates/bashkit-wasm/bench/cold-start.mjs b/crates/bashkit-wasm/bench/cold-start.mjs new file mode 100644 index 000000000..d749e49c8 --- /dev/null +++ b/crates/bashkit-wasm/bench/cold-start.mjs @@ -0,0 +1,85 @@ +// Manual browser experiment. CI gates deterministic bytes/semantics, not noisy wall time. +// Supply PLAYWRIGHT_MODULE when Playwright is installed outside this workspace. +import { createServer } from "node:http"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { brotliCompressSync, constants } from "node:zlib"; +import { artifact } from "./artifact.mjs"; + +const { chromium } = await import(process.env.PLAYWRIGHT_MODULE ?? "playwright"); +const packages = process.argv.slice(2).map(resolvePath => resolve(resolvePath)); +if (!packages.length) throw new Error("pass package directories to compare"); +const trials = Number(process.env.TRIALS ?? 5); +if (!Number.isInteger(trials) || trials < 1 || trials > 20) throw new Error("TRIALS must be 1..20"); +const conditions = { + local: null, + fast4g: { downloadThroughput: 1012500, uploadThroughput: 168750, latency: 165 }, + slow4g: { downloadThroughput: 180000, uploadThroughput: 84375, latency: 562.5 }, +}; +const routes = new Map(); +packages.forEach((pkg, i) => { + for (const file of ["index.js", "bashkit_wasm.js", "bashkit_wasm_bg.wasm"]) { + routes.set(`/${i}/${file}`, { type: file.endsWith("wasm") ? "application/wasm" : "text/javascript", + bytes: brotliCompressSync(readFileSync(resolve(pkg, file)), { params: { [constants.BROTLI_PARAM_QUALITY]: 11 } }) }); + } + const html = `
`; + routes.set(`/${i}/`, { type: "text/html", bytes: Buffer.from(html), plain: true }); +}); +const server = createServer((req, res) => { + const route = routes.get(req.url); + if (!route) { res.writeHead(404); res.end(); return; } + res.writeHead(200, { "Content-Type": route.type, "Content-Length": route.bytes.length, + "Cache-Control": "public, max-age=31536000, immutable", "Vary": "Accept-Encoding", + ...(route.plain ? {} : { "Content-Encoding": "br" }) }); + res.end(route.bytes); +}); +await new Promise(r => server.listen(0, "127.0.0.1", r)); +let browser; +try { + browser = await chromium.launch({ channel: process.env.CHROME_CHANNEL ?? "chrome", headless: true }); + const samples = []; + for (const [condition, throttle] of Object.entries(conditions)) { + for (let trial = 0; trial < trials; trial++) { + const order = packages.map((_, i) => (i + trial) % packages.length); + if (trial % 2) order.reverse(); + for (const i of order) { + const context = await browser.newContext(); // Cold HTTP cache per sample. + try { + const page = await context.newPage(); + const session = await context.newCDPSession(page); + await session.send("Network.enable"); + if (throttle) await session.send("Network.emulateNetworkConditions", { offline: false, ...throttle }); + await session.send("Emulation.setCPUThrottlingRate", { rate: 4 }); + const errors = []; page.on("pageerror", e => errors.push(String(e))); + await page.goto(`http://127.0.0.1:${server.address().port}/${i}/`, { waitUntil: "domcontentloaded", timeout: 60000 }); + await page.waitForFunction(() => window.result, null, { timeout: 60000 }); + const result = await page.evaluate(() => window.result); + if (errors.length) throw new Error(errors.join("\n")); + samples.push({ package: i, condition, trial, ...result }); + console.error(`${condition} ${trial + 1}/${trials} package ${i}: ${result.marks.prompt.toFixed(1)}ms`); + } finally { await context.close(); } + } + } + } + console.log(JSON.stringify({ method: "Cold context, Brotli 11, CDP Fast/Slow 4G, page CPU 4x; main-thread JS-host WASM, no terminal renderer dependency; prompt after first echo and two animation frames", browser: browser.version(), node: process.version, + packages: packages.map(pkg => ({ path: pkg, ...artifact(pkg) })), samples }, null, 2)); +} finally { await browser?.close(); await new Promise(r => server.close(r)); } diff --git a/crates/bashkit-wasm/bench/compat-cases.json b/crates/bashkit-wasm/bench/compat-cases.json new file mode 100644 index 000000000..490982c34 --- /dev/null +++ b/crates/bashkit-wasm/bench/compat-cases.json @@ -0,0 +1,26 @@ +[ + {"name":"quoted array", "script":"a=('one two' three); printf '<%s>\\n' \"${a[@]}\""}, + {"name":"sparse array", "script":"a=([2]=two [7]=seven); printf '%s\\n' \"${!a[@]}\" \"${#a[@]}\""}, + {"name":"array slice", "script":"a=(a b c d e); printf '%s\\n' \"${a[@]: -2}\""}, + {"name":"associative array", "script":"declare -A a=([key]='two words'); printf '%s\\n' \"${a[key]}\""}, + {"name":"nameref assignment", "script":"a=old; declare -n r=a; r=new; echo \"$a $r\""}, + {"name":"nameref array", "script":"a=(x y z); declare -n r=a; echo \"${r[1]} ${#r[@]}\""}, + {"name":"case conversion", "script":"a='hEllo World'; echo \"${a^^} ${a,,}\""}, + {"name":"pattern replacement", "script":"a=abcabc; echo \"${a//a/X} ${a#*b} ${a%%b*}\""}, + {"name":"indirect expansion", "script":"a=hello; r=a; echo \"${!r}\""}, + {"name":"dynamic scope", "script":"a=outer; g() { echo \"$a\"; }; f() { local a=inner; g; }; f; echo \"$a\""}, + {"name":"arithmetic array", "script":"a=(4 5); ((a[1]+=3)); echo \"${a[1]}\""}, + {"name":"brace sequence", "script":"printf '%s\\n' {03..09..3}"}, + {"name":"command substitution", "script":"a=$(printf 'x\\n\\n'); printf '<%s>\\n' \"$a\""}, + {"name":"subshell isolation", "script":"a=parent; (a=child; echo \"$a\"); echo \"$a\""}, + {"name":"process substitution", "script":"read -r a < <(echo hello); echo \"$a\""}, + {"name":"here string", "script":"read -r a b <<< 'one two'; echo \"$b $a\""}, + {"name":"pipefail", "script":"set -o pipefail; false | true; echo $?"}, + {"name":"errexit exception", "script":"set -e; false && echo no; echo yes"}, + {"name":"case fallthrough", "script":"case x in x) echo a ;;& y) echo no ;; *) echo b ;; esac"}, + {"name":"regex capture", "script":"[[ abc123 =~ ([a-z]+)([0-9]+) ]]; echo \"${BASH_REMATCH[1]} ${BASH_REMATCH[2]}\""}, + {"name":"mapfile", "script":"mapfile -t a <<< $'one\\ntwo'; printf '<%s>\\n' \"${a[@]}\""}, + {"name":"child shell", "script":"bash -c 'echo child'"}, + {"name":"read delimiter", "script":"IFS=: read -r a b <<< 'one:two'; echo \"$a/$b\""}, + {"name":"negative status", "script":"false"} +] diff --git a/crates/bashkit-wasm/bench/compat.mjs b/crates/bashkit-wasm/bench/compat.mjs new file mode 100644 index 000000000..7f9171889 --- /dev/null +++ b/crates/bashkit-wasm/bench/compat.mjs @@ -0,0 +1,54 @@ +// Fresh instances, exact bytes/status, bounded worker lifetime. GNU Bash is the oracle. +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { execFileSync, spawnSync } from "node:child_process"; +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +if (!isMainThread) { + const { Bash, initBashkit } = await import(pathToFileURL(resolve(workerData.pkg, "index.js"))); + await initBashkit(readFileSync(resolve(workerData.pkg, "bashkit_wasm_bg.wasm"))); + for (const item of workerData.cases) { + const bash = new Bash({ profile: "interactive", maxCommands: 100000, + maxLoopIterations: 10000, maxMemory: 8 * 1024 * 1024, timeoutMs: 1000 }); + try { + const result = await bash.execute(item.script); + parentPort.postMessage({ name: item.name, stdout: Buffer.from(result.stdoutBytes).toString("base64"), status: result.exitCode }); + result.free(); + } catch (error) { + parentPort.postMessage({ name: item.name, error: String(error) }); + } finally { bash.free(); } + } +} else { + const cases = JSON.parse(readFileSync(new URL("./compat-cases.json", import.meta.url))); + const oracle = process.env.BASH ?? "bash"; + const version = execFileSync(oracle, ["--version"], { encoding: "utf8" }).split("\n")[0]; + if (!/version ([5-9]|[1-9][0-9])\./.test(version)) throw new Error("GNU Bash 5+ required"); + const expected = cases.map(item => { + const r = spawnSync(oracle, ["--noprofile", "--norc", "-c", item.script], { timeout: 2000, maxBuffer: 65536 }); + if (r.error || r.signal) throw new Error(`oracle failed: ${item.name}`); + return { stdout: r.stdout.toString("base64"), status: r.status }; + }); + const packages = process.argv.slice(2); + if (!packages.length) packages.push(new URL("../pkg/", import.meta.url).pathname); + const reports = []; + for (const pkg of packages) { + const actual = await new Promise((resolveResults, reject) => { + const results = []; + const worker = new Worker(new URL(import.meta.url), { workerData: { pkg: resolve(pkg), cases } }); + const timer = setTimeout(() => { worker.terminate(); reject(new Error("compatibility worker exceeded 30s")); }, 30000); + worker.on("message", item => results.push(item)); + worker.on("error", reject); + worker.on("exit", code => { clearTimeout(timer); code ? reject(new Error(`worker exited ${code}`)) : resolveResults(results); }); + }); + if (actual.length !== cases.length) throw new Error("incomplete results"); + const results = actual.map((result, i) => { + const pass = result.stdout === expected[i].stdout && result.status === expected[i].status; + return { name: result.name, pass, ...(!pass ? { expected: expected[i], actual: result } : {}) }; + }); + reports.push({ package: resolve(pkg), passed: results.filter(r => r.pass).length, total: cases.length, results }); + } + console.log(JSON.stringify({ oracle: version, reports }, null, 2)); + // Single-package mode is the CI gate; comparisons record old releases' failures. + if (packages.length === 1 && reports[0].passed !== cases.length) process.exitCode = 1; +} diff --git a/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.json b/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.json new file mode 100644 index 000000000..f262816e4 --- /dev/null +++ b/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.json @@ -0,0 +1,2064 @@ +{ + "date": "2026-10-15", + "sourceBase": "8a817bf0", + "toolchain": { + "rust": "1.95.0", + "wasmBindgen": "0.2.128", + "binaryen": "125" + }, + "published": { + "version": "0.18.2", + "gitHead": "567511386572d4c4b9f649ab1da97aad53b9f341", + "integrity": "sha512-WW8cdRvODapGKCtqh3h8foufAgUXjaL/fsd8Wt/qERqgubG024I/HLl5rQJnTQ8Nfj+UJdYBdj0EaTsv5s/vig==" + }, + "compat": { + "oracle": "GNU bash, version 5.3.20(1)-release (aarch64-apple-darwin27.0.0)", + "reports": [ + { + "package": "/private/tmp/bashkit-published/package", + "passed": 19, + "total": 24, + "results": [ + { + "name": "quoted array", + "pass": true + }, + { + "name": "sparse array", + "pass": false, + "expected": { + "stdout": "Mgo3CjIK", + "status": 0 + }, + "actual": { + "name": "sparse array", + "stdout": "MAoxCjIK", + "status": 0 + } + }, + { + "name": "array slice", + "pass": false, + "expected": { + "stdout": "ZAplCg==", + "status": 0 + }, + "actual": { + "name": "array slice", + "stdout": "ZCBlCg==", + "status": 0 + } + }, + { + "name": "associative array", + "pass": false, + "expected": { + "stdout": "dHdvIHdvcmRzCg==", + "status": 0 + }, + "actual": { + "name": "associative array", + "stdout": "J3R3bwo=", + "status": 0 + } + }, + { + "name": "nameref assignment", + "pass": true + }, + { + "name": "nameref array", + "pass": true + }, + { + "name": "case conversion", + "pass": true + }, + { + "name": "pattern replacement", + "pass": true + }, + { + "name": "indirect expansion", + "pass": true + }, + { + "name": "dynamic scope", + "pass": true + }, + { + "name": "arithmetic array", + "pass": false, + "expected": { + "stdout": "OAo=", + "status": 0 + }, + "actual": { + "name": "arithmetic array", + "stdout": "NQo=", + "status": 0 + } + }, + { + "name": "brace sequence", + "pass": false, + "expected": { + "stdout": "MDMKMDYKMDkK", + "status": 0 + }, + "actual": { + "name": "brace sequence", + "stdout": "Mwo2CjkK", + "status": 0 + } + }, + { + "name": "command substitution", + "pass": true + }, + { + "name": "subshell isolation", + "pass": true + }, + { + "name": "process substitution", + "pass": true + }, + { + "name": "here string", + "pass": true + }, + { + "name": "pipefail", + "pass": true + }, + { + "name": "errexit exception", + "pass": true + }, + { + "name": "case fallthrough", + "pass": true + }, + { + "name": "regex capture", + "pass": true + }, + { + "name": "mapfile", + "pass": true + }, + { + "name": "child shell", + "pass": true + }, + { + "name": "read delimiter", + "pass": true + }, + { + "name": "negative status", + "pass": true + } + ] + }, + { + "package": "/private/tmp/bashkit-tag-package", + "passed": 19, + "total": 24, + "results": [ + { + "name": "quoted array", + "pass": true + }, + { + "name": "sparse array", + "pass": false, + "expected": { + "stdout": "Mgo3CjIK", + "status": 0 + }, + "actual": { + "name": "sparse array", + "stdout": "MAoxCjIK", + "status": 0 + } + }, + { + "name": "array slice", + "pass": false, + "expected": { + "stdout": "ZAplCg==", + "status": 0 + }, + "actual": { + "name": "array slice", + "stdout": "ZCBlCg==", + "status": 0 + } + }, + { + "name": "associative array", + "pass": false, + "expected": { + "stdout": "dHdvIHdvcmRzCg==", + "status": 0 + }, + "actual": { + "name": "associative array", + "stdout": "J3R3bwo=", + "status": 0 + } + }, + { + "name": "nameref assignment", + "pass": true + }, + { + "name": "nameref array", + "pass": true + }, + { + "name": "case conversion", + "pass": true + }, + { + "name": "pattern replacement", + "pass": true + }, + { + "name": "indirect expansion", + "pass": true + }, + { + "name": "dynamic scope", + "pass": true + }, + { + "name": "arithmetic array", + "pass": false, + "expected": { + "stdout": "OAo=", + "status": 0 + }, + "actual": { + "name": "arithmetic array", + "stdout": "NQo=", + "status": 0 + } + }, + { + "name": "brace sequence", + "pass": false, + "expected": { + "stdout": "MDMKMDYKMDkK", + "status": 0 + }, + "actual": { + "name": "brace sequence", + "stdout": "Mwo2CjkK", + "status": 0 + } + }, + { + "name": "command substitution", + "pass": true + }, + { + "name": "subshell isolation", + "pass": true + }, + { + "name": "process substitution", + "pass": true + }, + { + "name": "here string", + "pass": true + }, + { + "name": "pipefail", + "pass": true + }, + { + "name": "errexit exception", + "pass": true + }, + { + "name": "case fallthrough", + "pass": true + }, + { + "name": "regex capture", + "pass": true + }, + { + "name": "mapfile", + "pass": true + }, + { + "name": "child shell", + "pass": true + }, + { + "name": "read delimiter", + "pass": true + }, + { + "name": "negative status", + "pass": true + } + ] + }, + { + "package": "/private/tmp/bashkit-wasm-startup/crates/bashkit-wasm/pkg", + "passed": 24, + "total": 24, + "results": [ + { + "name": "quoted array", + "pass": true + }, + { + "name": "sparse array", + "pass": true + }, + { + "name": "array slice", + "pass": true + }, + { + "name": "associative array", + "pass": true + }, + { + "name": "nameref assignment", + "pass": true + }, + { + "name": "nameref array", + "pass": true + }, + { + "name": "case conversion", + "pass": true + }, + { + "name": "pattern replacement", + "pass": true + }, + { + "name": "indirect expansion", + "pass": true + }, + { + "name": "dynamic scope", + "pass": true + }, + { + "name": "arithmetic array", + "pass": true + }, + { + "name": "brace sequence", + "pass": true + }, + { + "name": "command substitution", + "pass": true + }, + { + "name": "subshell isolation", + "pass": true + }, + { + "name": "process substitution", + "pass": true + }, + { + "name": "here string", + "pass": true + }, + { + "name": "pipefail", + "pass": true + }, + { + "name": "errexit exception", + "pass": true + }, + { + "name": "case fallthrough", + "pass": true + }, + { + "name": "regex capture", + "pass": true + }, + { + "name": "mapfile", + "pass": true + }, + { + "name": "child shell", + "pass": true + }, + { + "name": "read delimiter", + "pass": true + }, + { + "name": "negative status", + "pass": true + } + ] + } + ] + }, + "profiles": { + "baseline": { + "files": [ + { + "name": "index.js", + "sha256": "1583d806464a708b29a27fa4f9cd2c3299df6e142fe301043a94c30b4b813cae", + "raw": 1503, + "gzip": 828, + "brotli": 673 + }, + { + "name": "bashkit_wasm.js", + "sha256": "3b03202632bfae9333b32b2eebbb69519962ef3e5d0f6ae35546ee67fa1b9fa9", + "raw": 65159, + "gzip": 8704, + "brotli": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "sha256": "f59d5c508f7405c03400a1c46ab1bc7e06fc352c540abfbff6ba197548d9b1b2", + "raw": 9452877, + "gzip": 3560745, + "brotli": 2460299 + } + ], + "total": { + "raw": 9519539, + "gzip": 3570277, + "brotli": 2468453 + }, + "sections": [ + { + "name": "section-1", + "raw": 1008 + }, + { + "name": "section-2", + "raw": 3841 + }, + { + "name": "section-3", + "raw": 8439 + }, + { + "name": "section-4", + "raw": 7 + }, + { + "name": "section-5", + "raw": 3 + }, + { + "name": "section-6", + "raw": 25 + }, + { + "name": "section-7", + "raw": 1819 + }, + { + "name": "section-9", + "raw": 8164 + }, + { + "name": "section-12", + "raw": 2 + }, + { + "name": "code", + "raw": 8279718 + }, + { + "name": "data", + "raw": 1149749 + }, + { + "name": "custom:producers", + "raw": 60 + } + ] + }, + "selected": { + "files": [ + { + "name": "index.js", + "sha256": "1583d806464a708b29a27fa4f9cd2c3299df6e142fe301043a94c30b4b813cae", + "raw": 1503, + "gzip": 828, + "brotli": 673 + }, + { + "name": "bashkit_wasm.js", + "sha256": "15ab168775e5a4623bfb4d0164f34dcdc78f8b73aaa1e0a0063cfc1aafed207d", + "raw": 65168, + "gzip": 8705, + "brotli": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "sha256": "d65414678c9122205486686cd3c1f0296a92129c046229b2bcef9e2d59ff2ea7", + "raw": 7602233, + "gzip": 2963968, + "brotli": 2117510 + } + ], + "total": { + "raw": 7668904, + "gzip": 2973501, + "brotli": 2125682 + }, + "sections": [ + { + "name": "section-1", + "raw": 913 + }, + { + "name": "section-2", + "raw": 3841 + }, + { + "name": "section-3", + "raw": 10594 + }, + { + "name": "section-4", + "raw": 7 + }, + { + "name": "section-5", + "raw": 3 + }, + { + "name": "section-6", + "raw": 25 + }, + { + "name": "section-7", + "raw": 1822 + }, + { + "name": "section-9", + "raw": 8076 + }, + { + "name": "section-12", + "raw": 2 + }, + { + "name": "code", + "raw": 6433520 + }, + { + "name": "data", + "raw": 1143328 + }, + { + "name": "custom:producers", + "raw": 60 + } + ] + }, + "rejectedZ": { + "files": [ + { + "name": "index.js", + "sha256": "1583d806464a708b29a27fa4f9cd2c3299df6e142fe301043a94c30b4b813cae", + "raw": 1503, + "gzip": 828, + "brotli": 673 + }, + { + "name": "bashkit_wasm.js", + "sha256": "91c7a36d8fe29a8bb99b4f0dad6113664aedb95e6f58cf30f40ef6c298f1beb1", + "raw": 65168, + "gzip": 8707, + "brotli": 7479 + }, + { + "name": "bashkit_wasm_bg.wasm", + "sha256": "e532fe6b404c546e50e553947672ed56715cf5b5b854872517a1cb29a693881c", + "raw": 6353397, + "gzip": 2496856, + "brotli": 1843216 + } + ], + "total": { + "raw": 6420068, + "gzip": 2506391, + "brotli": 1851368 + }, + "sections": [ + { + "name": "section-1", + "raw": 1031 + }, + { + "name": "section-2", + "raw": 3841 + }, + { + "name": "section-3", + "raw": 13682 + }, + { + "name": "section-4", + "raw": 7 + }, + { + "name": "section-5", + "raw": 3 + }, + { + "name": "section-6", + "raw": 25 + }, + { + "name": "section-7", + "raw": 1822 + }, + { + "name": "section-9", + "raw": 8335 + }, + { + "name": "section-12", + "raw": 2 + }, + { + "name": "code", + "raw": 5149690 + }, + { + "name": "data", + "raw": 1174857 + }, + { + "name": "custom:producers", + "raw": 60 + } + ] + } + }, + "cold": { + "method": "Cold context, Brotli 11, CDP Fast/Slow 4G, page CPU 4x; main-thread JS-host WASM, no terminal renderer dependency; prompt after first echo and two animation frames", + "browser": "155.0.8059.79", + "node": "v26.5.0", + "packages": [ + { + "path": "/private/tmp/bashkit-baseline-oz", + "files": [ + { + "name": "index.js", + "sha256": "1583d806464a708b29a27fa4f9cd2c3299df6e142fe301043a94c30b4b813cae", + "raw": 1503, + "gzip": 828, + "brotli": 673 + }, + { + "name": "bashkit_wasm.js", + "sha256": "3b03202632bfae9333b32b2eebbb69519962ef3e5d0f6ae35546ee67fa1b9fa9", + "raw": 65159, + "gzip": 8704, + "brotli": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "sha256": "f59d5c508f7405c03400a1c46ab1bc7e06fc352c540abfbff6ba197548d9b1b2", + "raw": 9452877, + "gzip": 3560745, + "brotli": 2460299 + } + ], + "total": { + "raw": 9519539, + "gzip": 3570277, + "brotli": 2468453 + }, + "sections": [ + { + "name": "section-1", + "raw": 1008 + }, + { + "name": "section-2", + "raw": 3841 + }, + { + "name": "section-3", + "raw": 8439 + }, + { + "name": "section-4", + "raw": 7 + }, + { + "name": "section-5", + "raw": 3 + }, + { + "name": "section-6", + "raw": 25 + }, + { + "name": "section-7", + "raw": 1819 + }, + { + "name": "section-9", + "raw": 8164 + }, + { + "name": "section-12", + "raw": 2 + }, + { + "name": "code", + "raw": 8279718 + }, + { + "name": "data", + "raw": 1149749 + }, + { + "name": "custom:producers", + "raw": 60 + } + ] + }, + { + "path": "/private/tmp/bashkit-balanced-profile-oz", + "files": [ + { + "name": "index.js", + "sha256": "1583d806464a708b29a27fa4f9cd2c3299df6e142fe301043a94c30b4b813cae", + "raw": 1503, + "gzip": 828, + "brotli": 673 + }, + { + "name": "bashkit_wasm.js", + "sha256": "15ab168775e5a4623bfb4d0164f34dcdc78f8b73aaa1e0a0063cfc1aafed207d", + "raw": 65168, + "gzip": 8705, + "brotli": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "sha256": "d65414678c9122205486686cd3c1f0296a92129c046229b2bcef9e2d59ff2ea7", + "raw": 7602233, + "gzip": 2963968, + "brotli": 2117510 + } + ], + "total": { + "raw": 7668904, + "gzip": 2973501, + "brotli": 2125682 + }, + "sections": [ + { + "name": "section-1", + "raw": 913 + }, + { + "name": "section-2", + "raw": 3841 + }, + { + "name": "section-3", + "raw": 10594 + }, + { + "name": "section-4", + "raw": 7 + }, + { + "name": "section-5", + "raw": 3 + }, + { + "name": "section-6", + "raw": 25 + }, + { + "name": "section-7", + "raw": 1822 + }, + { + "name": "section-9", + "raw": 8076 + }, + { + "name": "section-12", + "raw": 2 + }, + { + "name": "code", + "raw": 6433520 + }, + { + "name": "data", + "raw": 1143328 + }, + { + "name": "custom:producers", + "raw": 60 + } + ] + } + ], + "samples": [ + { + "package": 0, + "condition": "local", + "trial": 0, + "marks": { + "start": 21.100000023841858, + "imported": 55.39999997615814, + "initialized": 135, + "command": 191.69999992847443, + "prompt": 222.79999995231628 + }, + "resources": [ + { + "name": "index.js", + "start": 22.100000023841858, + "end": 25.100000023841858, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 40.60000002384186, + "end": 43.5, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 57.10000002384186, + "end": 96.89999997615814, + "transfer": 2460599, + "encoded": 2460299 + }, + { + "name": "favicon.ico", + "start": 73.19999992847443, + "end": 74, + "transfer": 300, + "encoded": 0 + } + ] + }, + { + "package": 1, + "condition": "local", + "trial": 0, + "marks": { + "start": 12.899999976158142, + "imported": 37.39999997615814, + "initialized": 100, + "command": 147.29999995231628, + "prompt": 172.20000004768372 + }, + "resources": [ + { + "name": "index.js", + "start": 13.899999976158142, + "end": 17.299999952316284, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 18.399999976158142, + "end": 19.100000023841858, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 39.299999952316284, + "end": 63.700000047683716, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "local", + "trial": 1, + "marks": { + "start": 17.399999976158142, + "imported": 41.89999997615814, + "initialized": 116.29999995231628, + "command": 173.5, + "prompt": 200.5 + }, + "resources": [ + { + "name": "index.js", + "start": 18.899999976158142, + "end": 21.199999928474426, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 22.399999976158142, + "end": 22.899999976158142, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 43.299999952316284, + "end": 74.5, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "local", + "trial": 1, + "marks": { + "start": 18.199999928474426, + "imported": 40.199999928474426, + "initialized": 100.19999992847443, + "command": 148.10000002384186, + "prompt": 173.89999997615814 + }, + "resources": [ + { + "name": "index.js", + "start": 19.199999928474426, + "end": 21.899999976158142, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 22.899999976158142, + "end": 24, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 41.5, + "end": 65.10000002384186, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "local", + "trial": 2, + "marks": { + "start": 20.899999976158142, + "imported": 45.39999997615814, + "initialized": 113.39999997615814, + "command": 168.69999992847443, + "prompt": 196.59999990463257 + }, + "resources": [ + { + "name": "index.js", + "start": 21.899999976158142, + "end": 24.5, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 25.299999952316284, + "end": 25.799999952316284, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 47.39999997615814, + "end": 74.39999997615814, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "local", + "trial": 2, + "marks": { + "start": 17.300000071525574, + "imported": 24, + "initialized": 95.40000009536743, + "command": 141.90000009536743, + "prompt": 168 + }, + "resources": [ + { + "name": "index.js", + "start": 18.200000047683716, + "end": 20.800000071525574, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 21.800000071525574, + "end": 22.600000023841858, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 25.90000009536743, + "end": 61.40000009536743, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "local", + "trial": 3, + "marks": { + "start": 16.100000023841858, + "imported": 39.699999928474426, + "initialized": 106.29999995231628, + "command": 162.19999992847443, + "prompt": 189.89999997615814 + }, + "resources": [ + { + "name": "index.js", + "start": 16.699999928474426, + "end": 19.699999928474426, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 20.899999976158142, + "end": 22.199999928474426, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 41.799999952316284, + "end": 68.5, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "local", + "trial": 3, + "marks": { + "start": 17.09999990463257, + "imported": 39.59999990463257, + "initialized": 94, + "command": 140, + "prompt": 163.89999997615814 + }, + "resources": [ + { + "name": "index.js", + "start": 17.899999976158142, + "end": 20.299999952316284, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 21.5, + "end": 21.899999976158142, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 41, + "end": 62.59999990463257, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "local", + "trial": 4, + "marks": { + "start": 16.700000047683716, + "imported": 40.89999997615814, + "initialized": 105.10000002384186, + "command": 160.39999997615814, + "prompt": 186.39999997615814 + }, + "resources": [ + { + "name": "index.js", + "start": 17.399999976158142, + "end": 20.800000071525574, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 22.300000071525574, + "end": 23, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 42, + "end": 66.5, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "local", + "trial": 4, + "marks": { + "start": 14.899999976158142, + "imported": 37.199999928474426, + "initialized": 94.79999995231628, + "command": 140.19999992847443, + "prompt": 163.29999995231628 + }, + "resources": [ + { + "name": "index.js", + "start": 15.899999976158142, + "end": 18.699999928474426, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 19.899999976158142, + "end": 20.699999928474426, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 38.799999952316284, + "end": 59.199999928474426, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "fast4g", + "trial": 0, + "marks": { + "start": 188.39999997615814, + "imported": 545.8999999761581, + "initialized": 3229.100000023842, + "command": 3284, + "prompt": 3309.5 + }, + "resources": [ + { + "name": "index.js", + "start": 189.10000002384186, + "end": 361.6999999284744, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 362.6999999284744, + "end": 544.3999999761581, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 547.3999999761581, + "end": 3189.399999976158, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "fast4g", + "trial": 0, + "marks": { + "start": 190.5, + "imported": 544.8999999761581, + "initialized": 2842.3000000715256, + "command": 2888.5, + "prompt": 2913.8000000715256 + }, + "resources": [ + { + "name": "index.js", + "start": 190.80000007152557, + "end": 365.8000000715256, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 366.8000000715256, + "end": 543.8999999761581, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 546.8999999761581, + "end": 2809.899999976158, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "fast4g", + "trial": 1, + "marks": { + "start": 189.79999995231628, + "imported": 552.1000000238419, + "initialized": 3196.5, + "command": 3251.899999976158, + "prompt": 3278.600000023842 + }, + "resources": [ + { + "name": "index.js", + "start": 190.69999992847443, + "end": 367.2999999523163, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 368.6999999284744, + "end": 550.5, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 553.1999999284744, + "end": 3154.399999976158, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "fast4g", + "trial": 1, + "marks": { + "start": 187.90000009536743, + "imported": 544.5, + "initialized": 2847.3000000715256, + "command": 2893.3000000715256, + "prompt": 2917.8000000715256 + }, + "resources": [ + { + "name": "index.js", + "start": 188.80000007152557, + "end": 358.5, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 359.7000000476837, + "end": 542.5, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 545.6000000238419, + "end": 2809.8000000715256, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "fast4g", + "trial": 2, + "marks": { + "start": 189.70000004768372, + "imported": 546, + "initialized": 3199.899999976158, + "command": 3255.100000023842, + "prompt": 3282.2000000476837 + }, + "resources": [ + { + "name": "index.js", + "start": 190.70000004768372, + "end": 365.5, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 365.89999997615814, + "end": 544.7000000476837, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 547.6000000238419, + "end": 3152.2000000476837, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "fast4g", + "trial": 2, + "marks": { + "start": 188.89999997615814, + "imported": 543.8999999761581, + "initialized": 2847.7000000476837, + "command": 2893.7000000476837, + "prompt": 2919.5 + }, + "resources": [ + { + "name": "index.js", + "start": 189.89999997615814, + "end": 361.5, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 362.10000002384186, + "end": 542.5, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 546, + "end": 2811, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "fast4g", + "trial": 3, + "marks": { + "start": 189.10000002384186, + "imported": 549.6000000238419, + "initialized": 3203, + "command": 3258.600000023842, + "prompt": 3285.100000023842 + }, + "resources": [ + { + "name": "index.js", + "start": 190.5, + "end": 364.39999997615814, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 365.8000000715256, + "end": 548, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 551.1000000238419, + "end": 3154.5, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "fast4g", + "trial": 3, + "marks": { + "start": 189, + "imported": 544.6999999284744, + "initialized": 2852.6999999284744, + "command": 2898.600000023842, + "prompt": 2921.600000023842 + }, + "resources": [ + { + "name": "index.js", + "start": 189.89999997615814, + "end": 364.7999999523163, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 365.39999997615814, + "end": 542.6999999284744, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 546.1999999284744, + "end": 2813.100000023842, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "fast4g", + "trial": 4, + "marks": { + "start": 190.30000007152557, + "imported": 546.5, + "initialized": 3236.100000023842, + "command": 3294.3000000715256, + "prompt": 3326.5 + }, + "resources": [ + { + "name": "index.js", + "start": 190.60000002384186, + "end": 366.60000002384186, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 367.10000002384186, + "end": 545.1000000238419, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 548.8000000715256, + "end": 3192.100000023842, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "fast4g", + "trial": 4, + "marks": { + "start": 190.5, + "imported": 546.8999999761581, + "initialized": 2876.600000023842, + "command": 2921.7000000476837, + "prompt": 2945.600000023842 + }, + "resources": [ + { + "name": "index.js", + "start": 190.70000004768372, + "end": 363.2000000476837, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 365, + "end": 545.5, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 548.2000000476837, + "end": 2842.399999976158, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "slow4g", + "trial": 0, + "marks": { + "start": 599.3999999761581, + "imported": 1792.8999999761581, + "initialized": 16080.699999928474, + "command": 16137.599999904633, + "prompt": 16166.399999976158 + }, + "resources": [ + { + "name": "index.js", + "start": 600.5, + "end": 1172.5, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 1174.1999999284744, + "end": 1791.6999999284744, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 1794.6999999284744, + "end": 16040.099999904633, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "slow4g", + "trial": 0, + "marks": { + "start": 595.5, + "imported": 1799.2000000476837, + "initialized": 14169.300000071526, + "command": 14216.600000023842, + "prompt": 14244.200000047684 + }, + "resources": [ + { + "name": "index.js", + "start": 596.5, + "end": 1177.9000000953674, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 1179.5, + "end": 1797.8000000715256, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 1801.4000000953674, + "end": 14135.400000095367, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "slow4g", + "trial": 1, + "marks": { + "start": 589.6999999284744, + "imported": 1792.5, + "initialized": 16091.099999904633, + "command": 16148.099999904633, + "prompt": 16177.199999928474 + }, + "resources": [ + { + "name": "index.js", + "start": 591.3999999761581, + "end": 1174.3999999761581, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 1175.5999999046326, + "end": 1791, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 1794, + "end": 16048.399999976158, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "slow4g", + "trial": 1, + "marks": { + "start": 590.6000000238419, + "imported": 1801.7000000476837, + "initialized": 14179, + "command": 14225.5, + "prompt": 14250.800000071526 + }, + "resources": [ + { + "name": "index.js", + "start": 591.6000000238419, + "end": 1174, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 1175.2000000476837, + "end": 1799.6000000238419, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 1803.5, + "end": 14141.5, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "slow4g", + "trial": 2, + "marks": { + "start": 589.5, + "imported": 1784.6000000238419, + "initialized": 16065, + "command": 16121.300000071526, + "prompt": 16148.600000023842 + }, + "resources": [ + { + "name": "index.js", + "start": 590.3999999761581, + "end": 1164.7000000476837, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 1165.5, + "end": 1783, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 1786.8000000715256, + "end": 16022.300000071526, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "slow4g", + "trial": 2, + "marks": { + "start": 587.8000000715256, + "imported": 1784.8000000715256, + "initialized": 14156.600000023842, + "command": 14202.700000047684, + "prompt": 14227.300000071526 + }, + "resources": [ + { + "name": "index.js", + "start": 588.7000000476837, + "end": 1165.6000000238419, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 1167, + "end": 1783.2000000476837, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 1786.8000000715256, + "end": 14117.300000071526, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "slow4g", + "trial": 3, + "marks": { + "start": 597.7000000476837, + "imported": 1799.6000000238419, + "initialized": 16093.5, + "command": 16150.299999952316, + "prompt": 16177.600000023842 + }, + "resources": [ + { + "name": "index.js", + "start": 598.8999999761581, + "end": 1181.2000000476837, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 1182.2999999523163, + "end": 1797.7999999523163, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 1800.7000000476837, + "end": 16047.899999976158, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "slow4g", + "trial": 3, + "marks": { + "start": 601.3999999761581, + "imported": 1792.6000000238419, + "initialized": 14169, + "command": 14221.699999928474, + "prompt": 14253.5 + }, + "resources": [ + { + "name": "index.js", + "start": 602, + "end": 1172.5, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 1173.7999999523163, + "end": 1791.1999999284744, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 1794.8999999761581, + "end": 14130.100000023842, + "transfer": 2117810, + "encoded": 2117510 + } + ] + }, + { + "package": 0, + "condition": "slow4g", + "trial": 4, + "marks": { + "start": 595.1000000238419, + "imported": 1799.1000000238419, + "initialized": 16091.600000023842, + "command": 16149.400000095367, + "prompt": 16177.100000023842 + }, + "resources": [ + { + "name": "index.js", + "start": 596.4000000953674, + "end": 1180.5, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 1183.8000000715256, + "end": 1797.2000000476837, + "transfer": 7781, + "encoded": 7481 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 1801.5, + "end": 16043.400000095367, + "transfer": 2460599, + "encoded": 2460299 + } + ] + }, + { + "package": 1, + "condition": "slow4g", + "trial": 4, + "marks": { + "start": 597.5, + "imported": 1811.3000000715256, + "initialized": 14179.800000071526, + "command": 14225.900000095367, + "prompt": 14250.700000047684 + }, + "resources": [ + { + "name": "index.js", + "start": 598.6000000238419, + "end": 1184.3000000715256, + "transfer": 973, + "encoded": 673 + }, + { + "name": "bashkit_wasm.js", + "start": 1185.5, + "end": 1809.1000000238419, + "transfer": 7799, + "encoded": 7499 + }, + { + "name": "bashkit_wasm_bg.wasm", + "start": 1812.8000000715256, + "end": 14139.600000023842, + "transfer": 2117810, + "encoded": 2117510 + } + ] + } + ] + }, + "runtime": { + "baseline": { + "node": "v26.5.0", + "package": "/private/tmp/bashkit-baseline-oz", + "results": { + "arithmetic": { + "stdout": "199990000\n", + "samples": [ + 44.23554200000001, + 44.316374999999994, + 43.21366599999999, + 44.59429100000003, + 44.638458000000014, + 43.765625, + 43.305542 + ], + "median": 44.23554200000001 + }, + "substitutions": { + "stdout": "hi\n", + "samples": [ + 6.691499999999962, + 6.413416999999981, + 9.098292000000015, + 6.096458999999982, + 5.677875000000029, + 6.283957999999984, + 5.434708999999998 + ], + "median": 6.283957999999984 + }, + "jq": { + "stdout": "1001000\n", + "samples": [ + 2.5480830000000196, + 2.786707999999976, + 2.460124999999948, + 2.3708750000000123, + 2.6324579999999855, + 2.233041000000071, + 2.3139159999999492 + ], + "median": 2.460124999999948 + }, + "text": { + "stdout": "10000\n", + "samples": [ + 10.493916000000013, + 10.508416000000011, + 9.975500000000011, + 9.83141599999999, + 9.62195799999995, + 9.435292000000004, + 9.550375000000031 + ], + "median": 9.83141599999999 + } + } + }, + "selected": { + "node": "v26.5.0", + "package": "/private/tmp/bashkit-balanced-profile-oz", + "results": { + "arithmetic": { + "stdout": "199990000\n", + "samples": [ + 50.007542000000015, + 48.53175000000002, + 48.67329200000003, + 50.149541, + 49.739832999999976, + 49.493041000000005, + 48.65091699999999 + ], + "median": 49.493041000000005 + }, + "substitutions": { + "stdout": "hi\n", + "samples": [ + 7.53895799999998, + 6.6450419999999895, + 6.147041999999999, + 6.18829199999999, + 6.094959000000017, + 5.852458000000013, + 5.859375 + ], + "median": 6.147041999999999 + }, + "jq": { + "stdout": "1001000\n", + "samples": [ + 2.4794160000000147, + 2.3771659999999883, + 2.6838749999999436, + 2.2523749999999154, + 2.1724590000000035, + 2.162667000000056, + 2.2771249999999554 + ], + "median": 2.2771249999999554 + }, + "text": { + "stdout": "10000\n", + "samples": [ + 11.623333000000002, + 11.464875000000006, + 10.260125000000016, + 10.209958000000029, + 10.119290999999976, + 9.871250000000032, + 10.29849999999999 + ], + "median": 10.260125000000016 + } + } + }, + "rejectedZ": { + "node": "v26.5.0", + "package": "/private/tmp/bashkit-size-profile-oz", + "results": { + "arithmetic": { + "stdout": "199990000\n", + "samples": [ + 66.689583, + 66.24974999999998, + 66.00487499999997, + 66.73350000000005, + 66.180791, + 66.12816700000002, + 65.74704099999997 + ], + "median": 66.180791 + }, + "substitutions": { + "stdout": "hi\n", + "samples": [ + 8.666624999999954, + 7.969583000000057, + 7.808374999999955, + 7.881167000000005, + 7.785916000000043, + 7.719707999999969, + 7.606457999999975 + ], + "median": 7.808374999999955 + }, + "jq": { + "stdout": "1001000\n", + "samples": [ + 3.4603329999999914, + 3.604332999999997, + 3.381917000000044, + 3.0702499999999873, + 2.683917000000065, + 2.3656250000000227, + 2.283625000000029 + ], + "median": 3.0702499999999873 + }, + "text": { + "stdout": "10000\n", + "samples": [ + 13.437832999999955, + 13.11283299999991, + 12.069624999999974, + 11.965250000000083, + 11.758916999999997, + 11.905624999999986, + 11.672791999999959 + ], + "median": 11.965250000000083 + } + } + } + } +} diff --git a/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.md b/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.md new file mode 100644 index 000000000..542b7ddc5 --- /dev/null +++ b/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.md @@ -0,0 +1,133 @@ +# Browser package compatibility and cold start — 2026-10-15 + +Bashkit's published/source compatibility gap is version drift. The npm 0.18.2 +artifact identifies release commit `567511386572d4c4b9f649ab1da97aad53b9f341` +(September 22), while the independent study's source is `491c3281`, 269 commits +newer. The source and published distributions use the same build path. The +package version remains 0.18.2 on unreleased main; it is not an artifact identity. + +A dedicated `wasm-release` profile (`opt-level = "s"`, fat LTO, Binaryen 125 +`-Oz`) reduces current-package Brotli transfer by **13.9%** without dropping +`jq`, `Terminal`, commands, or resource limits. Native release settings stay +unchanged. A more aggressive `"z"` profile saved 25.0% but slowed the arithmetic +sample about 50%, so it was rejected. + +## Independent evidence and validation scope + +- [brush-web compatibility study](https://github.com/ShaulLavo/brush-web/pull/7): + Oils / Brush / Bashkit corpora reported source 95.4 / 84.1 / 96.2% versus + published 55.1 / 55.8 / 70.8% for builtins-only cases. Its own report attributes + the old package's failures to later compatibility work. +- [mobile cold-start study](https://github.com/ShaulLavo/brush-web/pull/9): + transfer dominated startup; source grew across those 269 commits; removing + Terminal saved only about 50 KB Brotli; `wasm-opt -Oz` alone did not help. +- [initial API comparison](https://github.com/ShaulLavo/brush-web/pull/5): + the published package lacked the source tree's newer Terminal API. + +This investigation independently checks npm `gitHead` against the release tag, +compares 24 bounded GNU Bash cases, rebuilds the exact release source, and measures +real Chromium startup of the full current package. The 24 cases are a regression +sample, **not a rerun or independent confirmation of the three large-corpus +percentages above**. They isolate representative release gaps without turning a +large, evolving third-party corpus into a fragile release gate. + +## Compatibility + +GNU Bash 5.3.20 is the oracle. Fresh shell per case; exact stdout bytes and exit +status; stderr not scored. Results are in the matching JSON report. + +| Artifact | Cases passed | +|---|---:| +| npm 0.18.2 | 19/24 | +| Rebuilt v0.18.2 source tag | 19/24, identical five failures | +| Current source, original release profile | 24/24 | +| Current source, selected shipping profile | 24/24 | + +Published failures: sparse array indices, negative array slices, associative +array literals, arithmetic array updates, zero-padded brace sequences. The +existing source fixes remain intact under the new profile. All 70 browser +package integration/packaging tests pass, including async host filesystem, +timers, cancellation/resource limits, jq, binary output, and interactive terminal. + +## Artifact attribution + +Same source base `8a817bf0`, Rust 1.95.0, wasm-bindgen 0.2.128, single-threaded +`wasm32-unknown-unknown`, `terminal` enabled, and pinned Binaryen 125 `-Oz` for +both baseline and selected builds. Byte counts include `index.js`, generated JS +glue, and the WASM; compression is per file, as served by a CDN. + +| Profile | Raw bytes | gzip-9 bytes | Brotli-11 bytes | +|---|---:|---:|---:| +| Original release: opt 3, thin LTO | 9,519,539 | 3,570,277 | 2,468,453 | +| Selected: opt s, fat LTO | 7,668,904 | 2,973,501 | 2,125,682 | +| Rejected: opt z, fat LTO | 6,420,068 | 2,506,391 | 1,851,368 | + +The selected build saves 342,771 Brotli bytes (13.9%), 596,776 gzip bytes (16.7%), +and 1,850,635 raw bytes (19.4%). Its WASM is 99.6% of the Brotli load graph; +wrapper + glue total about 8 KB. The WASM code section falls from 8,279,718 to +6,433,520 bytes; data stays roughly constant (1,149,749 to 1,143,328 bytes). +The improvement removes generated code overhead rather than command capability +or symbol names: the baseline has no large debug/name custom section to strip. + +The old npm artifact is 1,826,367 Brotli bytes for the same three-file graph. +Current source carries substantially more behavior. Even after this change, +that old artifact remains smaller; publishing the repaired source does not mean +restoring the old feature set or promising just-bash's much smaller payload. + +## Cold startup + +Five trials per artifact/condition, fresh browser context (cold HTTP cache), +rotated order, Chrome 155.0.8059.79, Node 26.5.0, macOS Apple Silicon. Brotli-11 +loopback server and CDP network throttling, page CPU 4x. Both artifacts use the +real loader and must run `echo hi`, drive Terminal, and paint the result. Samples +and resource timings are retained. Full method: [experiment guide](../README.md). + +| Condition | Baseline median | Selected median | Saved | +|---|---:|---:|---:| +| Loopback + page CPU 4x | 196.6 ms | 168.0 ms | 28.6 ms | +| Fast 4G (1,012,500 B/s, 165 ms) | 3,285.1 ms | 2,919.5 ms | 365.6 ms | +| Slow 4G (180,000 B/s, 562.5 ms) | 16,177.1 ms | 14,250.7 ms | 1,926.4 ms | + +On Slow 4G, the last resource arrives at median 16,043.4 / 14,135.4 ms. The +remaining loader work is 42.7 / 38.9 ms; the first `Bash.execute('echo hi')` +is 56.9 / 46.5 ms. Resource transfer dominates both artifacts. These are +component medians; do not add independently computed medians as an exact sum. + +These numbers are package-only, not brush-web's navigation path: no Ghostty, +framework, worker, or other shared assets. CPU throttling covers this page's +interpreter, but does not model a physical phone. Contexts have cold HTTP caches; +the browser process can share compiled-code caches. No warm-visit claim is made. +The development host also ran repository checks: timings are exploratory, while +byte counts and semantic gates are deterministic. The selected bundle crossed +below 15 seconds here, but an adopter must retain an appropriate startup budget +for their larger application and slower networks. + +## Runtime tradeoff + +One warmup, seven measured fresh-instance executions; serial exploratory samples +on the same development host, Node 26.5.0. Not a clean-host performance baseline. + +| Workload | Original median | Selected median | Rejected z median | +|---|---:|---:|---:| +| 20,000 arithmetic iterations | 44.24 ms | 49.49 ms | 66.18 ms | +| 1,000 command substitutions | 6.28 ms | 6.15 ms | 7.81 ms | +| jq map/sum over 1,000 values | 2.46 ms | 2.28 ms | 3.07 ms | +| sed/sort/wc over 10,000 lines | 9.83 ms | 10.26 ms | 11.97 ms | + +The selected profile trades about 12% arithmetic throughput in this sample for +14% less download. No claim of universal throughput improvement. The gain is a +real first-visit improvement with all package behavior retained. + +## Gates and publication boundary + +CI and npm publication run identical checks: all browser package tests, 24-case +GNU Bash parity, and raw/gzip/Brotli budgets (8,000,000 / 3,100,000 / 2,220,000 +bytes, about 4-5% measured headroom). There is no noisy wall-clock CI threshold. +The original artifact fails these budgets; the shipping artifact passes. +`build-info.json` and npm-archive tests make exact source/profile/hash verifiable. + +No npm release was performed by this benchmark change. On October 15, +`just check-nightly` failed because one of the last three nightly runs had a red +Advisory Audit ([run 38679252914](https://github.com/everruns/bashkit/actions/runs/38679252914)); +all three fuzz runs were green. The release contract prohibits publication until +that gate is green. A merged code change is distinct from a repaired npm package. diff --git a/crates/bashkit-wasm/bench/runtime.mjs b/crates/bashkit-wasm/bench/runtime.mjs new file mode 100644 index 000000000..68f640412 --- /dev/null +++ b/crates/bashkit-wasm/bench/runtime.mjs @@ -0,0 +1,31 @@ +// Representative commands guard against buying transfer savings with unusable execution. +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +const pkg = resolve(process.argv[2]); +const { initBashkit, Bash } = await import(pathToFileURL(resolve(pkg, "index.js"))); +await initBashkit(readFileSync(resolve(pkg, "bashkit_wasm_bg.wasm"))); +const cases = { + arithmetic: "n=0; for ((i=0;i<20000;i++)); do ((n+=i)); done; echo $n", + substitutions: 'for ((i=0;i<1000;i++)); do a=$(echo hi); done; echo "$a"', + jq: "seq 1000 | jq -s 'map(. * 2) | add'", + text: "seq 10000 | sed 's/1/x/g' | sort | wc -l", +}; +const results = {}; +for (const [name, script] of Object.entries(cases)) { + const samples = []; + let output; + for (let i = 0; i < 8; i++) { + const bash = new Bash({ profile: "interactive", maxCommands: 1000000, maxLoopIterations: 100000 }); + const start = performance.now(); + const result = await bash.execute(script); + const ms = performance.now() - start; + if (result.exitCode !== 0) throw new Error(`${name}: ${result.stderr}`); + if (output !== undefined && output !== result.stdout) throw new Error("unstable output"); + output = result.stdout; + if (i) samples.push(ms); + result.free(); bash.free(); + } + results[name] = { stdout: output, samples, median: [...samples].sort((a, b) => a - b)[3] }; +} +console.log(JSON.stringify({ node: process.version, package: pkg, results }, null, 2)); diff --git a/crates/bashkit-wasm/bench/size-budget.json b/crates/bashkit-wasm/bench/size-budget.json new file mode 100644 index 000000000..8eaa130ac --- /dev/null +++ b/crates/bashkit-wasm/bench/size-budget.json @@ -0,0 +1,5 @@ +{ + "raw": 8000000, + "gzip": 3100000, + "brotli": 2220000 +} diff --git a/crates/bashkit-wasm/package.json b/crates/bashkit-wasm/package.json index 8c903cd2e..5cd08712b 100644 --- a/crates/bashkit-wasm/package.json +++ b/crates/bashkit-wasm/package.json @@ -11,7 +11,8 @@ "types": "./index.d.ts", "default": "./index.js" }, - "./bashkit_wasm_bg.wasm": "./bashkit_wasm_bg.wasm" + "./bashkit_wasm_bg.wasm": "./bashkit_wasm_bg.wasm", + "./build-info.json": "./build-info.json" }, "sideEffects": [ "./index.js", @@ -24,6 +25,7 @@ "bashkit_wasm.d.ts", "bashkit_wasm_bg.wasm", "bashkit_wasm_bg.wasm.d.ts", + "build-info.json", "README.md" ], "license": "MIT", diff --git a/crates/bashkit-wasm/scripts/build-info.mjs b/crates/bashkit-wasm/scripts/build-info.mjs new file mode 100644 index 000000000..264d049e5 --- /dev/null +++ b/crates/bashkit-wasm/scripts/build-info.mjs @@ -0,0 +1,35 @@ +// Publish the exact source/build identity with the bundle; version alone stays +// unchanged between releases and cannot identify a source-tree build. +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync, realpathSync } from "node:fs"; +import { resolve } from "node:path"; +const [pkg, profile, features, wasmOpt] = process.argv.slice(2); +const workspace = realpathSync(new URL("../../../", import.meta.url)); +const command = (name, args) => execFileSync(name, args, { + cwd: workspace, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], +}).trim(); +let sourceRevision = null; +let sourceDirty = null; +try { + if (realpathSync(command("git", ["rev-parse", "--show-toplevel"])) !== workspace) { + throw new Error("source archive inside another repository"); + } + sourceRevision = command("git", ["rev-parse", "HEAD"]); + sourceDirty = command("git", ["status", "--porcelain"]).length > 0; +} catch { + // Source archives have no Git identity. Record absence instead of guessing + // from the package version; CI/release checkouts always have both fields. +} +const info = { + version: JSON.parse(readFileSync(resolve(pkg, "package.json"))).version, + sourceRevision, + sourceDirty, + profile, + features: features.split(",").filter(Boolean).sort(), + rustc: command("rustc", ["--version"]), + wasmBindgen: command("wasm-bindgen", ["--version"]), + wasmOpt, + wasmSha256: createHash("sha256").update(readFileSync(resolve(pkg, "bashkit_wasm_bg.wasm"))).digest("hex"), +}; +writeFileSync(resolve(pkg, "build-info.json"), JSON.stringify(info, null, 2) + "\n"); diff --git a/crates/bashkit-wasm/scripts/build.sh b/crates/bashkit-wasm/scripts/build.sh index cf0d04875..b4a462775 100755 --- a/crates/bashkit-wasm/scripts/build.sh +++ b/crates/bashkit-wasm/scripts/build.sh @@ -12,23 +12,24 @@ CRATE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" WORKSPACE_ROOT="$(cd "$CRATE_DIR/../.." && pwd)" OUT_DIR="$CRATE_DIR/pkg" TARGET=wasm32-unknown-unknown -PROFILE="${1:-release}" +PROFILE="${1:-wasm-release}" # Cargo features, comma-separated. The npm package ships `terminal` (the # `Terminal` class, ~60 KB gzipped); set BASHKIT_WASM_FEATURES="" to drop it. FEATURES="${BASHKIT_WASM_FEATURES-terminal}" +cd "$WORKSPACE_ROOT" +# Keep `release` as the workflow-facing spelling; both CI and publication use +# the same dedicated profile. Reject typos instead of silently shipping debug. +case "$PROFILE" in + release|wasm-release) PROFILE=wasm-release; TARGET_SUBDIR=wasm-release ;; + dev|debug) PROFILE=dev; TARGET_SUBDIR=debug ;; + *) echo "unsupported profile: $PROFILE" >&2; exit 1 ;; +esac echo "==> cargo build ($PROFILE) for $TARGET" -if [ "$PROFILE" = "release" ]; then - CARGO_PROFILE_FLAG="--release" - TARGET_SUBDIR="release" -else - CARGO_PROFILE_FLAG="" - TARGET_SUBDIR="debug" -fi # shellcheck disable=SC2086 -cargo build -p bashkit-wasm --target "$TARGET" $CARGO_PROFILE_FLAG ${FEATURES:+--features "$FEATURES"} +cargo build --locked -p bashkit-wasm --target "$TARGET" --profile "$PROFILE" ${FEATURES:+--features "$FEATURES"} -WASM_IN="$WORKSPACE_ROOT/target/$TARGET/$TARGET_SUBDIR/bashkit_wasm.wasm" +WASM_IN="${CARGO_TARGET_DIR:-$WORKSPACE_ROOT/target}/$TARGET/$TARGET_SUBDIR/bashkit_wasm.wasm" echo "==> wasm-bindgen --target web" rm -rf "$OUT_DIR" @@ -40,6 +41,7 @@ wasm-bindgen "$WASM_IN" \ --omit-default-module-path if command -v wasm-opt >/dev/null 2>&1; then + WASM_OPT_VERSION="$(wasm-opt --version)" # Rust emits memory.copy, saturating float conversions, and sign extension; # enable those stable features without accepting unrelated proposals. echo "==> wasm-opt -Oz (Rust WebAssembly features enabled)" @@ -49,6 +51,7 @@ if command -v wasm-opt >/dev/null 2>&1; then --enable-sign-ext \ "$OUT_DIR/bashkit_wasm_bg.wasm" -o "$OUT_DIR/bashkit_wasm_bg.wasm" else + WASM_OPT_VERSION=none echo "==> wasm-opt not found; skipping size optimization" fi @@ -57,6 +60,7 @@ cp "$CRATE_DIR/js/index.js" "$OUT_DIR/index.js" cp "$CRATE_DIR/js/index.d.ts" "$OUT_DIR/index.d.ts" cp "$CRATE_DIR/package.json" "$OUT_DIR/package.json" cp "$CRATE_DIR/README.md" "$OUT_DIR/README.md" +node "$CRATE_DIR/scripts/build-info.mjs" "$OUT_DIR" "$PROFILE" "$FEATURES" "$WASM_OPT_VERSION" echo "==> built $OUT_DIR" ls -lh "$OUT_DIR" diff --git a/knowledge/operations/performance-results.md b/knowledge/operations/performance-results.md index 8487154bf..eda55efff 100644 --- a/knowledge/operations/performance-results.md +++ b/knowledge/operations/performance-results.md @@ -100,3 +100,14 @@ most misses). `/benches` renders both after the gap telemetry panels. See When changing result schemas, update the transformer and this spec in the same PR. Do not hand-edit `performance-timeline.json` except by running the script. + +## Browser package experiments + +Browser transfer/startup reports use +`crates/bashkit-wasm/bench/results/wasm-*.{json,md}`. The scripts emit JSON to +stdout for explicit capture; browser measurements are exploratory and do not +refresh `/benches` or use the native/ Criterion schemas. They record exact +artifact hashes and compressed transfer bytes alongside cold Chromium timings. +CI gates deterministic size budgets and a bounded compatibility sample, not +noisy startup time. See [Browser Package](../runtimes/browser-package.md) and the +[methodology](../../crates/bashkit-wasm/bench/README.md). diff --git a/knowledge/operations/release-process.md b/knowledge/operations/release-process.md index ccf806fad..75a93e510 100644 --- a/knowledge/operations/release-process.md +++ b/knowledge/operations/release-process.md @@ -309,6 +309,14 @@ integration suite, and publishes `@everruns/bashkit-wasm` to npm with provenance. Uses the same `NPM_TOKEN` as `publish-js.yml`; the package version is synced manually from the workspace version during release preparation. +CI and publication both use the `wasm-release` profile and run the package's +GNU Bash compatibility sample, compressed-size budgets, and npm archive tests. +The archive includes `build-info.json` with source commit and the shipped WASM +hash; inspect those fields after publication as well as npm version/dist-tags. +A current source checkout can still say `0.18.2` while containing hundreds of +commits absent from the published `0.18.2`, so version equality does not prove +artifact parity. See [Browser Package](../runtimes/browser-package.md). + ### c-api-binaries.yml Dispatched by `release.yml` from the verified release tag. Builds and attaches diff --git a/knowledge/runtimes/browser-package.md b/knowledge/runtimes/browser-package.md index bd7a615ad..131ff7cc7 100644 --- a/knowledge/runtimes/browser-package.md +++ b/knowledge/runtimes/browser-package.md @@ -186,7 +186,7 @@ core already gates off under `cfg(target_family = "wasm")` (see loads from a CDN, a bundler, or a plain `