Skip to content

P 13 Engine Start Steps

Karthik P edited this page Oct 27, 2026 · 1 revision

P-13: Engine start-up steps and the compiled-engine cache

Context: After P-07, a warm start loads the engine entirely from the HTTP cache (3 KB transferred), yet it was ready no sooner than a cold start: about 1.6 s against a 1 s warm budget. There was no measure of where that time went. DuckDB-wasm 1.32.0's workers also stream the module through a progress counter into a new Response before compiling it, and Chromium only caches compiled WebAssembly for a response that came from fetch() itself, so the compiled engine was never cached.

Hypothesis: Letting Chromium cache the compiled module, and then starting the engine alongside the app shell, brings warm engine-ready under 1 s.

Change: #158 (#157).

  • Step marks. User Timing marks record each step of starting the engine, and time-to-engine-ready reports how long each took: engineBootStartMs, workerMs, instantiateMs, openMs, connectMs and readyMs.
  • Compiled-engine cache. The production build rewrites DuckDB-wasm's eh and mvp workers to compile fetch()'s own response (duckdbWorkers() in src/engine/vite-plugin.ts).
    • Each rewritten worker is emitted with its own content hash.
    • The build fails if the code it replaces is missing, so an upgrade can't silently undo it.
    • Blinkql never used the progress events.

Method: Reference laptop (i5-8250U, Windows 11, Chromium 156). time-to-app-shell and time-to-engine-ready, cold and warm, 3 warm-up and 10 measured iterations. The branch ran first and main (86bfbf9) straight after. Warm mode uses a persistent profile, as since P-07. The cache itself was checked separately: a persistent Chromium profile ran queries on each load and held the page open for 6 s, and SiteIsolatedCodeCache.WASM.Behaviour was read after every load.

Metric Before (median / p95) After (median / p95) Change (median)
timeToAppShell, cold 396.2 / 493.1 ms 351.2 / 454.2 ms within noise
timeToAppShell, warm 123.5 / 171.3 ms 109.2 / 147.1 ms within noise
timeToEngineReady, cold 2169.2 / 2322.0 ms 2066.9 / 2262.0 ms within noise
timeToEngineReady, warm 1444.7 / 1686.0 ms 1362.7 / 1531.9 ms within noise

Where the time goes, on the branch (median / p95, ms):

Step Cold Warm
Navigation start until the engine starts (engineBootStartMs) 413.0 / 458.4 138.8 / 165.0
Choosing the bundle, creating the worker (workerMs) 6.1 / 8.4 4.9 / 5.9
The worker's script, then fetching, compiling and instantiating the module (instantiateMs) 1410.5 / 1567.5 957.8 / 1096.3
Opening the database (openMs) 175.7 / 189.1 181.7 / 231.8
Version check and connection (connectMs) 1.3 / 1.5 1.2 / 1.4
Start-up settings and the first query (readyMs) 50.4 / 58.1 46.7 / 63.1

The cache after sessions that ran queries (ms):

Load Code cache instantiate open ready
1 miss, then written 1289 169 41
2 hit 984 43 10
3 hit 970 71 11
4 hit 1041 45 9

Without the change, every load was a miss and nothing was written.

Findings:

  1. The engine now caches, but only after real work.

    • After a session that ran queries, later starts load the compiled engine from the cache: opening the database drops from about 170 to 45 ms, and the first query from about 45 to 10 ms.
    • V8 only writes the cache once enough optimised code exists and compiling has been quiet for 2 s. Start-up alone never gets there, so the benchmark's warm runs, which only start the engine, don't change.
  2. Compiling was not the main cost. A trace of a warm instantiateMs (about 1 s) splits roughly into:

    • the worker's 770 KB script, about 180 ms;
    • deserialising the cached module, about 370 ms;
    • instantiating it, about 160 ms;
    • DuckDB's own initialisation, about 390 ms.

    A cache hit trades compiling for hashing and deserialising, so instantiateMs barely moves.

  3. The remaining ideas can't reach 1 s.

    • Starting the engine before the first paint would save at most engineBootStartMs, about 140 ms warm.
    • Folding the start-up queries would save at most readyMs, about 45 ms.
    • Together they would leave about 1.15 s.

Trade-offs:

  • The build now rewrites two files from a dependency. The rewrite is one expression, checked at build time and covered by a unit test.
  • The dev server serves the workers unchanged, so the cache only applies to production builds.

Evidence: raw results in bench/results/2026-10-27T04-32-31Z-86bfbf9.json (after) and bench/results/2026-10-27T04-34-05Z-86bfbf9.json (before). A Chrome trace shows v8.wasm.moduleCacheHit for duckdb-eh-*.wasm on loads after a session that ran queries.

Conclusion: Keep both changes. The step marks show where start-up time goes, and the cache makes returning sessions faster at opening the database and answering their first query. The hypothesis doesn't hold: about 1.3–1.4 s of a warm start is instantiating and initialising DuckDB, which no cache removes. The warm engine-ready budget is now 1.5 s (see Performance budgets). Going lower would need a smaller engine build.

Clone this wiki locally