Skip to content

bundle: Update bundle roots conflict detection algorithm.#8664

Merged
philipaconrad merged 1 commit into
open-policy-agent:mainfrom
philipaconrad:philip/bundle-overlap-algorithm
May 18, 2026
Merged

bundle: Update bundle roots conflict detection algorithm.#8664
philipaconrad merged 1 commit into
open-policy-agent:mainfrom
philipaconrad:philip/bundle-overlap-algorithm

Conversation

@philipaconrad
Copy link
Copy Markdown
Member

@philipaconrad philipaconrad commented May 16, 2026

What code changed in this PR?

This commit is a change of algorithm for how we detect conflicts between the roots of multiple bundles. The old algorithm was an O(N^2) all-to-all root paths comparison. The new algorithm changes this to a string sorting and scanning process, which results in O(N log N) string comparisons, and fewer string split operations.

The benchmarks included with this commit indicate a meaningful improvement across nearly all cases tested, with even the pathological cases showing massive improvement in runtime and memory usage/allocs.

The algorithm here is a backport of the one I figured out for Swift OPA, over in open-policy-agent/swift-opa#110

Notes for Reviewers

  • Around half of the new code is just the benchmarks. (~200 LOC)
  • Around 20-30% of the new lines in v1/bundle/store.go are comments explaining what's going on for each step, so that it's hopefully clear why the new algorithm works the same as the previous one, despite doing the actual conflict detection work very differently.

How to test?

  • Existing bundle and store tests all pass.
  • New benchmarks are available in v1/bundle/store_bench_test.go to exercise the conflict detection algorithm. The differences are stark (some cases as much as a 95%+ speedup over the original algorithm), and scales as bundle counts loaded on the OPA instance increase.

Benchmark Results

benchstat results (Click to expand)

On a 2024 M4 MacBook Pro this is what I got from running benchstat on the results. The benchmarking commands were of the form:

  • go test -tags=opa_wasm ./v1/bundle/ -run '^$' -bench '^(BenchmarkHasRootsOverlap|BenchmarkHasRootsOverlapWithStore)$' -benchmem -benchtime=2s -count=15 > run.txt
goos: darwin
goarch: arm64
pkg: github.com/open-policy-agent/opa/v1/bundle
cpu: Apple M4 Max
                                                       │    main.txt    │               pr.txt                │
                                                       │     sec/op     │   sec/op     vs base                │
HasRootsOverlap/disjoint/10-16                              4.638µ ± 1%   1.095µ ± 0%  -76.39% (p=0.000 n=15)
HasRootsOverlap/disjoint/100-16                            414.70µ ± 1%   11.78µ ± 0%  -97.16% (p=0.000 n=15)
HasRootsOverlap/disjoint/1000-16                          41813.5µ ± 1%   155.5µ ± 1%  -99.63% (p=0.000 n=15)
HasRootsOverlap/disjoint/10000-16                        4293.300m ± 1%   2.017m ± 1%  -99.95% (p=0.000 n=15)
HasRootsOverlap/identical/10-16                            12.553µ ± 0%   1.980µ ± 0%  -84.23% (p=0.000 n=15)
HasRootsOverlap/identical/100-16                          1164.09µ ± 0%   24.03µ ± 0%  -97.94% (p=0.000 n=15)
HasRootsOverlap/identical/1000-16                        120744.0µ ± 0%   326.6µ ± 1%  -99.73% (p=0.000 n=15)
HasRootsOverlap/chain/10-16                                 28.34µ ± 0%   10.60µ ± 0%  -62.60% (p=0.000 n=15)
HasRootsOverlap/chain/100-16                               13.838m ± 1%   1.888m ± 1%  -86.36% (p=0.000 n=15)
HasRootsOverlap/chain/500-16                               1366.1m ± 1%   145.7m ± 1%  -89.33% (p=0.000 n=15)
HasRootsOverlap/multi-root/10x10-16                        513.30µ ± 0%   11.24µ ± 1%  -97.81% (p=0.000 n=15)
HasRootsOverlap/multi-root/100x10-16                      53035.9µ ± 0%   134.2µ ± 0%  -99.75% (p=0.000 n=15)
HasRootsOverlap/multi-root/1000x10-16                    5685.456m ± 1%   1.684m ± 0%  -99.97% (p=0.000 n=15)
HasRootsOverlap/wide-fanout/10-16                          11.084µ ± 0%   3.366µ ± 0%  -69.63% (p=0.000 n=15)
HasRootsOverlap/wide-fanout/100-16                         646.39µ ± 1%   40.57µ ± 1%  -93.72% (p=0.000 n=15)
HasRootsOverlap/wide-fanout/1000-16                       57643.7µ ± 1%   509.0µ ± 0%  -99.12% (p=0.000 n=15)
HasRootsOverlapWithStore/disjoint/store=100,new=1-16        19.98µ ± 1%   26.06µ ± 2%  +30.47% (p=0.000 n=15)
HasRootsOverlapWithStore/disjoint/store=1000,new=1-16       239.4µ ± 1%   315.0µ ± 3%  +31.57% (p=0.000 n=15)
HasRootsOverlapWithStore/disjoint/store=10000,new=1-16      2.532m ± 2%   3.549m ± 3%  +40.13% (p=0.000 n=15)
geomean                                                     2.768m        112.5µ       -95.94%

                                                       │     main.txt     │                pr.txt                │
                                                       │       B/op       │     B/op      vs base                │
HasRootsOverlap/disjoint/10-16                               3.908Ki ± 0%   1.227Ki ± 0%  -68.62% (p=0.000 n=15)
HasRootsOverlap/disjoint/100-16                            319.779Ki ± 0%   7.628Ki ± 0%  -97.61% (p=0.000 n=15)
HasRootsOverlap/disjoint/1000-16                          31405.38Ki ± 0%   79.23Ki ± 0%  -99.75% (p=0.000 n=15)
HasRootsOverlap/disjoint/10000-16                        3126225.5Ki ± 0%   787.9Ki ± 0%  -99.97% (p=0.000 n=15)
HasRootsOverlap/identical/10-16                             10.377Ki ± 0%   2.599Ki ± 0%  -74.96% (p=0.000 n=15)
HasRootsOverlap/identical/100-16                            949.11Ki ± 0%   23.73Ki ± 0%  -97.50% (p=0.000 n=15)
HasRootsOverlap/identical/1000-16                          94081.0Ki ± 0%   313.7Ki ± 0%  -99.67% (p=0.000 n=15)
HasRootsOverlap/chain/10-16                                  30.33Ki ± 0%   12.09Ki ± 0%  -60.14% (p=0.000 n=15)
HasRootsOverlap/chain/100-16                                22.210Mi ± 0%   4.736Mi ± 0%  -78.67% (p=0.000 n=15)
HasRootsOverlap/chain/500-16                                2680.7Mi ± 0%   508.1Mi ± 0%  -81.05% (p=0.000 n=15)
HasRootsOverlap/multi-root/10x10-16                         563.57Ki ± 0%   13.09Ki ± 0%  -97.68% (p=0.000 n=15)
HasRootsOverlap/multi-root/100x10-16                      61885.88Ki ± 0%   99.70Ki ± 0%  -99.84% (p=0.000 n=15)
HasRootsOverlap/multi-root/1000x10-16                     6097.653Mi ± 0%   1.456Mi ± 0%  -99.98% (p=0.000 n=15)
HasRootsOverlap/wide-fanout/10-16                           10.362Ki ± 0%   3.530Ki ± 0%  -65.93% (p=0.000 n=15)
HasRootsOverlap/wide-fanout/100-16                          670.38Ki ± 0%   33.87Ki ± 0%  -94.95% (p=0.000 n=15)
HasRootsOverlap/wide-fanout/1000-16                        63219.4Ki ± 0%   424.6Ki ± 0%  -99.33% (p=0.000 n=15)
HasRootsOverlapWithStore/disjoint/store=100,new=1-16         35.11Ki ± 0%   26.29Ki ± 0%  -25.12% (p=0.000 n=15)
HasRootsOverlapWithStore/disjoint/store=1000,new=1-16        434.1Ki ± 0%   257.0Ki ± 0%  -40.80% (p=0.000 n=15)
HasRootsOverlapWithStore/disjoint/store=10000,new=1-16       3.920Mi ± 0%   2.460Mi ± 3%  -37.24% (p=0.000 n=15)
geomean                                                      2.942Mi        109.7Ki       -96.36%

                                                       │    main.txt     │               pr.txt                │
                                                       │    allocs/op    │  allocs/op   vs base                │
HasRootsOverlap/disjoint/10-16                               190.00 ± 0%    21.00 ± 0%  -88.95% (p=0.000 n=15)
HasRootsOverlap/disjoint/100-16                             19816.0 ± 0%    111.0 ± 0%  -99.44% (p=0.000 n=15)
HasRootsOverlap/disjoint/1000-16                          1998.029k ± 0%   1.011k ± 0%  -99.95% (p=0.000 n=15)
HasRootsOverlap/disjoint/10000-16                        199980.16k ± 0%   10.01k ± 0%  -99.99% (p=0.000 n=15)
HasRootsOverlap/identical/10-16                              380.00 ± 0%    36.00 ± 0%  -90.53% (p=0.000 n=15)
HasRootsOverlap/identical/100-16                            39634.0 ± 0%    138.0 ± 0%  -99.65% (p=0.000 n=15)
HasRootsOverlap/identical/1000-16                         3996.339k ± 0%   1.060k ± 0%  -99.97% (p=0.000 n=15)
HasRootsOverlap/chain/10-16                                   478.0 ± 0%    174.0 ± 0%  -63.60% (p=0.000 n=15)
HasRootsOverlap/chain/100-16                                 49.68k ± 0%   15.04k ± 0%  -69.73% (p=0.000 n=15)
HasRootsOverlap/chain/500-16                                1249.1k ± 0%   375.9k ± 0%  -69.91% (p=0.000 n=15)
HasRootsOverlap/multi-root/10x10-16                         18010.0 ± 0%    114.0 ± 0%  -99.37% (p=0.000 n=15)
HasRootsOverlap/multi-root/100x10-16                      1980.021k ± 0%   1.014k ± 0%  -99.95% (p=0.000 n=15)
HasRootsOverlap/multi-root/1000x10-16                    199800.67k ± 0%   10.02k ± 0%  -99.99% (p=0.000 n=15)
HasRootsOverlap/wide-fanout/10-16                            304.00 ± 0%    66.00 ± 0%  -78.29% (p=0.000 n=15)
HasRootsOverlap/wide-fanout/100-16                          20844.0 ± 0%    438.0 ± 0%  -97.90% (p=0.000 n=15)
HasRootsOverlap/wide-fanout/1000-16                       2008.233k ± 0%   4.061k ± 0%  -99.80% (p=0.000 n=15)
HasRootsOverlapWithStore/disjoint/store=100,new=1-16          514.0 ± 0%    410.0 ± 0%  -20.23% (p=0.000 n=15)
HasRootsOverlapWithStore/disjoint/store=1000,new=1-16        5.025k ± 0%   4.010k ± 0%  -20.20% (p=0.000 n=15)
HasRootsOverlapWithStore/disjoint/store=10000,new=1-16       50.08k ± 0%   40.01k ± 0%  -20.11% (p=0.000 n=15)
geomean                                                      65.42k        1.005k       -98.46%

Summary:

  • Massive wins in most cases.
  • A small regression in speed when there's a large number of already-activated bundles parked in the store, and we're only adding 1x new bundle. (~1ms slower when adding just 1x bundle to a set of 10k already-loaded bundles.)
    • The new algorithm always sorts the lists of roots across all bundles. It scales on roots, not bundles, and has to always sort the entire list of bundle roots.
  • Batch activation of bundles is enormously faster, so we might be able to push that direction further down the road.

This commit is a change of algorithm for how we detect
conflicts between the roots of multiple bundles. The old
algorithm was an O(N^2) all-to-all root paths comparison.
The new algorithm changes this to a string sorting and
scanning process, which results in O(N log N) comparisons,
and fewer string split operations.

The benchmarks included with this commit indicate a
meaningful improvement across all cases tested, with
even the pathological cases showing massive improvement
in runtime and memory usage/allocs.

Signed-off-by: Philip Conrad <philip@chariot-chaser.net>
Copy link
Copy Markdown
Contributor

@srenatus srenatus left a comment

Choose a reason for hiding this comment

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

Nice work. LGTM 👏

@philipaconrad philipaconrad merged commit 3250b65 into open-policy-agent:main May 18, 2026
40 checks passed
@github-actions
Copy link
Copy Markdown

Benchmark Regression Detected

Commit 3250b653ac3446fa4eacf78fc085298421b0398a introduced benchmark regressions (threshold: >25% ns/op increase).

Package Benchmark Regression
v1/ast BenchmarkFromBuiltinNames/two_parts-4 +44%
v1/types BenchmarkAnyUnionAllUniqueTypes/100x250-4 +40%

Benchmarks Dashboard

This comment was automatically generated by the benchmarks workflow.

@srenatus
Copy link
Copy Markdown
Contributor

Still too noisy/brittle. But at least we know the other benchmarks are all within the threshold, which is nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants