mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
perf(topdown): optimize bindings allocation with dynamic pre-sizing (#8233)
Reduce memory waste in bindings allocation by using compile-time size hints for function evaluation. Before: All bindings pre-allocated 16-slot arrays regardless of usage After: Dynamic sizing based on known argument counts Memory improvements: - 20 bindings: 10.7% less memory, 13.3% faster - 50 bindings: 20.7% less memory, 8.3% faster - Transition cost: 22.4% faster for large binding sets Implementation: - Add newBindingsWithSize() constructor with size hint parameter - Add newBindingsArrayHashmapWithSize() for smart array/map selection - Add childWithBindingSizeHint() method for eval context creation - Apply optimization in evalOneRule() using len(args) as hint Fixes #7266 Signed-off-by: alex60217101990 <alex6021710@gmail.com>
This commit is contained in:
+13
-1
@@ -24,6 +24,17 @@ import (
|
||||
"github.com/open-policy-agent/opa/v1/util"
|
||||
)
|
||||
|
||||
// maxBindingsEstimate is the cap for binding count estimates in comprehensions.
|
||||
// This value aligns with maxLinearScan in topdown/bindings.go.
|
||||
const maxBindingsEstimate = 16
|
||||
|
||||
// EstimateBodyBindingCount returns an estimate of the number of bindings needed
|
||||
// for evaluating a comprehension body. It uses the body length as a heuristic,
|
||||
// capped at maxBindingsEstimate.
|
||||
func EstimateBodyBindingCount(body Body) (estimate int) {
|
||||
return min(len(body), maxBindingsEstimate)
|
||||
}
|
||||
|
||||
var (
|
||||
NullValue Value = Null{}
|
||||
|
||||
@@ -669,8 +680,9 @@ func NumberTerm(n json.Number) *Term {
|
||||
}
|
||||
|
||||
// IntNumberTerm creates a new Term with an integer Number value.
|
||||
// For values between -1 and 512, returns a cached Term to reduce allocations.
|
||||
func IntNumberTerm(i int) *Term {
|
||||
return &Term{Value: newIntNumberValue(i)}
|
||||
return internedIntNumberTerm(i)
|
||||
}
|
||||
|
||||
// UIntNumberTerm creates a new Term with an unsigned integer Number value.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package rego
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -74,46 +73,6 @@ func BenchmarkPartialObjectRuleCrossModule(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCustomFunctionInHotPath(b *testing.B) {
|
||||
ctx := b.Context()
|
||||
input := ast.MustParseTerm(mustReadFileAsString(b, "testdata/ast.json"))
|
||||
module := ast.MustParseModule(`package test
|
||||
|
||||
import rego.v1
|
||||
|
||||
r := count(refs)
|
||||
|
||||
refs contains value if {
|
||||
walk(input, [_, value])
|
||||
is_ref(value)
|
||||
}
|
||||
|
||||
is_ref(value) if value.type == "ref"
|
||||
is_ref(value) if value[0].type == "ref"`)
|
||||
|
||||
r := New(Query("data.test.r = x"), ParsedModule(module))
|
||||
|
||||
pq, err := r.PrepareForEval(ctx)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
res, err := pq.Eval(ctx, EvalParsedInput(input.Value))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
b.Fatal("expected result")
|
||||
}
|
||||
|
||||
if res[0].Bindings["x"].(json.Number) != "402" {
|
||||
b.Fatalf("expected 402, got %v", res[0].Bindings["x"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmarks of the ACI test data from Regorus
|
||||
// https://github.com/microsoft/regorus?tab=readme-ov-file#performance
|
||||
|
||||
|
||||
+77
-14
@@ -40,6 +40,14 @@ func newBindings(id uint64, instr *Instrumentation) *bindings {
|
||||
return &bindings{id, values, instr}
|
||||
}
|
||||
|
||||
// newBindingsWithSize creates bindings pre-sized for the expected number of entries.
|
||||
// This avoids over-allocation when the binding count is known in advance (e.g., function arguments).
|
||||
// For sizeHint <= maxLinearScan, it uses array mode; for larger hints, it pre-allocates a map.
|
||||
func newBindingsWithSize(id uint64, instr *Instrumentation, sizeHint int) *bindings {
|
||||
values := newBindingsArrayHashmapWithSize(sizeHint)
|
||||
return &bindings{id, values, instr}
|
||||
}
|
||||
|
||||
func (u *bindings) Iter(caller *bindings, iter func(*ast.Term, *ast.Term) error) error {
|
||||
|
||||
var err error
|
||||
@@ -300,12 +308,16 @@ func (vis namespacingVisitor) namespaceTerm(a *ast.Term) *ast.Term {
|
||||
|
||||
const maxLinearScan = 16
|
||||
|
||||
// bindingsArrayHashMap uses an array with linear scan instead
|
||||
// bindingsArrayHashMap uses a dynamically growing slice with linear scan instead
|
||||
// of a hash map for smaller # of entries. Hash maps start to
|
||||
// show off their performance advantage only after 16 keys.
|
||||
//
|
||||
// Memory optimization: The slice grows incrementally (2 -> 4 -> 8 -> 16) to avoid
|
||||
// wasting memory when only a few bindings are used. This is critical for scenarios
|
||||
// like comprehensions and functions with few arguments that are called thousands of times.
|
||||
type bindingsArrayHashmap struct {
|
||||
n int // Entries in the array.
|
||||
a *[maxLinearScan]bindingArrayKeyValue
|
||||
n int // Entries in the slice.
|
||||
a []bindingArrayKeyValue
|
||||
m map[ast.Var]bindingArrayKeyValue
|
||||
}
|
||||
|
||||
@@ -318,29 +330,74 @@ func newBindingsArrayHashmap() bindingsArrayHashmap {
|
||||
return bindingsArrayHashmap{}
|
||||
}
|
||||
|
||||
// newBindingsArrayHashmapWithSize creates a bindingsArrayHashmap pre-sized for the expected number of entries.
|
||||
// This optimization reduces memory waste when the binding count is known in advance.
|
||||
//
|
||||
// Size selection strategy:
|
||||
// - sizeHint == 0: lazy allocation (no pre-allocation)
|
||||
// - sizeHint <= maxLinearScan: pre-allocate slice with exact capacity to avoid reallocation
|
||||
// - sizeHint > maxLinearScan: pre-allocate map with exact capacity
|
||||
//
|
||||
// Memory impact example:
|
||||
// - Without hint: dynamic growth 0 -> 2 -> 4 -> 8 -> 16 (saves memory for small counts)
|
||||
// - With hint=2: pre-allocates slice with capacity 2 (exact fit, no waste)
|
||||
// - With hint=20: pre-allocates map with capacity 20 (saves array allocation + reallocation)
|
||||
func newBindingsArrayHashmapWithSize(sizeHint int) bindingsArrayHashmap {
|
||||
if sizeHint <= 0 {
|
||||
// For unknown sizes, use default lazy allocation with dynamic growth.
|
||||
return bindingsArrayHashmap{}
|
||||
}
|
||||
|
||||
if sizeHint <= maxLinearScan {
|
||||
// For small known sizes, pre-allocate slice with exact capacity to avoid growth overhead.
|
||||
return bindingsArrayHashmap{
|
||||
a: make([]bindingArrayKeyValue, 0, sizeHint),
|
||||
}
|
||||
}
|
||||
|
||||
// For larger sizes, pre-allocate map to avoid array allocation + transition cost.
|
||||
return bindingsArrayHashmap{
|
||||
m: make(map[ast.Var]bindingArrayKeyValue, sizeHint),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bindingsArrayHashmap) Put(key *ast.Term, value value) {
|
||||
if b.m == nil {
|
||||
if b.a == nil {
|
||||
b.a = new([maxLinearScan]bindingArrayKeyValue)
|
||||
} else if i := b.find(key); i >= 0 {
|
||||
// Check if key already exists and update value
|
||||
if i := b.find(key); i >= 0 {
|
||||
b.a[i].value = value
|
||||
return
|
||||
}
|
||||
|
||||
// Still room in slice mode (< maxLinearScan)
|
||||
if b.n < maxLinearScan {
|
||||
b.a[b.n] = bindingArrayKeyValue{key, value}
|
||||
// Grow slice if needed using exponential growth strategy
|
||||
if b.n == cap(b.a) {
|
||||
newCap := cap(b.a) * 2
|
||||
if newCap == 0 {
|
||||
newCap = 2 // Start with 2 elements
|
||||
}
|
||||
if newCap > maxLinearScan {
|
||||
newCap = maxLinearScan
|
||||
}
|
||||
newA := make([]bindingArrayKeyValue, b.n, newCap)
|
||||
copy(newA, b.a)
|
||||
b.a = newA
|
||||
}
|
||||
b.a = append(b.a, bindingArrayKeyValue{key, value})
|
||||
b.n++
|
||||
return
|
||||
}
|
||||
|
||||
// Array is full, revert to using the hash map instead.
|
||||
|
||||
// Slice is full (reached maxLinearScan), transition to map mode.
|
||||
b.m = make(map[ast.Var]bindingArrayKeyValue, maxLinearScan+1)
|
||||
for _, kv := range *b.a {
|
||||
for _, kv := range b.a {
|
||||
b.m[kv.key.Value.(ast.Var)] = bindingArrayKeyValue{kv.key, kv.value}
|
||||
}
|
||||
b.m[key.Value.(ast.Var)] = bindingArrayKeyValue{key, value}
|
||||
|
||||
// Clear slice to allow GC
|
||||
b.a = nil
|
||||
b.n = 0
|
||||
return
|
||||
}
|
||||
@@ -372,7 +429,8 @@ func (b *bindingsArrayHashmap) Delete(key *ast.Term) {
|
||||
if i < n {
|
||||
b.a[i] = b.a[n]
|
||||
}
|
||||
|
||||
// Shrink slice to reflect deletion
|
||||
b.a = b.a[:n]
|
||||
b.n = n
|
||||
}
|
||||
return
|
||||
@@ -383,9 +441,11 @@ func (b *bindingsArrayHashmap) Delete(key *ast.Term) {
|
||||
|
||||
func (b *bindingsArrayHashmap) Iter(f func(k *ast.Term, v value) bool) {
|
||||
if b.m == nil {
|
||||
for i := range b.n {
|
||||
if f(b.a[i].key, b.a[i].value) {
|
||||
return
|
||||
if b.a != nil {
|
||||
for i := range b.n {
|
||||
if f(b.a[i].key, b.a[i].value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -399,6 +459,9 @@ func (b *bindingsArrayHashmap) Iter(f func(k *ast.Term, v value) bool) {
|
||||
}
|
||||
|
||||
func (b *bindingsArrayHashmap) find(key *ast.Term) int {
|
||||
if b.a == nil || b.n == 0 {
|
||||
return -1
|
||||
}
|
||||
v := key.Value.(ast.Var)
|
||||
for i := range b.n {
|
||||
if b.a[i].key.Value.(ast.Var) == v {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package topdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/storage"
|
||||
inmem "github.com/open-policy-agent/opa/v1/storage/inmem/test"
|
||||
)
|
||||
|
||||
// BenchmarkBindingsAllocation benchmarks memory allocation for bindings with different sizes.
|
||||
// This directly tests the optimization from issue #7266.
|
||||
func BenchmarkBindingsAllocation(b *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bindings int
|
||||
}{
|
||||
{"1_binding", 1},
|
||||
{"2_bindings", 2},
|
||||
{"3_bindings", 3},
|
||||
{"5_bindings", 5},
|
||||
{"10_bindings", 10},
|
||||
{"16_bindings", 16},
|
||||
{"20_bindings", 20},
|
||||
{"50_bindings", 50},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name+"_without_hint", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
bi := newBindings(0, nil)
|
||||
for j := range tt.bindings {
|
||||
key := ast.VarTerm(fmt.Sprintf("x%d", j))
|
||||
val := ast.IntNumberTerm(j)
|
||||
bi.bind(key, val, nil, &undo{})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run(tt.name+"_with_hint", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
bi := newBindingsWithSize(0, nil, tt.bindings)
|
||||
for j := range tt.bindings {
|
||||
key := ast.VarTerm(fmt.Sprintf("x%d", j))
|
||||
val := ast.IntNumberTerm(j)
|
||||
bi.bind(key, val, nil, &undo{})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkFunctionArgumentCounts benchmarks functions with varying argument counts.
|
||||
// This demonstrates the memory waste when small functions allocate 16-slot arrays.
|
||||
func BenchmarkFunctionArgumentCounts(b *testing.B) {
|
||||
argCounts := []int{1, 2, 3, 5, 10, 15, 20}
|
||||
|
||||
for _, argCount := range argCounts {
|
||||
b.Run(fmt.Sprintf("%d_args", argCount), func(b *testing.B) {
|
||||
ctx := b.Context()
|
||||
|
||||
// Create function with N arguments
|
||||
args := make([]string, argCount)
|
||||
checks := make([]string, argCount)
|
||||
for i := range argCount {
|
||||
args[i] = fmt.Sprintf("x%d", i)
|
||||
checks[i] = fmt.Sprintf("%s == %d", args[i], i)
|
||||
}
|
||||
|
||||
module := fmt.Sprintf(`package test
|
||||
|
||||
f(%s) if {
|
||||
%s
|
||||
}
|
||||
`, strings.Join(args, ", "), strings.Join(checks, "\n\t\t\t\t"))
|
||||
|
||||
compiler := ast.MustCompileModules(map[string]string{
|
||||
"test.rego": module,
|
||||
})
|
||||
|
||||
store := inmem.NewFromObject(map[string]any{})
|
||||
|
||||
// Create call with matching arguments
|
||||
callArgs := make([]string, argCount)
|
||||
for i := range argCount {
|
||||
callArgs[i] = strconv.Itoa(i)
|
||||
}
|
||||
query := ast.MustParseBody(fmt.Sprintf(`test.f(%s)`, strings.Join(callArgs, ", ")))
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
err := storage.Txn(ctx, store, storage.TransactionParams{}, func(txn storage.Transaction) error {
|
||||
q := NewQuery(query).
|
||||
WithCompiler(compiler).
|
||||
WithStore(store).
|
||||
WithTransaction(txn)
|
||||
|
||||
_, err := q.Run(ctx)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkBindingsArrayHashmapTransition benchmarks the transition from array to map mode.
|
||||
func BenchmarkBindingsArrayHashmapTransition(b *testing.B) {
|
||||
b.Run("without_hint_transition_at_17", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
bh := newBindingsArrayHashmap()
|
||||
// Add 17 bindings to force transition to map
|
||||
for j := range 17 {
|
||||
key := ast.VarTerm(fmt.Sprintf("x%d", j))
|
||||
val := value{v: ast.IntNumberTerm(j)}
|
||||
bh.Put(key, val)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("with_hint_starts_with_map", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
bh := newBindingsArrayHashmapWithSize(17)
|
||||
// Add 17 bindings directly to map (no transition)
|
||||
for j := range 17 {
|
||||
key := ast.VarTerm(fmt.Sprintf("x%d", j))
|
||||
val := value{v: ast.IntNumberTerm(j)}
|
||||
bh.Put(key, val)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -101,3 +101,179 @@ func testBindingKey(key int) *ast.Term {
|
||||
func testBindingValue(b *bindings, key int) value {
|
||||
return value{b, ast.IntNumberTerm(key)}
|
||||
}
|
||||
|
||||
// TestBindingsArrayHashmapDynamicGrowth tests the dynamic growth behavior of the slice-based implementation.
|
||||
// This validates that the optimization correctly grows the slice incrementally (2 -> 4 -> 8 -> 16).
|
||||
func TestBindingsArrayHashmapDynamicGrowth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
numBindings int
|
||||
expectedCap int // Expected final capacity
|
||||
shouldUseMap bool
|
||||
}{
|
||||
{"1_binding", 1, 2, false}, // Should allocate cap=2
|
||||
{"2_bindings", 2, 2, false}, // Should use cap=2
|
||||
{"3_bindings", 3, 4, false}, // Should grow to cap=4
|
||||
{"4_bindings", 4, 4, false}, // Should use cap=4
|
||||
{"5_bindings", 5, 8, false}, // Should grow to cap=8
|
||||
{"8_bindings", 8, 8, false}, // Should use cap=8
|
||||
{"9_bindings", 9, 16, false}, // Should grow to cap=16
|
||||
{"16_bindings", 16, 16, false}, // Should use cap=16
|
||||
{"17_bindings", 17, 0, true}, // Should transition to map
|
||||
}
|
||||
|
||||
var bindings bindings
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
b := newBindingsArrayHashmap()
|
||||
|
||||
// Add bindings
|
||||
for i := range tc.numBindings {
|
||||
b.Put(testBindingKey(i), testBindingValue(&bindings, i))
|
||||
}
|
||||
|
||||
// Verify mode (slice vs map)
|
||||
if tc.shouldUseMap {
|
||||
if b.m == nil {
|
||||
t.Errorf("Expected map mode but still in slice mode")
|
||||
}
|
||||
if b.a != nil {
|
||||
t.Errorf("Expected slice to be nil after transition to map")
|
||||
}
|
||||
} else {
|
||||
if b.m != nil {
|
||||
t.Errorf("Expected slice mode but transitioned to map")
|
||||
}
|
||||
if cap(b.a) != tc.expectedCap {
|
||||
t.Errorf("Expected capacity %d but got %d", tc.expectedCap, cap(b.a))
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all values are retrievable
|
||||
for i := range tc.numBindings {
|
||||
expected := testBindingValue(&bindings, i)
|
||||
if v, ok := b.Get(testBindingKey(i)); !ok {
|
||||
t.Errorf("Value %d not found", i)
|
||||
} else if !v.equal(&expected) {
|
||||
t.Errorf("Value %d not equal", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify count via iteration
|
||||
count := 0
|
||||
b.Iter(func(k *ast.Term, v value) bool {
|
||||
count++
|
||||
return false
|
||||
})
|
||||
if count != tc.numBindings {
|
||||
t.Errorf("Expected %d bindings but found %d", tc.numBindings, count)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBindingsArrayHashmapWithSizeHint tests the pre-allocation behavior with size hints.
|
||||
func TestBindingsArrayHashmapWithSizeHint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
sizeHint int
|
||||
numBindings int
|
||||
expectedCap int // Expected initial capacity
|
||||
shouldUseMap bool
|
||||
}{
|
||||
{"hint_0", 0, 5, 8, false}, // No hint, dynamic growth
|
||||
{"hint_2", 2, 2, 2, false}, // Pre-allocated cap=2
|
||||
{"hint_5", 5, 5, 5, false}, // Pre-allocated cap=5
|
||||
{"hint_10", 10, 10, 10, false}, // Pre-allocated cap=10
|
||||
{"hint_16", 16, 16, 16, false}, // Pre-allocated cap=16
|
||||
{"hint_20", 20, 20, 0, true}, // Pre-allocated map
|
||||
{"hint_50", 50, 50, 0, true}, // Pre-allocated map
|
||||
{"hint_2_grow", 2, 10, 16, false}, // Start with hint=2, grow to 10
|
||||
}
|
||||
|
||||
var bindings bindings
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
b := newBindingsArrayHashmapWithSize(tc.sizeHint)
|
||||
|
||||
// Check initial state before any Put operations
|
||||
if tc.sizeHint > 0 && tc.sizeHint <= maxLinearScan {
|
||||
if cap(b.a) != tc.sizeHint {
|
||||
t.Errorf("Expected initial capacity %d but got %d", tc.sizeHint, cap(b.a))
|
||||
}
|
||||
} else if tc.sizeHint > maxLinearScan {
|
||||
if b.m == nil {
|
||||
t.Errorf("Expected pre-allocated map but got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Add bindings
|
||||
for i := range tc.numBindings {
|
||||
b.Put(testBindingKey(i), testBindingValue(&bindings, i))
|
||||
}
|
||||
|
||||
// Verify mode and capacity
|
||||
if tc.shouldUseMap {
|
||||
if b.m == nil {
|
||||
t.Errorf("Expected map mode but still in slice mode")
|
||||
}
|
||||
} else {
|
||||
if b.m != nil {
|
||||
t.Errorf("Expected slice mode but transitioned to map")
|
||||
}
|
||||
// Only check final capacity for cases that don't transition
|
||||
if tc.numBindings <= maxLinearScan && cap(b.a) != tc.expectedCap {
|
||||
t.Errorf("Expected final capacity %d but got %d", tc.expectedCap, cap(b.a))
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all values
|
||||
for i := range tc.numBindings {
|
||||
expected := testBindingValue(&bindings, i)
|
||||
if v, ok := b.Get(testBindingKey(i)); !ok {
|
||||
t.Errorf("Value %d not found", i)
|
||||
} else if !v.equal(&expected) {
|
||||
t.Errorf("Value %d not equal", i)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBindingsArrayHashmapUpdateExisting tests updating existing keys.
|
||||
func TestBindingsArrayHashmapUpdateExisting(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var bindings bindings
|
||||
b := newBindingsArrayHashmap()
|
||||
|
||||
// Add initial bindings
|
||||
for i := range 5 {
|
||||
b.Put(testBindingKey(i), testBindingValue(&bindings, i))
|
||||
}
|
||||
|
||||
initialCap := cap(b.a)
|
||||
|
||||
// Update existing keys - should not grow capacity
|
||||
for i := range 5 {
|
||||
b.Put(testBindingKey(i), testBindingValue(&bindings, i*10))
|
||||
}
|
||||
|
||||
if cap(b.a) != initialCap {
|
||||
t.Errorf("Capacity changed from %d to %d on update", initialCap, cap(b.a))
|
||||
}
|
||||
|
||||
// Verify updated values
|
||||
for i := range 5 {
|
||||
expected := testBindingValue(&bindings, i*10)
|
||||
if v, ok := b.Get(testBindingKey(i)); !ok {
|
||||
t.Errorf("Value %d not found", i)
|
||||
} else if !v.equal(&expected) {
|
||||
t.Errorf("Value %d not updated correctly", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package topdown
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
||||
)
|
||||
|
||||
// Benchmark comprehensions with different variable counts
|
||||
func BenchmarkComprehensionBindings(b *testing.B) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
module string
|
||||
query string
|
||||
}{
|
||||
{
|
||||
name: "ArrayComp_1Var",
|
||||
module: `package test
|
||||
arr := [1, 2, 3, 4, 5]
|
||||
result := [x | x := arr[_]]`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "ArrayComp_2Vars",
|
||||
module: `package test
|
||||
arr := [1, 2, 3, 4, 5]
|
||||
result := [y | x := arr[_]; y := x * 2]`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "ArrayComp_3Vars",
|
||||
module: `package test
|
||||
arr := [1, 2, 3, 4, 5]
|
||||
result := [z | x := arr[_]; y := x * 2; z := y + 1]`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "SetComp_1Var",
|
||||
module: `package test
|
||||
arr := [1, 2, 3, 4, 5]
|
||||
result := {x | x := arr[_]}`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "SetComp_2Vars",
|
||||
module: `package test
|
||||
arr := [1, 2, 3, 4, 5]
|
||||
result := {y | x := arr[_]; y := x * 2}`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "ObjectComp_2Vars",
|
||||
module: `package test
|
||||
arr := [1, 2, 3, 4, 5]
|
||||
result := {x: y | x := arr[_]; y := x * 2}`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "ObjectComp_3Vars",
|
||||
module: `package test
|
||||
arr := [1, 2, 3, 4, 5]
|
||||
result := {x: z | x := arr[_]; y := x * 2; z := y + 1}`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
mod := ast.MustParseModule(tc.module)
|
||||
compiler := ast.NewCompiler()
|
||||
compiler.Compile(map[string]*ast.Module{"test": mod})
|
||||
if compiler.Failed() {
|
||||
b.Fatalf("Compilation failed: %v", compiler.Errors)
|
||||
}
|
||||
|
||||
store := inmem.New()
|
||||
ctx := context.Background()
|
||||
|
||||
var m1, m2 runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m1)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
q := NewQuery(ast.MustParseBody(tc.query)).
|
||||
WithCompiler(compiler).
|
||||
WithStore(store)
|
||||
|
||||
_, err := q.Run(ctx)
|
||||
if err != nil {
|
||||
b.Fatalf("Query failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m2)
|
||||
|
||||
allocPerOp := (m2.TotalAlloc - m1.TotalAlloc) / uint64(b.N)
|
||||
mallocsPerOp := (m2.Mallocs - m1.Mallocs) / uint64(b.N)
|
||||
|
||||
b.ReportMetric(float64(allocPerOp), "B/op")
|
||||
b.ReportMetric(float64(mallocsPerOp), "allocs/op")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark large comprehension iterations
|
||||
func BenchmarkComprehensionLargeIteration(b *testing.B) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
module string
|
||||
query string
|
||||
}{
|
||||
{
|
||||
name: "ArrayComp_20Elements",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
numbers := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||
result := [y | x := numbers[_]; y := x * 2]`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "SetComp_20Elements",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
numbers := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||
result := {y | x := numbers[_]; y := x * 2}`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "NestedComp",
|
||||
module: `package test
|
||||
arr := [1, 2, 3, 4, 5]
|
||||
result := [z | x := arr[_]; y := [a | a := arr[_]; a > x][_]; z := x + y]`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
mod := ast.MustParseModule(tc.module)
|
||||
compiler := ast.NewCompiler()
|
||||
compiler.Compile(map[string]*ast.Module{"test": mod})
|
||||
if compiler.Failed() {
|
||||
b.Fatalf("Compilation failed: %v", compiler.Errors)
|
||||
}
|
||||
|
||||
store := inmem.New()
|
||||
ctx := context.Background()
|
||||
|
||||
var m1, m2 runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m1)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
q := NewQuery(ast.MustParseBody(tc.query)).
|
||||
WithCompiler(compiler).
|
||||
WithStore(store)
|
||||
|
||||
_, err := q.Run(ctx)
|
||||
if err != nil {
|
||||
b.Fatalf("Query failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m2)
|
||||
|
||||
allocPerOp := (m2.TotalAlloc - m1.TotalAlloc) / uint64(b.N)
|
||||
mallocsPerOp := (m2.Mallocs - m1.Mallocs) / uint64(b.N)
|
||||
|
||||
b.ReportMetric(float64(allocPerOp), "B/op")
|
||||
b.ReportMetric(float64(mallocsPerOp), "allocs/op")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test to verify EstimateBindingCount returns body length
|
||||
func TestComprehensionBindingEstimate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
module string
|
||||
expected int // Expected is body length
|
||||
}{
|
||||
{
|
||||
name: "ArrayComp_1Expr",
|
||||
module: `package test
|
||||
result := [x | x := [1, 2, 3][_]]`,
|
||||
expected: 1, // 1 expression in body
|
||||
},
|
||||
{
|
||||
name: "ArrayComp_2Exprs",
|
||||
module: `package test
|
||||
result := [y | x := [1, 2, 3][_]; y := x * 2]`,
|
||||
expected: 2, // 2 expressions in body
|
||||
},
|
||||
{
|
||||
name: "ArrayComp_3Exprs",
|
||||
module: `package test
|
||||
result := [z | x := [1, 2, 3][_]; y := x * 2; z := y + 1]`,
|
||||
expected: 3, // 3 expressions in body
|
||||
},
|
||||
{
|
||||
name: "SetComp_2Exprs",
|
||||
module: `package test
|
||||
result := {y | x := [1, 2, 3][_]; y := x * 2}`,
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
name: "ObjectComp_3Exprs",
|
||||
module: `package test
|
||||
result := {x: z | x := [1, 2, 3][_]; y := x * 2; z := y + 1}`,
|
||||
expected: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mod := ast.MustParseModule(tc.module)
|
||||
|
||||
// Find the comprehension in the module
|
||||
var count int
|
||||
ast.WalkRules(mod, func(r *ast.Rule) bool {
|
||||
ast.WalkTerms(r, func(term *ast.Term) bool {
|
||||
switch c := term.Value.(type) {
|
||||
case *ast.ArrayComprehension:
|
||||
count = ast.EstimateBodyBindingCount(c.Body)
|
||||
return true
|
||||
case *ast.SetComprehension:
|
||||
count = ast.EstimateBodyBindingCount(c.Body)
|
||||
return true
|
||||
case *ast.ObjectComprehension:
|
||||
count = ast.EstimateBodyBindingCount(c.Body)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
if count != tc.expected {
|
||||
t.Errorf("Expected %d bindings, got %d", tc.expected, count)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package topdown
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
||||
)
|
||||
|
||||
// Benchmark comparing comprehension performance with and without binding size hints
|
||||
// This simulates "before" vs "after" optimization by measuring allocation patterns
|
||||
func BenchmarkComprehensionOptimizationComparison(b *testing.B) {
|
||||
// Test case with typical comprehension usage patterns
|
||||
testCases := []struct {
|
||||
name string
|
||||
module string
|
||||
query string
|
||||
}{
|
||||
{
|
||||
name: "SimpleArrayComp",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
data_array := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
result := [y | x := data_array[_]; y := x * 2]`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "SimpleSetComp",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
data_array := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
result := {y | x := data_array[_]; y := x * 2}`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "SimpleObjectComp",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
data_array := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
result := {x: y | x := data_array[_]; y := x * 2}`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "MultiExpressionComp",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
data_array := [1, 2, 3, 4, 5]
|
||||
result := [z |
|
||||
x := data_array[_]
|
||||
y := x * 2
|
||||
z := y + 1
|
||||
z < 10
|
||||
]`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "NestedComprehension",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
outer := [1, 2, 3]
|
||||
inner := [4, 5, 6]
|
||||
result := [sum |
|
||||
x := outer[_]
|
||||
y := inner[_]
|
||||
sum := x + y
|
||||
]`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
mod := ast.MustParseModule(tc.module)
|
||||
compiler := ast.NewCompiler()
|
||||
compiler.Compile(map[string]*ast.Module{"test": mod})
|
||||
if compiler.Failed() {
|
||||
b.Fatalf("Compilation failed: %v", compiler.Errors)
|
||||
}
|
||||
|
||||
store := inmem.New()
|
||||
ctx := context.Background()
|
||||
|
||||
// Warmup
|
||||
for range 3 {
|
||||
q := NewQuery(ast.MustParseBody(tc.query)).
|
||||
WithCompiler(compiler).
|
||||
WithStore(store)
|
||||
_, err := q.Run(ctx)
|
||||
if err != nil {
|
||||
b.Fatalf("Warmup query failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var m1, m2 runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m1)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
q := NewQuery(ast.MustParseBody(tc.query)).
|
||||
WithCompiler(compiler).
|
||||
WithStore(store)
|
||||
|
||||
_, err := q.Run(ctx)
|
||||
if err != nil {
|
||||
b.Fatalf("Query failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m2)
|
||||
|
||||
allocPerOp := (m2.TotalAlloc - m1.TotalAlloc) / uint64(b.N)
|
||||
mallocsPerOp := (m2.Mallocs - m1.Mallocs) / uint64(b.N)
|
||||
|
||||
b.ReportMetric(float64(allocPerOp), "B/op")
|
||||
b.ReportMetric(float64(mallocsPerOp), "allocs/op")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark high-frequency comprehension scenarios (mimicking Regal-style workloads)
|
||||
func BenchmarkComprehensionHighFrequency(b *testing.B) {
|
||||
// Simulate scenarios where comprehensions are evaluated many times
|
||||
// (similar to linting operations where rules are checked repeatedly)
|
||||
module := `package test
|
||||
import rego.v1
|
||||
|
||||
violations contains msg if {
|
||||
some file in input.files
|
||||
some line in file.lines
|
||||
line.length > 80
|
||||
msg := sprintf("Line too long in %s", [file.name])
|
||||
}
|
||||
|
||||
short_lines := {line |
|
||||
some file in input.files
|
||||
some line in file.lines
|
||||
line.length <= 80
|
||||
}
|
||||
|
||||
file_stats := {file.name: len |
|
||||
some file in input.files
|
||||
len := count(file.lines)
|
||||
}`
|
||||
|
||||
// Mock input data
|
||||
inputData := map[string]any{
|
||||
"files": []map[string]any{
|
||||
{
|
||||
"name": "file1.rego",
|
||||
"lines": []map[string]any{
|
||||
{"length": 50},
|
||||
{"length": 90},
|
||||
{"length": 70},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "file2.rego",
|
||||
"lines": []map[string]any{
|
||||
{"length": 60},
|
||||
{"length": 85},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mod := ast.MustParseModule(module)
|
||||
compiler := ast.NewCompiler()
|
||||
compiler.Compile(map[string]*ast.Module{"test": mod})
|
||||
if compiler.Failed() {
|
||||
b.Fatalf("Compilation failed: %v", compiler.Errors)
|
||||
}
|
||||
|
||||
store := inmem.NewFromObject(inputData)
|
||||
ctx := context.Background()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
query string
|
||||
}{
|
||||
{"Violations", "data.test.violations"},
|
||||
{"ShortLines", "data.test.short_lines"},
|
||||
{"FileStats", "data.test.file_stats"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
var m1, m2 runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m1)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
q := NewQuery(ast.MustParseBody(tc.query)).
|
||||
WithCompiler(compiler).
|
||||
WithStore(store).
|
||||
WithInput(ast.NewTerm(ast.MustInterfaceToValue(inputData)))
|
||||
|
||||
_, err := q.Run(ctx)
|
||||
if err != nil {
|
||||
b.Fatalf("Query failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m2)
|
||||
|
||||
allocPerOp := (m2.TotalAlloc - m1.TotalAlloc) / uint64(b.N)
|
||||
mallocsPerOp := (m2.Mallocs - m1.Mallocs) / uint64(b.N)
|
||||
|
||||
b.ReportMetric(float64(allocPerOp), "B/op")
|
||||
b.ReportMetric(float64(mallocsPerOp), "allocs/op")
|
||||
})
|
||||
}
|
||||
}
|
||||
+20
-14
@@ -222,12 +222,14 @@ func (e *eval) closure(query ast.Body, cpy *eval) {
|
||||
cpy.findOne = false
|
||||
}
|
||||
|
||||
func (e *eval) child(query ast.Body, cpy *eval) {
|
||||
// childWithBindingSizeHint creates a child evaluator with bindings pre-sized for the expected number of variables.
|
||||
// This reduces memory waste when evaluating functions or rules with known argument counts.
|
||||
func (e *eval) childWithBindingSizeHint(query ast.Body, cpy *eval, sizeHint int) {
|
||||
*cpy = *e
|
||||
cpy.index = 0
|
||||
cpy.query = query
|
||||
cpy.queryID = cpy.queryIDFact.Next()
|
||||
cpy.bindings = newBindings(cpy.queryID, e.instr)
|
||||
cpy.bindings = newBindingsWithSize(cpy.queryID, e.instr, sizeHint)
|
||||
cpy.parent = e
|
||||
cpy.findOne = false
|
||||
}
|
||||
@@ -1366,7 +1368,7 @@ func (e *eval) buildComprehensionCacheArray(x *ast.ArrayComprehension, keys []*a
|
||||
child := evalPool.Get()
|
||||
defer evalPool.Put(child)
|
||||
|
||||
e.child(x.Body, child)
|
||||
e.childWithBindingSizeHint(x.Body, child, ast.EstimateBodyBindingCount(x.Body))
|
||||
node := newComprehensionCacheElem()
|
||||
return node, child.Run(func(child *eval) error {
|
||||
values := make([]*ast.Term, len(keys))
|
||||
@@ -1388,7 +1390,7 @@ func (e *eval) buildComprehensionCacheSet(x *ast.SetComprehension, keys []*ast.T
|
||||
child := evalPool.Get()
|
||||
defer evalPool.Put(child)
|
||||
|
||||
e.child(x.Body, child)
|
||||
e.childWithBindingSizeHint(x.Body, child, ast.EstimateBodyBindingCount(x.Body))
|
||||
node := newComprehensionCacheElem()
|
||||
return node, child.Run(func(child *eval) error {
|
||||
values := make([]*ast.Term, len(keys))
|
||||
@@ -1411,7 +1413,7 @@ func (e *eval) buildComprehensionCacheObject(x *ast.ObjectComprehension, keys []
|
||||
child := evalPool.Get()
|
||||
defer evalPool.Put(child)
|
||||
|
||||
e.child(x.Body, child)
|
||||
e.childWithBindingSizeHint(x.Body, child, ast.EstimateBodyBindingCount(x.Body))
|
||||
node := newComprehensionCacheElem()
|
||||
return node, child.Run(func(child *eval) error {
|
||||
values := make([]*ast.Term, len(keys))
|
||||
@@ -2272,7 +2274,11 @@ func (e *evalFunc) evalOneRule(iter unifyIterator, rule *ast.Rule, args []*ast.T
|
||||
child := evalPool.Get()
|
||||
defer evalPool.Put(child)
|
||||
|
||||
e.e.child(rule.Body, child)
|
||||
// Optimization: pre-size bindings based on function argument count to reduce memory waste.
|
||||
// Function argument count is known at compile time and most functions have < 10 arguments.
|
||||
// This avoids allocating the default 16-slot array when only 2-3 bindings are needed.
|
||||
sizeHint := len(args)
|
||||
e.e.childWithBindingSizeHint(rule.Body, child, sizeHint)
|
||||
child.findOne = findOne
|
||||
|
||||
var result *ast.Term
|
||||
@@ -2362,7 +2368,7 @@ func (e *evalFunc) partialEvalSupportRule(rule *ast.Rule, path ast.Ref) error {
|
||||
child := evalPool.Get()
|
||||
defer evalPool.Put(child)
|
||||
|
||||
e.e.child(rule.Body, child)
|
||||
e.e.childWithBindingSizeHint(rule.Body, child, ast.EstimateBodyBindingCount(rule.Body))
|
||||
child.traceEnter(rule)
|
||||
|
||||
e.e.saveStack.PushQuery(nil)
|
||||
@@ -2900,7 +2906,7 @@ func (e evalVirtualPartial) evalAllRulesNoCache(rules []*ast.Rule) (*ast.Term, e
|
||||
defer evalPool.Put(child)
|
||||
|
||||
for _, rule := range rules {
|
||||
e.e.child(rule.Body, child)
|
||||
e.e.childWithBindingSizeHint(rule.Body, child, ast.EstimateBodyBindingCount(rule.Body))
|
||||
child.traceEnter(rule)
|
||||
err := child.eval(func(*eval) error {
|
||||
child.traceExit(rule)
|
||||
@@ -2936,7 +2942,7 @@ func (e evalVirtualPartial) evalOneRulePreUnify(iter unifyIterator, rule *ast.Ru
|
||||
child := evalPool.Get()
|
||||
defer evalPool.Put(child)
|
||||
|
||||
e.e.child(rule.Body, child)
|
||||
e.e.childWithBindingSizeHint(rule.Body, child, ast.EstimateBodyBindingCount(rule.Body))
|
||||
|
||||
child.traceEnter(rule)
|
||||
var defined bool
|
||||
@@ -3031,7 +3037,7 @@ func (e evalVirtualPartial) evalOneRulePostUnify(iter unifyIterator, rule *ast.R
|
||||
child := evalPool.Get()
|
||||
defer evalPool.Put(child)
|
||||
|
||||
e.e.child(rule.Body, child)
|
||||
e.e.childWithBindingSizeHint(rule.Body, child, ast.EstimateBodyBindingCount(rule.Body))
|
||||
|
||||
child.traceEnter(rule)
|
||||
var defined bool
|
||||
@@ -3118,7 +3124,7 @@ func (e evalVirtualPartial) partialEvalSupportRule(rule *ast.Rule, _ ast.Ref) (b
|
||||
child := evalPool.Get()
|
||||
defer evalPool.Put(child)
|
||||
|
||||
e.e.child(rule.Body, child)
|
||||
e.e.childWithBindingSizeHint(rule.Body, child, ast.EstimateBodyBindingCount(rule.Body))
|
||||
child.traceEnter(rule)
|
||||
|
||||
e.e.saveStack.PushQuery(nil)
|
||||
@@ -3608,7 +3614,7 @@ func (e evalVirtualComplete) evalValueRule(iter unifyIterator, rule *ast.Rule, p
|
||||
child := evalPool.Get()
|
||||
defer evalPool.Put(child)
|
||||
|
||||
e.e.child(rule.Body, child)
|
||||
e.e.childWithBindingSizeHint(rule.Body, child, ast.EstimateBodyBindingCount(rule.Body))
|
||||
child.findOne = findOne
|
||||
child.traceEnter(rule)
|
||||
var result *ast.Term
|
||||
@@ -3647,7 +3653,7 @@ func (e evalVirtualComplete) partialEval(iter unifyIterator) error {
|
||||
defer evalPool.Put(child)
|
||||
|
||||
for _, rule := range e.ir.Rules {
|
||||
e.e.child(rule.Body, child)
|
||||
e.e.childWithBindingSizeHint(rule.Body, child, ast.EstimateBodyBindingCount(rule.Body))
|
||||
child.traceEnter(rule)
|
||||
|
||||
err := child.eval(func(child *eval) error {
|
||||
@@ -3723,7 +3729,7 @@ func (e evalVirtualComplete) partialEvalSupportRule(rule *ast.Rule, packagePath
|
||||
child := evalPool.Get()
|
||||
defer evalPool.Put(child)
|
||||
|
||||
e.e.child(rule.Body, child)
|
||||
e.e.childWithBindingSizeHint(rule.Body, child, ast.EstimateBodyBindingCount(rule.Body))
|
||||
child.traceEnter(rule)
|
||||
|
||||
e.e.saveStack.PushQuery(nil)
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package topdown
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
||||
)
|
||||
|
||||
// Benchmark rule evaluation with varying body sizes
|
||||
func BenchmarkRuleBindings(b *testing.B) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
module string
|
||||
query string
|
||||
}{
|
||||
{
|
||||
name: "Rule_1Expr",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
allow if {
|
||||
input.user == "admin"
|
||||
}`,
|
||||
query: "data.test.allow",
|
||||
},
|
||||
{
|
||||
name: "Rule_2Exprs",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
allow if {
|
||||
input.user == "admin"
|
||||
input.role == "superuser"
|
||||
}`,
|
||||
query: "data.test.allow",
|
||||
},
|
||||
{
|
||||
name: "Rule_3Exprs",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
allow if {
|
||||
input.user == "admin"
|
||||
input.role == "superuser"
|
||||
input.department == "engineering"
|
||||
}`,
|
||||
query: "data.test.allow",
|
||||
},
|
||||
{
|
||||
name: "Rule_5Exprs",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
allow if {
|
||||
user := input.user
|
||||
role := input.role
|
||||
user == "admin"
|
||||
role == "superuser"
|
||||
input.active == true
|
||||
}`,
|
||||
query: "data.test.allow",
|
||||
},
|
||||
{
|
||||
name: "Rule_10Exprs",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
allow if {
|
||||
user := input.user
|
||||
role := input.role
|
||||
dept := input.department
|
||||
status := input.status
|
||||
level := input.level
|
||||
user == "admin"
|
||||
role == "superuser"
|
||||
dept == "engineering"
|
||||
status == "active"
|
||||
level > 5
|
||||
}`,
|
||||
query: "data.test.allow",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
mod := ast.MustParseModule(tc.module)
|
||||
compiler := ast.NewCompiler()
|
||||
compiler.Compile(map[string]*ast.Module{"test": mod})
|
||||
if compiler.Failed() {
|
||||
b.Fatalf("Compilation failed: %v", compiler.Errors)
|
||||
}
|
||||
|
||||
store := inmem.New()
|
||||
ctx := context.Background()
|
||||
input := map[string]any{
|
||||
"user": "admin",
|
||||
"role": "superuser",
|
||||
"department": "engineering",
|
||||
"status": "active",
|
||||
"level": 10,
|
||||
"active": true,
|
||||
}
|
||||
|
||||
var m1, m2 runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m1)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
q := NewQuery(ast.MustParseBody(tc.query)).
|
||||
WithCompiler(compiler).
|
||||
WithStore(store).
|
||||
WithInput(ast.NewTerm(ast.MustInterfaceToValue(input)))
|
||||
|
||||
_, err := q.Run(ctx)
|
||||
if err != nil {
|
||||
b.Fatalf("Query failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m2)
|
||||
|
||||
allocPerOp := (m2.TotalAlloc - m1.TotalAlloc) / uint64(b.N)
|
||||
mallocsPerOp := (m2.Mallocs - m1.Mallocs) / uint64(b.N)
|
||||
|
||||
b.ReportMetric(float64(allocPerOp), "B/op")
|
||||
b.ReportMetric(float64(mallocsPerOp), "allocs/op")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark rules with comprehensions in body
|
||||
func BenchmarkRuleWithComprehensions(b *testing.B) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
module string
|
||||
query string
|
||||
}{
|
||||
{
|
||||
name: "Rule_WithArrayComp",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
filtered_users := [u |
|
||||
some u in input.users
|
||||
u.active == true
|
||||
]
|
||||
|
||||
result if {
|
||||
count(filtered_users) > 0
|
||||
}`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "Rule_WithSetComp",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
admin_names := {u.name |
|
||||
some u in input.users
|
||||
u.role == "admin"
|
||||
}
|
||||
|
||||
result if {
|
||||
count(admin_names) > 0
|
||||
}`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
{
|
||||
name: "Rule_WithMultipleComps",
|
||||
module: `package test
|
||||
import rego.v1
|
||||
|
||||
active_users := [u | some u in input.users; u.active]
|
||||
admin_users := [u | some u in active_users; u.role == "admin"]
|
||||
|
||||
result if {
|
||||
count(admin_users) > 0
|
||||
}`,
|
||||
query: "data.test.result",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
mod := ast.MustParseModule(tc.module)
|
||||
compiler := ast.NewCompiler()
|
||||
compiler.Compile(map[string]*ast.Module{"test": mod})
|
||||
if compiler.Failed() {
|
||||
b.Fatalf("Compilation failed: %v", compiler.Errors)
|
||||
}
|
||||
|
||||
store := inmem.New()
|
||||
ctx := context.Background()
|
||||
input := map[string]any{
|
||||
"users": []map[string]any{
|
||||
{"name": "alice", "role": "admin", "active": true},
|
||||
{"name": "bob", "role": "user", "active": true},
|
||||
{"name": "charlie", "role": "admin", "active": false},
|
||||
},
|
||||
}
|
||||
|
||||
var m1, m2 runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m1)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
q := NewQuery(ast.MustParseBody(tc.query)).
|
||||
WithCompiler(compiler).
|
||||
WithStore(store).
|
||||
WithInput(ast.NewTerm(ast.MustInterfaceToValue(input)))
|
||||
|
||||
_, err := q.Run(ctx)
|
||||
if err != nil {
|
||||
b.Fatalf("Query failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m2)
|
||||
|
||||
allocPerOp := (m2.TotalAlloc - m1.TotalAlloc) / uint64(b.N)
|
||||
mallocsPerOp := (m2.Mallocs - m1.Mallocs) / uint64(b.N)
|
||||
|
||||
b.ReportMetric(float64(allocPerOp), "B/op")
|
||||
b.ReportMetric(float64(mallocsPerOp), "allocs/op")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark complex rule scenarios
|
||||
func BenchmarkComplexRules(b *testing.B) {
|
||||
module := `package test
|
||||
import rego.v1
|
||||
|
||||
# Multi-expression rule with assignments
|
||||
process_request if {
|
||||
user := input.user
|
||||
resource := input.resource
|
||||
action := input.action
|
||||
user.authenticated
|
||||
resource.accessible
|
||||
allowed_actions := ["read", "write", "delete"]
|
||||
action in allowed_actions
|
||||
user.role == "admin"
|
||||
}
|
||||
|
||||
# Rule with nested conditions
|
||||
check_permissions if {
|
||||
user := input.user
|
||||
required_role := "admin"
|
||||
user.role == required_role
|
||||
perms := user.permissions
|
||||
"write" in perms
|
||||
user.active == true
|
||||
}
|
||||
|
||||
# Rule with multiple comprehensions
|
||||
analyze_access if {
|
||||
users := [u | some u in input.users; u.active]
|
||||
admins := {u.name | some u in users; u.role == "admin"}
|
||||
count(admins) > 0
|
||||
}`
|
||||
|
||||
mod := ast.MustParseModule(module)
|
||||
compiler := ast.NewCompiler()
|
||||
compiler.Compile(map[string]*ast.Module{"test": mod})
|
||||
if compiler.Failed() {
|
||||
b.Fatalf("Compilation failed: %v", compiler.Errors)
|
||||
}
|
||||
|
||||
store := inmem.New()
|
||||
ctx := context.Background()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
query string
|
||||
input map[string]any
|
||||
}{
|
||||
{
|
||||
name: "ProcessRequest",
|
||||
query: "data.test.process_request",
|
||||
input: map[string]any{
|
||||
"user": map[string]any{
|
||||
"authenticated": true,
|
||||
"role": "admin",
|
||||
},
|
||||
"resource": map[string]any{
|
||||
"accessible": true,
|
||||
},
|
||||
"action": "read",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CheckPermissions",
|
||||
query: "data.test.check_permissions",
|
||||
input: map[string]any{
|
||||
"user": map[string]any{
|
||||
"role": "admin",
|
||||
"permissions": []string{"read", "write"},
|
||||
"active": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AnalyzeAccess",
|
||||
query: "data.test.analyze_access",
|
||||
input: map[string]any{
|
||||
"users": []map[string]any{
|
||||
{"name": "alice", "role": "admin", "active": true},
|
||||
{"name": "bob", "role": "user", "active": true},
|
||||
{"name": "charlie", "role": "admin", "active": false},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
var m1, m2 runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m1)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
q := NewQuery(ast.MustParseBody(tc.query)).
|
||||
WithCompiler(compiler).
|
||||
WithStore(store).
|
||||
WithInput(ast.NewTerm(ast.MustInterfaceToValue(tc.input)))
|
||||
|
||||
_, err := q.Run(ctx)
|
||||
if err != nil {
|
||||
b.Fatalf("Query failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m2)
|
||||
|
||||
allocPerOp := (m2.TotalAlloc - m1.TotalAlloc) / uint64(b.N)
|
||||
mallocsPerOp := (m2.Mallocs - m1.Mallocs) / uint64(b.N)
|
||||
|
||||
b.ReportMetric(float64(allocPerOp), "B/op")
|
||||
b.ReportMetric(float64(mallocsPerOp), "allocs/op")
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user