Skip to content

perf: memoize subprocess results across documents - #80

Draft
schloerke wants to merge 1 commit into
quarto-ext:mainfrom
schloerke:subprocess-cache
Draft

perf: memoize subprocess results across documents#80
schloerke wants to merge 1 commit into
quarto-ext:mainfrom
schloerke:subprocess-cache

Conversation

@schloerke

Copy link
Copy Markdown
Collaborator

Closes #79 — see the issue for full measurements on a 520-page site (CI build 47.5 → 38.1 min; local warm re-renders skip every shinylive subprocess call).

What this does

Adds a cachedPipe drop-in for pandoc.pipe that memoizes subprocess results to .quarto/shinylive-cache/ (one content-addressed file per result, atomic tmp-file + rename writes), so a full-project render pays each unique subprocess call once instead of once per document.

Design

  • Version-keyed invalidation, no heuristics. A call is only cached once the live version of the tool producing it is known: the Python path activates after --version runs (per document, never cached), the R path after extension info establishes versions.r, and the codeblock-to-json call is keyed on both the language package version and quarto.version. Every cache key embeds those versions, so upgrading shinylive or quarto invalidates naturally; a stale hit is impossible.
  • Side-effect safety. extension base-htmldeps downloads the shinylive web assets as a side effect; a naive cache elides that and emits HTML referencing files that don't exist (we hit this on a fresh CI runner). Cached results are only served if every "source"/"path" file they reference still exists — otherwise the subprocess re-runs and re-creates them.
  • Strictly an optimization. Cache setup, reads, and writes are all pcall-guarded; any failure falls back to pandoc.pipe. Failed subprocess calls are never cached (errors propagate exactly as before). Projects without a project directory get plain passthrough.

Testing

On a minimal one-page test site (quarto 1.8.25, py-shinylive 0.8.9):

  • cold render populates the cache; warm render is ~2.2× faster with byte-identical HTML
  • a poisoned cache entry pointing at nonexistent asset files is detected, the subprocess re-runs, and the entry heals — output still identical

Deployed at scale (a slightly different site-specific variant) on shiny.posit.co/py: posit-dev/py-shiny-site#382 — 520 pages, byte-identical output verified, CI + local measurements in the issue.

Opening as a draft for design feedback — happy to adjust key layout, cache location, or gate it behind an option.

The filter re-runs identical subprocesses for every document in a project
render (extension info, base-htmldeps, language-resources, plus two per
code block). Memoize their results to .quarto/shinylive-cache/ so a
full-site render pays each unique call once.

Cache keys embed the live version of the tool that produced the result
(py/r shinylive package version, and the quarto version for the
codeblock-to-json call), so stale reuse is impossible; the version
discovery calls themselves are never cached. Cached results whose
referenced files no longer exist are re-run, so the asset download that
base-htmldeps performs as a side effect can never be skipped when
needed. Any cache failure falls back to running the real subprocess.

Fixes quarto-ext#79
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.

Filter re-runs identical shinylive CLI subprocesses per document, dominating large-site render times

1 participant