Skip to content

fix(studio-server): a project's signature no longer freezes the host while it walks the files - #7073

Merged
miguel-heygen merged 3 commits into
mainfrom
fix/project-signature-never-blocks
Nov 3, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
fix/project-signature-never-blocks

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

What

Working out a project's signature no longer holds the host's event loop. The signature is what preview caching and the /projects/:id/signature poll compare against. Every request-path computation in studio-server now uses the async walk that already exists (createProjectSignatureAsync), and an adapter's getProjectSignature may return a Promise. For a host that runs studio-server in its own main process, a large project no longer freezes the whole app while each file is checked.

Why

createProjectSignature walks every file in the project with a synchronous lstat before it consults its cache. The cache saves the hashing, never the walk. On a project with many files, or a machine with slow disk or on-access scanning, that walk runs for tens of seconds. While it runs, nothing else on the event loop moves.

That walk ran synchronously on these request paths:

  • GET /projects/:id/signature: a poll target, so one full walk per poll.
  • A preview build: up to three walks.
    • at the request start;
    • again inside injectStudioPreviewAugmentations;
    • once more to check the built page against disk.
  • A thumbnail: a fresh signature plus the composition input signatures.

A desktop host measured main-process stalls of 18 to 125 s inside this walk.

Related work

None.

How

  • types.ts: getProjectSignature?: (projectDir) => string | Promise<string>. Adapters that return a string are unchanged.
  • resolveProjectSignature is async. It awaits the adapter's value and falls back to createProjectSignatureAsync. resolveProjectAndSignature and /projects/:id/signature await it.
  • preview.ts:
    • injectStudioPreviewAugmentations takes the signature the route already resolved, instead of walking again. That is one walk fewer per build.
    • The check that the built page still matches disk uses the async walk.
  • compositionInputSignature is async and uses the async walk. The thumbnail route awaits it, keeping the original order of its unchanged-inputs checks.
  • createProjectSignature (sync) stays exported for callers that need it outside a request.

Behaviour note: the preview page's injected signature is now the one its request resolved, rather than one read again after bundling. If files change during the build, the page carries the older signature, so the next poll refreshes it once. Before, it could carry a newer signature than its content.

Test plan

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable): no documented behaviour changes
  • Comments follow CONTRIBUTING.md "Comments"

fails without the fix: routes/projects.test.ts, "lets the event loop run while it walks a project". It writes a 300-file project and counts setImmediate turns while GET /projects/:id/signature answers. With projectSignature.ts and projects.ts restored from main it fails (expected 0 to be greater than 0). With the fix it passes, 3 runs in a row.

Run on a Linux build box at this head:

  • The whole packages/studio-server vitest suite: 1,602 passed, 2 skipped, exit 0.
  • tsc --noEmit for studio-server passed.
  • oxlint passed on the changed files.
  • oxfmt --check passes on every changed file. The first push was unformatted: the formatter's rewrites stayed on the build box, and the checks that followed in CI failed on that.
  • fallow audit --base origin/main passed.
  • The comment ratchet could not run on that box: its git has no check-attr --source. The diff adds no comment lines to product source; the only new comments are in a test, which the ratchet skips. CI runs it.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes at 136f67c5. The code is sound. The blocker is formatting, and it fails CI.

Blocker: two changed files fail oxfmt --check, so CI is red at this head

The body says oxfmt passed on the changed files, but I get a failure locally and so does CI. CI → Format fails, and so does Preflight (lint + format) in preview-regression and Windows render verification. Every other failure on this head (Tests/Render on windows-latest, preview-regression "Check results") comes after that preflight step. Both files are clean on main.

  • packages/studio-server/src/routes/preview.ts ~675: the injectStudioPreviewAugmentations(...) call was split by hand with the options object left unindented. oxfmt wants injectStudioPreviewAugmentations(html, signature, project.dir, compPath, {.
  • packages/studio-server/src/helpers/projectSignature.ts:267: the return (await adapter.getProjectSignature?.(projectDir)) ?? createProjectSignatureAsync(projectDir); line is too long; oxfmt wraps it in parentheses.

Run npx oxfmt on those two files. No logic change is needed.

Verified

  • Fails without the fix: with projectSignature.ts and projects.ts restored from main, "lets the event loop run while it walks a project" fails with expected 0 to be greater than 0. It passes at this head.
  • projects, compositionInputs, thumbnail* and preview* tests pass: 203/203.
  • No caller outside studio-server breaks on the newly async signatures. Nothing else calls resolveProjectSignature or compositionInputSignature, and both adapters (cli and the studio vite adapter) return a string, which still fits string | Promise<string>.
  • Passing the signature the route already resolved into injectStudioPreviewAugmentations is safe, and the stale-on-change direction in the behaviour note is the right one. The thumbnail route still runs the unchanged-inputs checks in their original order and short-circuits as before.

Non-blocking

  • The CLI adapter's getProjectSignature (packages/cli/src/server/studioServer.ts:457) is still a synchronous createProjectSignature behind a cache that every relevant watcher event clears. With the CLI server, the first poll or preview after each edit still walks synchronously. That is outside this PR's stated scope (hosts that use studio-server's own fallback), but a follow-up that makes that cache async would extend the fix to the CLI host.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 113 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

Unstable (1)

  • seqplainkeys-none-pct-r0-nested-z100: tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Formatted at fce1433: preview.ts and projectSignature.ts as you described, plus projects.test.ts, the only other changed file oxfmt --check flagged. My first run formatted the files on the build box and never brought the rewrites back, which also made the body wrong; the body is corrected. No logic change. On the CLI adapter's synchronous cached signature: agreed, it's a follow-up for the CLI host and outside this change.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approve at fce14330.

My blocker was formatting. The diff from 136f67c5 (an ancestor of this head) is formatting only: the oxfmt wrapping in projectSignature.ts:267 and preview.ts ~675, plus one wrapped loop line in projects.test.ts. No logic changed.

  • oxfmt --check is clean on all 8 changed files, locally and in CI: CI → Format and Windows render verification → Preflight (lint + format) both pass. The Comments check passes too.
  • projects.test.ts and compositionInputs.test.ts pass at this head. The fails-without-the-fix check I ran in the first review still applies, since the logic hasn't changed.

The non-blocking note from my first review still stands: the CLI adapter's cached signature is still computed synchronously on a cache miss.

@miguel-heygen
miguel-heygen force-pushed the fix/project-signature-never-blocks branch from fce1433 to a47ff86 Compare November 3, 2026 08:50

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approve at a47ff863, rebased onto main after #7041.

  • Rebase: the only conflict was injectStudioPreviewAugmentations. It now takes adapter and signature: adapter stays because #7041 reads adapter.runtimeUrl for the early-errors script, and signature replaces the second resolveProjectSignature walk. All four call sites pass the signature their route already resolved. Outside preview.ts, the diff against main is line-for-line the same as at fce14330, the head I approved.
  • Checks: oxfmt --check is clean on all 8 changed files. tsc --noEmit passes for studio-server. The projects, compositionInputs, thumbnail* and preview* tests pass (205/205). In CI so far, Comments, preview-regression, regression and PR captures pass and no job has failed; CI, Windows render verification and CodeQL were still running when I approved.

The earlier non-blocking note still stands: the CLI adapter's cached signature is still computed synchronously on a cache miss.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit ce9830f Nov 3, 2026
133 of 134 checks passed
@miguel-heygen
miguel-heygen deleted the fix/project-signature-never-blocks branch November 3, 2026 09:33
@WaterrrForever WaterrrForever mentioned this pull request Nov 3, 2026
1 task done
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.

1 participant