Speed up loading bundles with many included files - #6195
Open
janniklasrose wants to merge 2 commits into
Open
Conversation
Contributor
Approval status: pending
|
Each file in `include` was applied as its own mutator. Entering a mutator converts the whole configuration between its typed and dynamic representations, so file N re-converted the N-1 resources already merged, making load quadratic in the number of included files. A bundle with 6000 job files spent ~20 minutes in configuration load before any API call. Apply the per-file includes within the enclosing mutator scope instead, so the configuration is converted once per load. `ProcessInclude` merges through `config.Root.Merge`, which keeps both representations in sync, so it does not need a scope of its own. The expanded include list now goes through `Mutate` for the same reason: nothing converts the typed field back afterwards. Measured with `bundle validate` on generated bundles of 8-task jobs, one job per file: 3000 files 301s -> 35s, 5000 files 1237s -> 93s, 6000 files 1208s -> 131s. Merging itself is still quadratic: `mergeMap` allocates a new mapping and copies every existing entry, so file N also copies the N-1 resources already merged. That is untouched here and becomes the dominant term, so load is much faster but not linear. Co-authored-by: Isaac
janniklasrose
force-pushed
the
janniklasrose/bundle-large-number-of-includes
branch
from
August 7, 2026 08:31
22fb541 to
66cb8ab
Compare
Collaborator
Integration test reportCommit: f448601
8 interesting tests: 4 RECOVERED, 4 SKIP
Top 3 slowest tests (at least 2 minutes):
|
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.
Summary
Each file in
includewas applied as its own mutator. Entering a mutator converts the whole configuration between its typed and dynamic representations (config.Root.MarkMutatorEntry→convert.FromTyped), so file N re-converted the N-1 resources already merged — making configuration load quadratic in the number of included files.This came from a customer bundle with ~6000 jobs, one job per YAML file (the documented layout), plus ~18000 notebooks. Their CI spent ~29 minutes in configuration load before any API call.
Fix
Apply the per-file includes within the enclosing mutator scope, so the configuration is converted once per load rather than once per file.
ProcessIncludemerges throughconfig.Root.Merge, which keeps both representations in sync, so it does not need a scope of its own.The expanded include list now goes through
Mutatefor the same reason: with no per-file scope, nothing converts the typed field back into the dynamic tree afterwards, and the nextToTypedwould restore the raw glob patterns. An empty list is dropped rather than written as[], since the typed field isomitempty.Results
To size this, I generated bundles matching the customer's shape — real files on disk, one 8-task job per YAML file across nested directories, 4 wildcard include patterns, plus ~18000 notebook files at the 6000-job size. Measured with
bundle validate(the load path, no deploy) on an Apple M4 Max:At the customer's size, ~20 minutes becomes ~2 minutes (~89% less). Before the fix, doubling the file count roughly quadrupled the time; repeated runs at 3000 files spanned 301-316 s, so the growth is well outside measurement noise. A CPU profile at 800 files attributed 22% of samples to
MarkMutatorEntry, nearly all underconvert.FromTyped, plus the allocation and GC churn it caused.Defining several jobs per file is an effective workaround for affected bundles and needs no CLI change: 1000 jobs one-per-file took 36 s, and the same jobs in a single file took 1.5 s.
What this does not fix
merge.Mergeis independently quadratic —mergeMapallocates a new mapping and copies every existing entry, so file N also copies the N-1 resources already merged. That is untouched here and becomes the dominant term once this change lands, so load is much faster but not linear: at 100/200/400 files, time per file still rises from 91 µs to 120 µs to 159 µs.A prototype that accumulates included resources into a single mapping instead of folding each file into the whole configuration was linear across 500-6000 files and reached ~1.2 s at 6000 files. It is deliberately not part of this PR: it has to preserve the merge semantics the current pairwise merge provides, and that deserves review on its own. Worth recording for whoever picks it up — a naive version that replaces instead of deep-merging on key collision drops fields from the earlier definition and collapses the accumulated locations, which also stops
validate:unique_resource_keysfrom reporting duplicate resource keys, since it detects them by counting locations per key.One behavior change worth flagging: per-include mutator timings are no longer recorded individually in telemetry; they roll up into
ProcessRootIncludes.Tests
Three unit tests in
process_root_includes_test.go, covering what this change could plausibly break:bundle validate -o jsondoes not gain an emptyinclude(my first attempt at the above regressed this)Full unit suite (6235 tests) and acceptance suite (2524) pass.
bundle/templates/lakeflow-integrationsfails identically on a clean tree in my environment (local Python/uv build issue), unrelated to this change.This pull request and its description were written by Isaac.