Repository navigation
Reduce repeated work in dev builds, tests, and checks - #12659
Merged
Merged
Conversation
…ment its declarations
…ct dbg observation line
# Conflicts: # src/compile/coordinator.zig
# Conflicts: # src/cli/main.zig
design.md's dev pack sections named ProcArtifact, RuntimeRef, canonicalize, and assembleCanonical, which the semantic audit bans in design prose. The lifted machine-code ranges are now excerpts (CodeExcerpt), the fixed image layout is arranged (arrange, assembleArranged, ArrangedImage), and the Boxy runtime table numbers are RuntimeNumber. ROC_DEV_ARTIFACT_ROUNDTRIP becomes ROC_DEV_EXCERPT_ROUNDTRIP. The rest of the audit's design.md findings are reworded without changing their meaning.
# Conflicts: # design.md # src/backend/dev/PackFile.zig # src/backend/dev/mod.zig # src/cli/main.zig # src/cli/pack_store.zig # src/compile/test/ctfe_code_identity_test.zig
# Conflicts: # src/cli/main.zig # src/cli/test/parallel_cli_runner.zig
# Conflicts: # src/cli/main.zig # src/cli/pack_store.zig # src/cli/test/parallel_cli_runner.zig
# Conflicts: # design.md # src/cli/main.zig # src/cli/pack_store.zig # src/cli/test/parallel_cli_runner.zig # src/compile/cache_cleanup.zig
…name destructured values by
…dure ABI a dev build uses
…d one, so a module's cache entry does not depend on where it is compiled
…ner holds it, so stored values keep the occurrence fresh lowering picks after unrelated edits
…an in-memory interface summary does
…sules share the checked family
… a comment-only edit changes no value
… work only when a record's payload is gone
…code, and make the subset test's edits change its roots
…ce a comment changes no root
…since an edit reruns only its own root
…d evaluates only the roots its program demands
…checking-dependency columns
…o inline away Join scalarization runs in every mode so pruning can drop unread vars carried with a construct's result, pruning deletes dead pure definitions, known-tag jump threading tracks which tuple fields an exit observes, dev inlines only compiler-provided low-level wrappers, and the ARC certifier undoes outcome restitution as commuting changes and keeps restituted parameters live to their returns. Persisted interface summaries accept a placeholder only as a field kind's slot.
… structure Inside a loop, only a header's row comes from the per-bit search; every other member is one equation step from its successors, built after them, so ARC's liveness memory no longer grows with the product of a loop's live values and its nodes. Inline tests describe the unspecialized loop shape pruning now leaves, and the float math snapshot records its new dev object hashes.
…umptions A case-insensitive filesystem reports Main.roc as main.roc, so a module named Main.roc was treated as its own owning app. Cache tests now compare store paths with the store's separators, skip the truncation Windows forbids, and report the failing step of a concurrent store.
…ed slot as busy, and run native publication allocation checks over an allocator that refuses in-place growth
…since macOS can fail concurrent non-exclusive creations of one name
Giesch
pushed a commit
to Giesch/roc
that referenced
this pull request
Oct 30, 2026
…sentatives A checked module's ConstStore persisted the Monotype named type of each stored constant as lowered, and since roc-lang#12659 a closed named type's occurrence is one representative per program: the declaring module's occurrence when it holds one, else the lowest-keyed module's. That module can be the app the package was checked from, so a later program reusing the package's cache entry found no such module and `roc build` hit the "persistent named occurrence has no checked artifact" invariant while recording a persistent interface summary (roc-lang#12693). The constant-store writer now rebinds every closed named occurrence to the storing module's own occurrence of the same canonical key, so an entry names only modules its identity covers, and stored-type restoration canonicalizes each occurrence back to the current program's representative, so restored and freshly lowered types stay one Monotype. The representative itself now comes from an index built once on the coordinator from every module view's named closed root rows and borrowed by workers, replacing the per-builder lazy scan over all module views for each distinct closed key.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem and result
Unchanged dev commands were repeating compile-time demand discovery, evaluation, specialization, test execution, and historical native-pack loading. After an edit, even when most of the program was unchanged, the compiler repeatedly rebuilt checking results, specialization constraints, recursive proofs, and native dependency graphs.
This PR consolidates the incremental-compilation work into one branch. It caches completed command outcomes, makes finer-grained checking and specialization outputs reusable, replaces historical pack scanning with indexed lazy acquisition, reduces repeated in-process analysis, and integrates existing link preparation reuse with the indexed store. It also includes two opt-in specialization-cache experiments, described separately below.
In the latest measured configuration, all nine post-edit
roc test --opt=devsamples were below one second. Post-editroc build --opt=devwas approximately 0.37–0.39s for roc-pg, 1.29–1.32s for roc-pdf, and 1.39–1.99s for Observatory. The subsecond build goal remains unmet for roc-pdf and Observatory. Observatory's first edit was close enough to two seconds that this is not a robust sub-two-second guarantee either.Measurement scope matters: the tables use a ReleaseFast compiler with both experimental caches enabled. They do not establish the performance of ordinary compiler invocations with those experiments disabled. Timings are from source commit
b0f992f1c8, before merging main at88451f17bfin merge commite6e0d57517. Post-merge validation is listed below; the timing matrix was not repeated after that merge. These are measurements of this branch, not a controlled before/after comparison against current main.General principles
roc checksemantics, and cached tests must still report failures at current source locations.base.dev_diagnostics. The new experiment switches are unavailable in ordinary release builds; this PR does not add release-readable environment variables.The design contracts are documented in
design.md. The cache formats are explicit binary formats, not JSON.What changes
1. Completed dev builds bypass repeated compile-time work
src/compile/completed_compilation.zig,src/compile/coordinator.zig,src/compile/compile_build.zig, andsrc/cli/main.zigrecord and validate completed compilation inputs and outcomes.The records include resolution/input identities, source and asset content dependencies, relevant directory entries, current module-path bindings, typed diagnostic documents, compile-time observations, demanded roots and their dependencies, literal checks, and target configuration. Input validation happens before loading checked IR when the complete result is eligible for reuse.
A valid completed result can bypass demand rediscovery, fresh compile-time evaluation, and the second specialization that normally follows fresh evaluation. It replays the previous build's actual observations instead of switching to a different command's root-selection behavior. Ordinary compilation still enforces the existing demand and finalization contracts when this complete result is unavailable.
The reused output is not an executable accepted merely because a file exists. Runtime objects and link inputs are reused; builds still relink. Complete executable reuse remains separate work.
2. Completed and individual test outcomes are reusable
src/cli/completed_test.zig, the CLI test pipeline, checked-artifact identities, and test report-site bindings cover both whole-command and individual-test reuse.Completed outcomes retain pass/fail results, required observations and diagnostics, and exit behavior. Individual outcomes are keyed by their semantic checked dependencies rather than treating an old source offset as the identity of the test. Current report sites supply current locations when an unchanged failing test moves. Cached test roots are removed from the work requiring specialization, and eligible native test procedures can be reused for remaining work.
Changes to dependencies still invalidate affected outcomes. A cached failure remains a failure; success is not the only reusable result. The PR includes CLI fixtures and CI coverage for outcome reuse and reporting relocation.
3. Reuse checking results when the consumed checking contract is unchanged
checking_interface.zig,checking_dependencies.zig,checking_outcome.zig,inference_output.zig,reference_binding.zig, andcompile/inference_cache.zigseparate the interface consumed by an importer from the implementation identity of its dependency.The checker publishes explicit immutable interface facts and inference outputs while their producers are live. Reuse validates those facts and explicitly rebinds current foreign references and owners. An implementation-only dependency change need not force importer inference when the checking contract it consumed is unchanged. Declaration layout and diagnostic/reporting changes are handled through the corresponding identities and mappings rather than retaining stale indices.
This extends diagnostic-bearing checking reuse while preserving the canonicalization-warning staging fix already on main (#12573). Pre-evaluation checking diagnostics retain their required snapshot data and explicit source dependencies; diagnostics from evaluation are not silently folded into that checking outcome.
4. Persist immutable specialization interfaces and reuse settled analyses
src/postcheck/monotype/interface_summary.zigencodes request-relative settled constraints, including composite type graphs and executable defaults with explicit checked provenance. It does not persist mutable solver IDs. Validation and transactional import precede mutation of the receiving store.The Monotype, inhabitation, fixed-point, and Lambda Solved analyses retain completed proof results with the dependencies that can invalidate them. Recursive/SCC reasoning is reused only after it is settled; a provisional cyclic assumption cannot become a cached success. Updates notify the proofs whose inputs actually changed instead of clearing all retained work.
Additional reductions include reusing interned immutable Monotypes before cross-store copying, avoiding continuation frames for already-cached checked-type requests, simplifying builtin instantiation, retaining completed erased-callable traversals within their mutation epoch, and sharing immutable expansion reachability across argument scopes. Nominal ownership and structural backings remain explicit.
Dev procedure reuse uses an explicit owned base ABI rather than multiplying reusable artifacts by caller ARC-mode demand. Dev specialization rewrites are limited to the iterator-fusion work needed by that pipeline, with iterator normalization scope represented in identities. Loop-append promotion qualifies candidates before constructing expensive lifetime topology. This trades some opportunities for caller-specific optimization for reusable dev code; it is not a claim about optimized runtime throughput across arbitrary programs.
5. Native artifacts use indexed, lazy, dependency-aware acquisition
src/backend/dev/CodeExcerpt.zig,PackFile.zig,NativeProcCompiler.zig,src/cli/pack_index.zig,pack_store.zig, andsrc/lir/pack_program.zigdefine and consume immutable native excerpts and their dependencies. TheProcArtifact→CodeExcerptterminology distinguishes reusable generated code from its arrangement into an executable image.Specialization-key offers lead to selected dependency components and payloads. The loader no longer needs to eagerly decode every historical pack before finding requested specializations. Older artifacts remain eligible when their complete identities and current-source requirements still match. Shared cyclic dependency components are represented explicitly rather than repeatedly flattened into unrelated copies.
This builds on the native procedure compatibility domain already on main (#12578): code compatibility stays separate from platform libraries and final linking, including musl/glibc host-link differences. It also preserves the specialized native-callable implementation already on main (#12574). New reuse paths carry the pending erased-call/tail-drive summaries that cached bodies need, rather than losing those producer facts on a hit.
The latest acquisition changes further reduce overhead:
Grouping reduces catalog synchronization, but still opens individual payload files. It does not yet implement the indexed multi-record storage containers proposed below.
6. Indexed storage, retained leases, and incremental cleanup
src/compile/artifact_store.zig,cache_index.zig,cache_maintenance.zig,cache_cleanup.zig, andcache_manager.zigreplace foreground whole-store sweeps with indexed acquisition and incremental maintenance.The store has a compact catalog, explicit published/staging/retiring generations, recovery for interrupted catalog updates, and leases protecting readers. Retained mappings are synchronized before reads; header sequence changes invalidate retained decoded nodes. Retention observations are batched with the exact generation they refer to. Native source-eligibility facts remain usable when a graph gains appended artifacts.
Maintenance has explicit progress/cursor state and distinguishes idle-capacity work from bounded compulsory progress, so constrained machines are not permanently exempt from cleanup. An in-progress compiler or link retains the ownership needed to prevent cleanup from removing its inputs.
7. Integrate existing link reuse with indexed storage
The immutable-object and host-inspection improvements from #12579 are already on main. This PR adapts
src/cli/link_artifacts.zigand CLI integration to the indexed artifact store: acquire a catalog generation lease before creating a scratch hard link, and publish newly materialized objects through reserved staging generations. The scratch hard link then pins the inode for the complete link. Linker entry also supplies an explicit maintenance safe point. The existing content identities and avoidance of repeated byte copies remain intact.Relinking remains intentional. Observatory's remaining link time is a significant floor even when preceding compilation work hits cache.
8. Experimental completed-body replay
Opt-in:
ROC_COMPLETED_BODY_CACHE=1, in a compiler built with-Ddev-diagnostics=true.The prototype persists a completed owned worker body shard, including its required types/names, nested lexical bodies, specialization frontier requests, and compile-time observations. Admission includes source/type/evidence/substitution/method inputs, caller and lowering policy, and compile-time value dependencies.
Static references cross the cache boundary through checked producer/value identity plus constructor/capture paths, not a previous process's
ConstNodeId. Full dispatch substitutions and their ownership are retained. Decoding validates before committing type-store transactions; owners and IDs are explicitly rebound. Deferred context-free procedure values are not incorrectly treated as lexical dependencies.The normal coordinator still resolves requests and accepts restored shards. Replay does not change demanded compile-time behavior. This is completed-body reuse, not yet a dependency-closed, per-calling-module lambda-set cache.
9. Experimental completed lambda analysis
Opt-in:
ROC_LAMBDA_ANALYSIS_CACHE=1, also requiring-Ddev-diagnostics=true.This prototype identifies the exact lifted input consumed by lambda analysis, canonicalizes its reachable type graph, and stores only completed reachable annotations. It excludes abandoned scratch rows, redirects, and lazy solver state. Named-occurrence equality and nominal ownership remain part of validation.
A restored analysis uses the current executable IR, literal values, and reporting metadata. Checked capture provenance that the solver merely carries through is obtained from the corresponding current lifted local. Local structure, symbols, types, callable topology, and ABI inputs still have to match. This removes irrelevant provenance invalidation without pretending a changed solver input is unchanged.
A real hit is inexpensive, but whole-program identity is too coarse to cover every first edit. Native object reuse changes the assembled solver input while the cache settles. In the traced Observatory sequence, function counts changed from 4,366 on cold to 2,701 on the first edit and 2,603 thereafter; the first matching analysis appeared on the third edit. Those are different inputs and must not share a result by weakening validation.
Both experiments remain disabled by default because their cold cost and reuse boundaries need further work.
End-to-end measurements
Machine: AMD Ryzen 9 3900X, 12 cores / 24 threads, x86_64 Linux. Compiler: ReleaseFast,
-Ddev-diagnostics=true -Dstrip=true. Build and test use--opt=devwith specialization enabled.roc checkhas no optimization flag. Both experiment switches above are enabled.Each project/command starts with an empty Roc artifact cache, followed by three unchanged runs and three consecutive edits. Dependencies and platform artifacts are already available; “cold” does not mean a flushed OS page cache. The edits change a small source literal or
expect, so these measurements do not guarantee the same latency for arbitrary structural/type edits.Build entry points are
roc-pg/examples/minimal/main.roc,roc-pdf/examples/quarterly-report/main.roc, androc-gui/examples/observatory/main.roc. Build edits change their database URL/report title/window title respectively. Package tests usepackage/main.roc; Observatory tests use its application entry. Test edits change oneexpectinNumeric.roc,Pdf.roc, or Observatory'sHistory.roc.The table contains 63 serial plain-command samples. Instrumented profiles are separate. Other compiler process trees were paused during measurement. Peak RSS is from GNU
time -v. No Boxy measurements are included.Wall time, seconds
All 27 plain post-edit samples are below two seconds, but the separate instrumented first Observatory edit was 2.031s. All post-edit test samples meet the one-second target. Warm Observatory builds still miss the original 250ms target slightly. The third Observatory edit is distinguished because it is the first whole-analysis hit in this sequence.
Peak RSS, MiB
Warm/edit columns are the maximum of their three plain samples, not a sum across runs.
Cold build peaks are 2.14 GiB, 3.80 GiB, and 1.50 GiB respectively. The most concerning cold-memory case is roc-pg test, at 10.69 GiB.
Controlled comparisons isolating the experiments
These comparisons each use the same compiler on both sides, but were performed at different stages of the branch. Their savings must not be added together or interpreted as a comparison against main.
The analysis comparison alternated disabled/enabled runs and verified actual hits with counters on both sides. Enabled peak RSS was about 95 MiB lower. One steady Observatory analysis record was about 3.38 MB and restored in roughly 17–23ms.
The isolated grouped-acquisition probe reduced 8,000 steady acquisitions from approximately 96–97ms to 72ms and catalog-header reads from 8,000 to 341. This is a microbenchmark, not an end-to-end saving claim.
Cold-build and disk tradeoffs
The experiments spend time and space recording results that subsequent builds can consume. Their cost is not negligible, and the default remains off.
A controlled same-compiler cold roc-pg test comparison isolated completed-body caching:
That is 5.76s more time and 1.22 GiB more peak RSS, with 320,437,765 bytes of extra body records (about 305.6 MiB). This is not all disk-write time: the representation assembled for the second lambda solve also changed, with solved types increasing from 23,250,385 to 24,585,052 and expansions from 9,331,521 to 10,175,911. The exact attribution between encoding, retained graphs, and additional solving has not been isolated.
The final roc-pg cold-test measurement is 68.105s. It uses a later compiler and a different analysis-cache setting, so its difference from the earlier comparison is not an isolated regression attributable to one change. There is no controlled current-main baseline establishing the net cold cost of this entire PR.
Retained artifact payload size after the measurement sequence
The inventory below was captured after cold + three warm + three edits + an additional instrumented edit, before cleanup. It includes retained versions and is not cold-only write volume or incremental overhead versus main.
It counts artifact
*.datafiles. “Logical” is file length; “allocated” is filesystem blocks for those payloads. Catalogs, leases, scratch objects, output executables, package downloads, and platform libraries are excluded. Body/analysis columns are subsets of all payloads.Coarse completed bodies are relatively cheap to consume but duplicate reachable information across records. Whole-analysis records are compact enough to restore quickly, but can accumulate when the assembled program changes. Retention policy and cleanup bound stored history; they do not remove the need to reduce duplicate cold work and representation size.
Remaining work, in recommended order
For context, the final extra Observatory edit profile has approximately 122ms checking, 445ms Monotype lowering (including the 287ms native query), 18ms lambda analysis, 172ms LIR generation, 82ms ARC, 103ms backend, and 215ms linking. roc-pdf has approximately 187ms checking, 574ms Monotype lowering (including 477ms native query), 3ms lambda analysis, 54ms LIR generation, 26ms ARC, 139ms backend, and 39ms linking. Different instrumentation scopes are not additive: provider-lifetime counters can include consumers outside coordinator lookup, and worker wait includes the workers' computations rather than demonstrating idle time.
Validation
Before the main merge, on the measured source:
After merging main at
88451f17bf:e6e0d57517.git diff --check origin/mainpassed; the working tree is clean.MiniCI and aggregate eval suites were not run locally. No Debug Roc compiler or Boxy measurements contributed to the results. Investigation reports and machine-local reproduction scripts remain outside the repository.
For reproducibility, the measured compiler SHA256 is
f7e8c52866befbf079d98c4f27452d9cafa98200f68577e7513cd56b73f72d96. Its version string retainsrelease-fast-60830bd9because the frozen build snapshot preceded committing the final acquisition change. A SHA256 audit verified all 1,125 tracked compiler/build/design files in that snapshot againstb0f992f1c8. Raw timings, GNU-time resources, profiles, commands, and artifact inventories are retained under/home/rtfeldman/code/roc-dev-investigation/2026-10-27/rebuild-followup/; publication notes are in the adjacentpr-publication/directory.