Skip to content

perf: Start untracked-file discovery before the package graph is built - #13258

Merged
anthonyshew merged 1 commit into
shew/defer-transitive-closuresfrom
shew/early-untracked-scan
Jul 6, 2026
Merged

anthonyshew merged 1 commit into
shew/defer-transitive-closuresfrom
shew/early-untracked-scan

Conversation

@anthonyshew

Copy link
Copy Markdown
Contributor

Why

Untracked-file discovery is the largest single block on the time-to-first-task critical path (~98ms on a 1191-package/43k-file monorepo), and it waited for the package graph solely to compute per-package scan prefixes. That dependency turns out to be vestigial: the scan scope was always exactly the repo-root subtree — every package lives under the repo root (enforced at discovery), the root package's prefix is always in the prefix set, and UntrackedScope deduplicates nested prefixes. The per-package prefixes never restricted anything beyond the repo root's own prefix.

What

  • The untracked scan runs on the same background task that performs SCM detection and builds the tracked index, using the repo-root prefix directly (repo_prefix_for_repo_index: repo root anchored to the git root — empty for the common case, the subtree prefix when the repo is nested in a larger git repository). It starts as soon as the tracked index exists (~32ms) instead of after graph construction (~87ms).
  • The SCM handle is sent back over a oneshot channel the moment detection finishes, so the main flow (graph build, turbo.json loading, engine construction) never waits behind the scan. The index is joined where it was before, at the first consumer.
  • all_package_prefixes is deleted; the prefix-anchoring tests are repurposed to cover the repo-root prefix in both the nested-git-root and matching-root layouts.

How to verify

  • Scan-scope equivalence argument above; turborepo-scm's untracked/regression suites (191 tests) and turborepo-lib (432) pass unchanged; --dry=json byte-identical on the monorepo above.
  • Timeline on that monorepo: scan moves from ~87→185ms to ~32→125ms; with perf: Memoize resolved task definitions during engine construction #13257's engine memo the scan+engine phase of the critical path shrinks ~188ms → ~165ms, and engine construction itself drops ~73ms → ~40ms from reduced core contention. TTFT medians move less than the phase numbers on a saturated 4-core machine (~246 → ~238ms) because a previously-hidden serial segment between graph completion and engine start now gates; that segment is the next target.

Stacked on #13250. Compounds with #13251 and #13257.

Untracked-file discovery waited for the package graph so it could
compute per-package scan prefixes. But the scan scope was always exactly
the repo-root subtree: every package lives under the repo root (enforced
at discovery), the root package's prefix is always in the set, and
UntrackedScope deduplicates nested prefixes — so the per-package
prefixes never restricted anything beyond the repo root's own prefix.

The scan now runs on the same background task that performs SCM
detection and builds the tracked index, using the repo-root prefix
directly, and starts as soon as the tracked index exists instead of
after graph construction. The SCM handle is sent back over a oneshot
channel so the main flow proceeds without waiting behind the scan; the
index is joined where it was before, at the first consumer.

On a 1191-package/43k-file monorepo the scan now runs at ~32-125ms
instead of ~87-185ms, moving engine construction and task hashing off
its tail: the scan+engine phase of the critical path shrinks from
~188ms to ~165ms, with reduced core contention also speeding the
engine build itself. Dry-run JSON is byte-identical.
@anthonyshew
anthonyshew requested a review from a team as a code owner July 5, 2026 04:12
@anthonyshew
anthonyshew requested review from tknickman and removed request for a team July 5, 2026 04:12
@vercel

vercel Bot commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
examples-basic-web Ready Ready Preview, Comment, Open in v0 Jul 5, 2026 4:13am
examples-designsystem-docs Ready Ready Preview, Comment, Open in v0 Jul 5, 2026 4:13am
examples-gatsby-web Ready Ready Preview, Comment, Open in v0 Jul 5, 2026 4:13am
examples-kitchensink-blog Ready Ready Preview, Comment, Open in v0 Jul 5, 2026 4:13am
examples-nonmonorepo Ready Ready Preview, Comment, Open in v0 Jul 5, 2026 4:13am
examples-svelte-web Ready Ready Preview, Comment, Open in v0 Jul 5, 2026 4:13am
examples-tailwind-web Ready Ready Preview, Comment, Open in v0 Jul 5, 2026 4:13am
examples-vite-web Ready Ready Preview, Comment, Open in v0 Jul 5, 2026 4:13am
turbo-site Ready Ready Preview, Comment, Open in v0 Jul 5, 2026 4:13am
turborepo-eve-agent Ready Ready Preview, Comment, Open in v0 Jul 5, 2026 4:13am

@anthonyshew
anthonyshew merged commit 53b8b7b into shew/defer-transitive-closures Jul 6, 2026
42 checks passed
@anthonyshew
anthonyshew deleted the shew/early-untracked-scan branch July 6, 2026 12:25
anthonyshew added a commit that referenced this pull request Jul 6, 2026
## Why

On a 1191-package monorepo with a 5.4MB pnpm-lock.yaml, the fast-parse
tier spent ~57ms walking the document with one sequential scanner —
almost all of it in three top-level blocks of comparable size
(`importers` ~22ms, `packages` ~16ms, `snapshots` ~10ms). That parse
sits at the front of every command that builds the package graph.

## What

- Split the document positionally at column-zero key lines; each
top-level block is an independent subtree for the line scanner, so
blocks parse concurrently as standalone mini-documents.
- Re-split the three big sections at their child keys into core-count
chunks.
- Anything the splitter can't prove safe — and any fragment the scanner
rejects — falls back to the existing sequential tiers, so the parallel
path is a pure optimization: same accepted language, same duplicate-key
semantics (`set_once` across sections, insert-checks within them).

Isolated parse: **57.2ms → 20.5ms** median; in-process `parse_lockfile`
span **~81ms → ~29ms**.

## How to verify

- Differential tests now run the parallel splitter on the whole corpus
alongside the scanner/saphyr/serde tiers, plus new tests for split
boundaries, duplicate keys across fragments and chunks, and a synthetic
lockfile large enough to cross the production size gates. `cargo test -p
turborepo-lockfiles`: 243 passing.
- `--dry=json` output on the monorepo above is byte-identical.
- Honest scope note: time-to-first-task on that particular repo is
unchanged, because since #13250/#13258 the parse there already overlaps
the untracked-file scan. The win is CPU time and the critical path for
repos where lockfile parse is the gate (bigger lockfiles, smaller file
counts, `turbo prune`, cold starts).

This branch was successfully deployed

1 active deployment
Preview – turborepo-eve-agent — fcce8a29 Deployed Jul 5, 2026 by vercel[bot]
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