perf(opa): cache prepared eval queries to avoid per-Eval PrepareForEval#448
Open
matiasinsaurralde wants to merge 1 commit into
Open
Conversation
Eval built a fresh rego.New(...).Eval() on every call, which internally re-runs PrepareForEval (query compile + evaluation-plan build) each time. That work is identical for a fixed (query, compiler), so it was pure repeated cost on the hot findings.result / inventory.result queries run once per package. Cache one rego.PreparedEvalQuery per query string on Opa and evaluate with pq.Eval(ctx, rego.EvalInput(input)) so input stays per-call. Config lives in the store and is read via a fresh transaction each eval, so WithConfig needs no invalidation; Compile() clears the cache because it swaps the compiler (and thus the rule set) the plans are bound to. Removes the fixed prep overhead per Eval (~12x faster / ~6.6x less mem / ~4.8x fewer allocs on a trivial query; production queries keep their eval wall-clock but shed ~460 allocs each). Adds invariant tests covering per-input correctness, concurrent shared-Opa eval, and Compile() cache invalidation. Signed-off-by: Matías Insaurralde <matias@insaurral.de>
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
Opa.Evalbuilt a freshrego.New(...).Eval()on every call, which internally re-runsPrepareForEval(query compile + evaluation-plan build) each time. That work is identical for a fixed(query, compiler), so it was pure repeated cost.This caches one
rego.PreparedEvalQueryper query string onOpaand evaluates withpq.Eval(ctx, rego.EvalInput(input))so input stays per-call.Where this repeats
AnalyzeOrgconstructs one sharedOpaand reuses it across the whole repo loop; every repo runs the same two query strings on itdata.poutine.queries.inventory.resultanddata.poutine.queries.findings.result. So for an org of N repos,mainre-prepares each of those identical query strings N times (2N prepares total) even though the compiler never changes after startup.Single-target scans (
analyze_repo/analyze_local) run each query once, so they see no benefit — and no harm (one prepare either way). The win is org-scale scanning, poutine's primary use case.Correctness
rego.EvalInput, never baked into the cached plan.WithConfigneeds no cache invalidation.Compile()clears the cache because it swaps the compiler — and thus the rule set — that the cached plans are bound to.Impact
Benchmarks: Apple M4 Pro,
go test -benchmem -count=6, compared withbenchstat.The fixed prep cost that was removed. Measured on a trivial query, where evaluation time is negligible and the
PrepareForEvaloverhead is the whole signal. This overhead was being re-paid on everyEval:What that means for the two hot production queries. These are dominated by rule evaluation, so removing the prep step leaves wall-clock unchanged but strips the prep step's allocations from every call — and every call happens once per repo:
inventory.resultfindings.resultOver an org scan those per-call allocation savings recur
2Ntimes (inventory + findings per repo), on top of eliminating the2Nredundant query compilations.Tests
Behavior is unchanged — the full snapshot suite (
test/snapshot) passes. Adds invariant tests (opa/eval_prepared_test.go) for per-call input isolation, concurrent shared-Opaeval, andCompile()cache invalidation.go test ./opa/ -race(includeseval_prepared_test.go)go test ./scanner/ -racego test ./formatters/... -racemake lint-branch(0 issues)test/snapshot) — output identicalgo test ./...— all packages pass