Skip to content

hub-client: Phase 0 render-path measurement harness (bd-k8ptcrd9) - #968

Merged
shikokuchuo merged 7 commits into
mainfrom
feature/bd-k8ptcrd9-render-path-phase0
Oct 29, 2026
Merged

shikokuchuo merged 7 commits into
mainfrom
feature/bd-k8ptcrd9-render-path-phase0

Conversation

@shikokuchuo

@shikokuchuo shikokuchuo commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Phase 0 of the main-thread render-path plan: instrumentation and baselines only. No behavior changes.

What changed

  • Opt-in perf gauges on the render path (off by default; enable with QUARTO_PERF_STATS=1, localStorage.quartoPerfStats = '1', or ?perf=1): renders started/completed/discarded-stale, WASM envelope parse time and bytes, VFS write count and bytes, semantic-token requests, format-check timing.
  • perfCounters now exists as two copies (hub-client and @quarto/preview-runtime) sharing one globalThis gauge table, with a drift-guard test. This makes the package's WASM-boundary code instrumentable without breaking the repo's partial vi.mock('@quarto/preview-runtime') mocks.
  • Every JSON.parse on the WASM boundary in wasmRenderer.ts now goes through one parseEnvelope helper.
  • New characterization tests: render-queue ordering and error propagation, format-classification edge cases, the exact UPDATE_AST payload key set.

Baselines (per keystroke, ~99 KB document, release WASM)

  • Full render: 302 ms. Discarded format-check parse: 79 ms (18%). Semantic tokens: 44 ms. Envelope JSON.parse: 14 ms across 16 MB.
  • A 10-keystroke burst executes 10 renders: no coalescing today, so ~2.7 s of a 3.9 s burst is wasted work.

Cost of the gauges when disabled

Effectively zero. Every instrumented wrapper has an early-return fast path: one property read, then the exact pre-instrumentation code shape (no try/finally, no gauge calls). Microbenchmark shows a flat ~5 ns residual per call with no payload scaling; an end-to-end A/B driver run against pre-change code is identical within noise. Both paths are pinned by perfGauges.test.ts (enabled records; disabled returns identical results and records nothing).

Verification

  • hub-client unit tests (1743) and preview-runtime tests (84) pass; npm run build:all passes.
  • The preview-runtime tests also pass with the WASM glue absent, which is the CI TS-suites condition (fixed an earlier CI-only failure from mocking the wasm-quarto-hub-client import).
  • One smokeAll.wasm fixture failure is pre-existing on main and unrelated (confirmed on a pristine checkout; tracked as bd-ucxaqjn2).
  • Gauges were verified through tests and a Node driver against the real WASM. Not yet smoke-tested in a live browser session.

…8ptcrd9)

Phase 0 of the 2026-10-24 main-thread render-path plan: instrumentation
and baselines only, no behavior changes.

Gauges (QUARTO_PERF_STATS-gated, perf.<name> lines): render.started /
completed / wasm-call on the five leaf render entries,
render.discarded-stale on ReactPreview's lastContentRef guard,
wasm-envelope.parse (segment + bytes) via a parseEnvelope helper
replacing every JSON.parse in wasmRenderer, vfs.write (segment + bytes),
intelligence.semantic-tokens(.tokenize), parse.qmd-to-ast, and
router.format-check(.parse/.classify).

perfCounters now exists as two copies (hub-client/src/utils +
ts-packages/preview-runtime) sharing one globalThis gauge sink: one
table per JS realm, and hub-client app code no longer imports the
package copy through the package boundary, so the repo's partial
vi.mock('@quarto/preview-runtime') mocks are unaffected. The copies are
drift-guarded by perfCounters.sync.test.ts.

Characterization tests (Phase 1-3 contracts):
- renderQueue.test.ts: serial order, every-queued-render-executes
  (the no-coalescing pin Phase 1 will deliberately flip), rejection
  reaches its caller and does not jam the queue.
- previewModeClassification.wasm.test.ts: three editor-edge pins with
  discovered outcomes (unknown scalar format key => class 'neither';
  unterminated front matter resolves successfully as default html;
  broken body still classifies from the front matter).
- Q2SandboxedPreviewIframe.test.tsx: exact 13-key UPDATE_AST payload
  key set (the contract Phase 3 changes).

Baseline (plan file has the full table; Node driver, release WASM, warm
SASS cache, 1x ~99KB / 8x ~800KB): per-keystroke wall 431ms / 3410ms =
render 302/2735ms + discarded format-check parse 79/708ms (finding 1,
18% at 1x) + tokens 44/386ms + envelope parse 14/132ms over 16.3/134.5MB
+ 2 full-text VFS writes/keystroke (the amplifier). A 10-keystroke burst
executes 10 renders (finding 2 confirmed: no coalescing).

Gates: hub-client unit 1743 green, preview-runtime 82 green, wasm leg
green except one PRE-EXISTING smokeAll version-shortcode fixture failure
verified identical on pristine main (filed bd-ucxaqjn2). npm run
build:all green. eslint errors on touched files identical to main (6,
pre-existing). Browser smoke of the gauges not done (driver + tests
only), as recorded in the plan.
- performance-profiling.md gains a "Driving the real render path"
  paragraph (resolve-hook maps, file: fetch shim for initWasm,
  fake-indexeddb before initWasm for the SASS cache bridge), pointing
  at the Phase 0 driver.
- Fix a Q-2-10 strict-render error in the Phase 0 baseline text
  (apostrophe-before-space read as a closing quote); both files now
  render clean under q2 render --strict.
@posit-snyk-bot

posit-snyk-bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

vi.mock('wasm-quarto-hub-client') only intercepts when Vite can resolve
the aliased glue file; in the Workspace TS suites CI job (no WASM build)
the file is absent, so the import fell through to native ESM and failed
with ERR_MODULE_NOT_FOUND. Install the stub via a test-only setter
(_setWasmModuleForTest) instead — initWasm() early-returns and the
dynamic import never fires. Verified locally with the glue symlink
temporarily removed: full preview-runtime suite (82 tests) passes.
PR review follow-up: restructure every instrumented wrapper to an
early-return fast path. When profiling is off, each site is one
perfEnabled() property read and then the exact pre-instrumentation code
shape — no try/finally, no gauge calls. The perf sink object is cached
at module scope, so perfEnabled() itself is a single property read.

Re-measured with the Phase 0 microbench: payload-scaled overhead is
gone (16MB envelope delta was +9.4us, now lost in noise; 130KB +2.1us
became +0.3us), leaving a flat ~5ns residual per instrumented call.

Enabled/disabled behavior is pinned by the new perfGauges.test.ts
(enabled records the gauges; disabled returns identical results and
records nothing). Both perfCounters copies updated identically (the
sync drift test passes). Suites: preview-runtime 84, hub-client 1743,
both also green with the wasm glue absent (CI TS-suites conditions);
hub-client production build green.
@shikokuchuo
shikokuchuo merged commit 69c64bd into main Oct 29, 2026
15 checks passed
@shikokuchuo
shikokuchuo deleted the feature/bd-k8ptcrd9-render-path-phase0 branch October 29, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants