Skip to content

perf: cut serial compile work and duplicated parsing on dense projects - #1130

Merged
kvlonge merged 29 commits into
mainfrom
perf/chi-607-orchestration
Oct 17, 2026
Merged

kvlonge merged 29 commits into
mainfrom
perf/chi-607-orchestration

Conversation

@kvlonge

@kvlonge kvlonge commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Why

Cold compiles of large dense projects spent most of their wall time in serial main-thread work and duplicated parsing. The dense 10k all-rules CI measurement was 44.1 s on 0.157.0.

Changes

  • Rules reuse their parser tokens; SQL-test planning recognises bounded templates without full ASTs (Snowflake-safe markers) and copies less.
  • SQL scanners dispatch on the opening byte; internal rules cache keys use BLAKE3; artifact batches are written on a small pool.
  • Analysis hashes batch-wide settings once per batch instead of per member.
  • Discovered model files, headers, SQL and paths stay native; hook-independent header facts are read in a parallel pre-pass, and post-analysis consumers read native fields. Pools that cannot start fall back to in-turn work.
  • Built-in rules start speculatively after type recovery for projects without custom rules, with deferred cache writes and an identity check at the normal call site; failures are treated as misses.
  • Additive compile timings for the early rules start.

Verification

  • Compiled SQL and compile JSON (minus timings) identical on the dense 1k fixture after every change; new Rust, integration and real-CLI DuckDB tests for native headers and early rules.
  • Paired local cold runs and instruction counts per change; independent reviews of the native data and early rules changes, findings fixed.
  • CI dense 10k all-rules: 44.1 s (0.157.0) to 34.1 s at c041112.

…thon enum reads natively and read record fields from the native spec
…num and factory lookups

Header columns, audit instances and measurement thresholds are built with
positional arguments in their declared field order, verified once against the
registered record specs, instead of a per-object keyword dict. The model loop
keeps header columns and audits as vectors rather than round-tripping through
temporary tuples, derives direct audit identities from the parsed nodes, skips
enum column resolution when no enums are visible, and lists audit factories
only for headers that reference them.
A run selecting no rules now validates its rules configuration through a
model-free native request on the calling thread instead of spawning the
native rules thread, rebuilding the scope index and every model's rules
facts, and replaying the project's models to count unevaluated ones.
…und trip

The built-in rules request converted the native scope metadata projection
into Python dictionaries only to read them straight back into JSON values.
Convert the projection to the same sorted JSON value natively instead.
Rule-cache, custom-rule-cache, rows-memo and model-finding identities are internal, build-scoped keys. Replace software SHA-256 with BLAKE3 and stream model JSON into the hasher instead of buffering it. Bump the three cache versions so old entries are ignored.
Large batches of distinct files are written on four threads. The first failure in batch order is still the one reported, unchanged files are still counted, and batches that repeat a path stay sequential so the last queued contents win.
Shared-result keys and compact-batch template deduplication re-serialised the batch's function return types for every member. Hash the batch-wide settings once and keep them out of the per-member template identity; the request sent to the analysis pool is unchanged.
Model files keep their contents, query, header values and path text as Rust data
and build the Python dict, strings and pathlib objects only when Python reads them.
Model input records share the native query and expansion texts instead of Python
strings, and internal readers use the native fields directly.
Before the ordered model loop, a fixed four-worker pool reads every model's
native header off the GIL: template and macro presence and config-reuse checks
per header entry, and the header's columns. The ordered loop uses a fact only
for a config value that is the header's own object, so values that defaults,
templates or hooks replaced are still read from Python. Header columns parsed
natively build their Python records only when Python reads them.
Model facts share the model file's query and authored text instead of copying
them. Compiled artifacts, metadata checks and resource SQL requests read a
native model's name, path and SQL directly, and Python reads of a model's
query and file text share one lazily built string. Header facts and their
native node reader move to the discovery crate with Rust tests.
Compiles with several failing models report the first failure in model order,
native and templated header columns produce the same contract diagnostics, and
native column audits run against DuckDB.
Model file paths build their pathlib objects natively on first read, header
column parsing helpers move under the discovery helpers, the unused whole-config
macro walk is removed in favour of the per-entry walk, natively parsed columns
use the default contract classes, and pending Python-crossing budgets drop to
the new counts.
A failed pool creation was cached and failed every later compile in the process. Compute the header facts on the calling thread instead, so compiling never depends on spawning the pre-pass workers.
Add a speculative rows evaluation to the rules engine: it runs on its own
thread, holds its bulk cache and response memo writes, and answers the real
request only when that request has the same key, making the held writes in
the original order. A stop flag ends waiting and model evaluation early.
Add early_rules_build_ms, early_rules_wait_ms, early_rules_hits and
early_rules_misses to compile_timings.
After type recovery the compile builds the built-in rules request from the
recovered project and starts its evaluation on a background thread. The
rules phase builds its request as before and uses the early result only when
the request keys match. Projects with custom rules, model selections and
failed graphs keep the existing path.
A thread that cannot start, or a speculative evaluation that fails, no longer fails the compile: the rules phase evaluates the request as before.
@kvlonge kvlonge changed the title perf: cut serial compile work and Python round trips on dense projects perf: cut serial compile work and duplicated parsing on dense projects Oct 17, 2026
@kvlonge
kvlonge marked this pull request as ready for review October 17, 2026 22:33
@kvlonge
kvlonge enabled auto-merge (squash) October 17, 2026 22:33
@kvlonge
kvlonge merged commit 409e328 into main Oct 17, 2026
43 checks passed
@kvlonge
kvlonge deleted the perf/chi-607-orchestration branch October 17, 2026 23:11
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