Skip to content

perf: use a custom trie for path conflict validation - #302

Merged
niemeyer merged 15 commits into
canonical:mainfrom
letFunny:performance-tree
Sep 28, 2026
Merged

niemeyer merged 15 commits into
canonical:mainfrom
letFunny:performance-tree

Conversation

@letFunny

Copy link
Copy Markdown
Collaborator
  • Have you signed the CLA?

@letFunny
letFunny force-pushed the performance-tree branch 3 times, most recently from d4113ef to b8da2e8 Compare June 10, 2026 11:29
@letFunny
letFunny requested a review from Copilot June 10, 2026 15:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Introduces a trie-based conflict detector for slice path collisions and expands tests to cover more glob/double-glob conflict cases.

Changes:

  • Replaced the O(n²) glob/generate conflict scan in Release.validate() with a pathConflictTree-based detector.
  • Added extensive regression tests for glob conflicts and new focused unit tests for the conflict tree/segmenter.
  • Exposed internal conflict-tree types/helpers to external tests via export_test.go.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/setup/setup.go Switches validation to the new conflict-tree implementation.
internal/setup/conflict.go Adds trie-based conflict detection and path segmentation logic.
internal/setup/setup_test.go Adds multiple new test cases for glob/double-glob conflicts.
internal/setup/conflict_test.go Adds unit tests for pathToSegments and the conflict tree structure.
internal/setup/export_test.go Exports internal symbols to support setup_test package tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict_test.go Resolved
@letFunny
letFunny requested a review from Copilot June 12, 2026 08:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
@letFunny
letFunny marked this pull request as ready for review June 12, 2026 08:46
@letFunny

letFunny commented Jun 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

After some very light work on performance to improve the algorithm without added complexity (basically caching some data to avoid map lookups, iteration order, etc. *) the performance is:

Benchmark 1: BASE
  Time (mean ± σ):     14.511 s ±  0.071 s    [User: 15.039 s, System: 0.079 s]
  Range (min … max):   14.411 s … 14.617 s    10 runs
 
Benchmark 2: HEAD
  Time (mean ± σ):     151.2 ms ±   3.1 ms    [User: 181.2 ms, System: 18.6 ms]
  Range (min … max):   143.9 ms … 155.8 ms    19 runs
 
Summary
  'HEAD' ran
   95.96 ± 2.00 times faster than 'BASE'

*: All verified to reduce the number of cache misses using perf which is why the time also improves reliably.

@upils upils left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @letFunny ! I am amazed at how faster this is. I even looked at the messages we display to the user to see if we could remove any as this is now pretty much instantaneous.

The overall approach looks very sane. I could not find corner cases not handled properly. I have suggestions and questions but nothing that would change the approach.
This is definitely more complex than the previous implementation but the massive performance gains justify it I think.

Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict_test.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict.go Resolved
Comment thread internal/setup/conflict_test.go Resolved
Comment thread internal/setup/conflict.go Resolved
@letFunny
letFunny requested a review from upils June 18, 2026 15:07

@upils upils left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @letFunny. This is approved even though I think segmentEnd could be a bit more tweaked as suggested in the other comment. Feel free to move it to core review if you want to keep it like that or after this is fixed.

// explicit termination in the trie to distinguish them from parent directories
// of other paths.
func pathToSegments(path string) ([]segment, error) {
if path[0] != '/' {

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.

Suggested change
if path[0] != '/' {
if path == "" || path[0] != '/' {

otherwise path[0] panics

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.

i guess the error message needs adjusting or there should be two checks with different error messages then

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.

aah! damn! sorry @letFunny / @upils i did not know the bot would do this. 😬 will do a comment and ping you in the future.

Screenshot 2026-07-10 at 07 55 19

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@lczyk lczyk Jul 10, 2026 •

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.

yep. the legitimate case i found was my own validation of this PR for which i wrote some fuzzers, and just directly called the fun. can be easily added if we ever add fuzzers here.

for reference, my fuzzers

all clean on 5m runs

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ty for looking into this

@lczyk lczyk 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.

i did some fuzzing to validate this PR and all looks green on 5m run. +1 from me :)

for reference, my fuzzers

@lczyk

lczyk commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

@letFunny FYI found some more speed (and memory) letFunny#29. i promise i'm going to stop tinkering now. 😅

@lczyk

lczyk commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

adding 'priority' because it's unbearable to open a new release and wait for all the chisel cut's.

so far ~413k cuts. we're ~1/2 way through, so ~830k for the whole 26.10 opening. each cut spends ~28s of its ~32s re-validating the release; with this PR that drops to ~0.3s. 830000 * 28 / 3600 ~= 6.4k vcpu-hours. some jobs can parallelise cuts a bit, so this actually goes down to ~120-220h of wall-clock time.

lczyk added a commit to lczyk/spread-bread that referenced this pull request Sep 27, 2026
`CHISEL_HACKS=1` used to make `chisel-hacked` skip release validation
entirely. now it runs the full `validate()`, with the trie from
canonical/chisel#302 in place of the quadratic path conflict loop, plus
the letFunny/chisel#29 speedups on top. with `CHISEL_HACKS` unset it
behaves like upstream.

release | before (no skip) | trie | no validation
---|---|---|---
22.04 | 2.2s | 43ms | 42ms
24.04 | 16.7s | 131ms | 96ms
26.04 | 13.6s | 87ms | 59ms
26.10 | 11.8s | 74ms | 48ms

@niemeyer niemeyer 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.

Alright, thanks for your input @lczyk, and for exercising the PR. I have not had the time to sit down and review the heart of the algorithm in detail, but the algorithm is the product of extensive discussions on the topic that we had at the last sprint, so I'm comfortable with the theory behind it, and the surface that this touches on existing code is minimal, besides being functional and side-effect free in nature. The API and the testing is also elegant. So I'm giving it a vote of faith so we can stop the pain and benefit from the dramatic performance improvement.

@niemeyer
niemeyer merged commit b8b8016 into canonical:main Sep 28, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority Look at me first

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants