Skip to content

fix(quarto): wait for the document parse before restoring cached output - #15332

Merged
midleman merged 3 commits into
mainfrom
mi/quarto-restore-race-reland
Aug 7, 2026
Merged

fix(quarto): wait for the document parse before restoring cached output#15332
midleman merged 3 commits into
mainfrom
mi/quarto-restore-race-reland

Conversation

@midleman

@midleman midleman commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Restoring a Quarto document's cached inline output read the model's cell list without checking whether it was current. A restore that ran while a re-parse was still pending matched nothing by content hash and marked itself complete, dropping every cached output. The document model now reports its parse state, so the restore waits instead of inferring it from an empty cell list.

  • IQuartoDocumentModel.isParsed separates "this document has no code cells" from "the cells aren't known yet"
  • whenParsed() lets the restore await the parse instead of retrying on a parse event
  • The wait moved above the content-hash fallback, which also read cells too early
  • The previous guard only covered untitled documents, and only when no cache was found on disk

Note: the demonstrable trigger is the 100ms debounced re-parse. The close+reopen ordering this PR was opened around could not be reproduced, and the e2e failure that led here was diagnosed and fixed separately in #15331.

Release Notes

New Features

  • N/A

Bug Fixes

  • Quarto inline output is no longer dropped when the cached output is restored while the document is being re-parsed

Validation Steps

@:quarto @:web @:win

  1. Open a Quarto document with cell output, close it, and reopen it -- the output should reappear, including on a fresh window
  2. Save an untitled Quarto document with cell output to a file, then reload the window -- the output should survive

E2E Triage Diagnosis

🟢 High confidence -- CORRECTED (supersedes the parse-race summary below): on Save As the Quarto output cache is never rebound from the untitled URI to the saved document, because _transferCacheFromUntitled is gated on this._documentUri.scheme === (file) (quartoOutputManager.ts:391) and in web a saved document URI is vscode-remote. The post-save assertion still passes via the in-memory findCacheByContentHash fallback to the live untitled cache; that fallback cannot survive a window reload, so post-reload restore finds no cache under the saved URI and mints zero view zones.
  • Test: Quarto - Inline Output: Persistence > Python - Verify inline output works in untitled Quarto document and persists through save
  • Targeted failure: Pattern A -- expect(locator).toBeVisible() on locator('.quarto-inline-output').first().locator('.quarto-output-content'), 5 occurrences / 71.4% of 7 runs on main, sles/chromium, rep sha dea62542943085c854378d6c4132f800c0c5a1ba (run 30889748402).
  • Signal: VERBOSE=1 e2e-chromium run (renderer trace + console forwarded to e2e-test-runner.log), repeat1 failing at spec line 92: 19:22:39.886 Saved output for cell 0-c2a393fb-unlabeled in untitled:Untitled-1; 19:22:40.719 Initializing for vscode-remote://.../test-...qmd (Save As -> vscode-remote scheme, so no Transferring cache from untitled to file line ever appears); 19:22:40.777 No cache file found for vscode-remote://...; 19:22:40.780 Found matching untitled cache: untitled:Untitled-1 with 1 matching cells -> Found cache by content hash match -> Restored cached outputs for 1 cells (this is what makes the post-save assert pass); 19:22:43.75 Flushing all caches / Flush complete; after reload 19:22:46.16 Initializing for the saved URI -> 19:22:46.399 No cache file found -> 19:22:46.541 No cached outputs to restore, with no untitled-cache match this time. Environment skew confirmed by mechanism: desktop saves to a file-scheme URI so the transfer runs (0.8% on win/electron) while web never does (71.4% on sles/chromium).
  • Frequency: 5/7 runs (71.4%) on main, sles/chromium
  • Hypothesis: Dropping the file-scheme requirement on the untitled->saved transition (accept any non-untitled scheme) makes the transfer write the cache under the real saved URI, so the post-reload restore finds it. The fix(quarto): restore inline output on close+reopen before parse #15091 deferral fix is a genuine but separate bug: its log lines (No cells parsed yet, waiting for parse to retry cache load -> Cells found after parse, retrying cache load, 19:22:37) show it running as designed on the untitled document without affecting this failure. Ruled out: lost debounced write (the cache service joins flushAll on onWillShutdown and Flush complete is logged before the reload); _transferCacheFromUntitled losing a parse race (it is never called at all -- no transfer log line); the deferral gate (fix present and exercised, e2e rate unchanged: pre-fix 1/3, post-fix 4/6, verbose 4/4).
  • Supersedes: PR fix(quarto): restore inline output on close+reopen before parse #15091 (closed unmerged 2026-07-23, same mechanism, first found via the dataframe close+reopen flake)

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

E2E Tests 🚀
This PR will run tests tagged with: @:critical @:quarto @:web @:win

Why these tags?
Tag Source
@:critical Always runs (required)
@:quarto PR description
@:web PR description
@:win PR description

More on automatic tags from changed files.

readme  valid tags

@midleman midleman changed the title Retry the Quarto output restore when it runs before the cells are read fix: retry the Quarto output restore when it runs before the cells are read Aug 4, 2026
@midleman
midleman marked this pull request as draft August 4, 2026 20:29
@midleman
midleman marked this pull request as ready for review August 4, 2026 23:14
@midleman
midleman requested a review from jmcphers August 4, 2026 23:15

@jmcphers jmcphers left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that rather resolving the race with a retry, we should adjust the model so that callers can distinguish between "no cells found because there aren't any" and "no cells found because parsing isn't done yet"

@midleman
midleman force-pushed the mi/quarto-restore-race-reland branch from b7fcd84 to 1da1cc3 Compare August 6, 2026 12:31
@midleman midleman changed the title fix: retry the Quarto output restore when it runs before the cells are read fix(quarto): wait for the document parse before restoring cached output Aug 6, 2026
The cached-output restore read the model's cell list without checking
whether it was current, so a restore that ran during a pending re-parse
matched nothing by content hash and completed, dropping every cached
output. The old guard inferred parse state from an empty cell list and
only covered untitled documents with no cache on disk.

Add isParsed and whenParsed() to IQuartoDocumentModel so callers can tell
"this document has no code cells" from "the cells aren't known yet", and
have the restore await the parse before either content-hash lookup.
@midleman
midleman force-pushed the mi/quarto-restore-race-reland branch from 1da1cc3 to 25ba1c7 Compare August 6, 2026 12:55
Add a regression test for the restore that runs before the document is
parsed, and one asserting that a parsed document reporting no cells
completes the restore rather than waiting for a parse that will never
add cells.

Pin the ordering of the parse wait against the content-hash fallback:
the fallback keys off the model's cells, so the hashes it receives show
whether it ran before or after the parse.

Cover the model contract directly: isParsed goes false for a pending
debounced re-parse and true once it lands, and whenParsed() returns
without waiting when the cells are already current.
@midleman
midleman force-pushed the mi/quarto-restore-race-reland branch from 25ba1c7 to 9aa080f Compare August 6, 2026 13:07
@midleman

midleman commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

I think that rather resolving the race with a retry, we should adjust the model so that callers can distinguish between "no cells found because there aren't any" and "no cells found because parsing isn't done yet"

i made the updates as suggested. the model now has isParsed and whenParsed(), so callers can tell "no cells" from "not parsed yet" instead of guessing from an empty list. the restore awaits the parse, so the retry listener is gone.

note that i removed the untitled deferral. its comment says it covers an untitled doc restored via hot exit whose content hasn't loaded yet, but UntitledTextEditorModel.resolve() creates the text model with the backup content already in it, so the model parses in its constructor and that state never happens. nothing tests it either. did you hit that case for real? if so i'll put it back.

a note: the e2e failure that sent me here was the save-as gate fixed in #15331. the real bug left is that main drops cached output whenever a cache is found while the cells are stale. the new tests fail there and pass here.

@midleman
midleman requested a review from jmcphers August 6, 2026 13:24
* if it already does. Callers that read `cells` to decide something about
* the document, rather than to react to an edit, should await this first.
*/
whenParsed(): Promise<void>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idiomatically we'd use a Barrier for this.

@jmcphers

jmcphers commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

did you hit that case for real?

I did not!

Replaces the separate timeout-field and onDidParse-event reads behind
isParsed/whenParsed with a single Barrier, so both answers come from one
object rather than two mechanisms that have to be kept in agreement.

No behavior change.
@midleman
midleman merged commit 5ea70d6 into main Aug 7, 2026
27 checks passed
@midleman
midleman deleted the mi/quarto-restore-race-reland branch August 7, 2026 13:56
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants