Skip to content

Commit 2cf1626

Browse files
Env copy on write (#1405)
* Program plan optimizations | Benchmark Case | Before (ns/op) | After (ns/op) | Δ Time | Before (B/op) | After (B/op) | Δ Memory | Before (allocs) | After (allocs) | Δ Allocs | | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | | BenchmarkProgramPlan/Default | 8,153 | 930 | **-88.6%** | 8,320 | 1,416 | **-83.0%** | 36 | 26 | **-27.8%** | | BenchmarkProgramPlan/OptimizeUnneeded | 7,344 | 1,150 | **-84.3%** | 8,784 | 1,512 | **-82.8%** | 50 | 32 | **-36.0%** | | BenchmarkProgramPlan/OptimizeNeeded | 8,370 | 2,164 | **-74.1%** | 10,224 | 2,976 | **-70.9%** | 67 | 52 | **-22.4%** | * Minor refactor of the initialization logic to reduce program size * Copy-on-write semantics for types.Registry and cel.Env internals * Ensure shared declarations aren't copied unless necessary within the checker * Capture NewEnv setup benchmarks as well * Fix race-related issue with copy-on-write mutability check * Eliminate dead-code from former Copy() approach. More tests * Bug fix to support disabling declarations when using inherited declarations
1 parent b74d303 commit 2cf1626

12 files changed

Lines changed: 616 additions & 128 deletions

File tree

cel/cel_test.go

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,90 @@ func Test_ExampleWithBuiltins(t *testing.T) {
9595
}
9696
}
9797

98+
func TestExtendCheckerParity(t *testing.T) {
99+
// Base environment carrying standard library functions
100+
baseEnv, err := NewEnv(
101+
Variable("baseVar", StringType),
102+
)
103+
if err != nil {
104+
t.Fatalf("NewEnv() failed: %v", err)
105+
}
106+
107+
// Extended environment adding child variables (K8s CRD pattern)
108+
extEnv, err := baseEnv.Extend(
109+
Variable("value", StringType),
110+
Variable("oldValue", StringType),
111+
)
112+
if err != nil {
113+
t.Fatalf("baseEnv.Extend() failed: %v", err)
114+
}
115+
116+
// Equivalent flat environment created from scratch
117+
flatEnv, err := NewEnv(
118+
Variable("baseVar", StringType),
119+
Variable("value", StringType),
120+
Variable("oldValue", StringType),
121+
)
122+
if err != nil {
123+
t.Fatalf("flat NewEnv() failed: %v", err)
124+
}
125+
126+
testCases := []struct {
127+
expr string
128+
vars map[string]any
129+
want ref.Val
130+
}{
131+
{
132+
expr: `value + " " + oldValue + " " + baseVar`,
133+
vars: map[string]any{"value": "new", "oldValue": "old", "baseVar": "base"},
134+
want: types.String("new old base"),
135+
},
136+
{
137+
expr: `size(value) > 0 && [1, 2, 3].exists(x, x > 2)`,
138+
vars: map[string]any{"value": "test"},
139+
want: types.True,
140+
},
141+
}
142+
143+
for _, tc := range testCases {
144+
extAst, extIss := extEnv.Compile(tc.expr)
145+
if extIss.Err() != nil {
146+
t.Fatalf("extEnv.Compile(%q) failed: %v", tc.expr, extIss.Err())
147+
}
148+
flatAst, flatIss := flatEnv.Compile(tc.expr)
149+
if flatIss.Err() != nil {
150+
t.Fatalf("flatEnv.Compile(%q) failed: %v", tc.expr, flatIss.Err())
151+
}
152+
153+
if extAst.OutputType().TypeName() != flatAst.OutputType().TypeName() {
154+
t.Errorf("OutputType mismatch for %q: ext %v, flat %v", tc.expr, extAst.OutputType(), flatAst.OutputType())
155+
}
156+
157+
extPrg, err := extEnv.Program(extAst)
158+
if err != nil {
159+
t.Fatalf("extEnv.Program() failed: %v", err)
160+
}
161+
flatPrg, err := flatEnv.Program(flatAst)
162+
if err != nil {
163+
t.Fatalf("flatEnv.Program() failed: %v", err)
164+
}
165+
166+
extOut, _, err := extPrg.Eval(tc.vars)
167+
if err != nil {
168+
t.Fatalf("extPrg.Eval() failed: %v", err)
169+
}
170+
flatOut, _, err := flatPrg.Eval(tc.vars)
171+
if err != nil {
172+
t.Fatalf("flatPrg.Eval() failed: %v", err)
173+
}
174+
175+
if extOut.Equal(tc.want) != types.True || flatOut.Equal(tc.want) != types.True {
176+
t.Errorf("Eval result mismatch for %q: ext %v, flat %v, want %v", tc.expr, extOut, flatOut, tc.want)
177+
}
178+
}
179+
}
180+
181+
98182
func TestCompile(t *testing.T) {
99183
prg, err := Compile(`"hello " + name`, Variable("name", StringType))
100184
if err != nil {
@@ -4003,12 +4087,45 @@ func BenchmarkDynamicDispatch(b *testing.B) {
40034087
}
40044088

40054089
func BenchmarkProgramPlan(b *testing.B) {
4006-
env, err := NewEnv(
4090+
b.Run("NewEnv", func(b *testing.B) {
4091+
b.ReportAllocs()
4092+
b.ResetTimer()
4093+
for i := 0; i < b.N; i++ {
4094+
_, err := NewEnv(
4095+
Variable("ai", IntType),
4096+
Variable("ar", MapType(StringType, StringType)),
4097+
)
4098+
if err != nil {
4099+
b.Fatalf("NewEnv() failed: %v", err)
4100+
}
4101+
}
4102+
})
4103+
4104+
baseEnv, err := NewEnv()
4105+
if err != nil {
4106+
b.Fatalf("NewEnv() failed: %v", err)
4107+
}
4108+
4109+
b.Run("ExtendEnv", func(b *testing.B) {
4110+
b.ReportAllocs()
4111+
b.ResetTimer()
4112+
for i := 0; i < b.N; i++ {
4113+
_, err := baseEnv.Extend(
4114+
Variable("ai", IntType),
4115+
Variable("ar", MapType(StringType, StringType)),
4116+
)
4117+
if err != nil {
4118+
b.Fatalf("baseEnv.Extend() failed: %v", err)
4119+
}
4120+
}
4121+
})
4122+
4123+
env, err := baseEnv.Extend(
40074124
Variable("ai", IntType),
40084125
Variable("ar", MapType(StringType, StringType)),
40094126
)
40104127
if err != nil {
4011-
b.Fatalf("NewEnv() failed: %v", err)
4128+
b.Fatalf("Extend() failed: %v", err)
40124129
}
40134130
astSimple, iss := env.Compile("ai == 20 || ar['foo'] == 'bar'")
40144131
if iss.Err() != nil {

cel/decls.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,8 @@ func FunctionDecls(funcs ...*decls.FunctionDecl) EnvOption {
223223
if len(funcs) == 0 {
224224
return e, nil
225225
}
226-
e.ensureMutableFunctions()
227226
var err error
227+
e.ensureMutableFunctions()
228228
for _, fn := range funcs {
229229
if existing, found := e.functions[fn.Name()]; found {
230230
fn, err = existing.Merge(fn)

cel/env.go

Lines changed: 32 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -532,34 +532,17 @@ func (e *Env) CompileSource(src Source) (*Ast, *Issues) {
532532
// TypeProvider are immutable, or that their underlying implementations are based on the
533533
// ref.TypeRegistry which provides a Copy method which will be invoked by this method.
534534
func (e *Env) Extend(opts ...EnvOption) (*Env, error) {
535-
chk, chkErr := e.getCheckerOrError()
536-
if chkErr != nil {
535+
if _, chkErr := e.getCheckerOrError(); chkErr != nil {
537536
return nil, chkErr
538537
}
539538

540-
prsrOptsCopy := make([]parser.Option, len(e.prsrOpts))
541-
copy(prsrOptsCopy, e.prsrOpts)
542-
543-
// The type-checker is configured with Declarations. The declarations may either be provided
544-
// as options which have not yet been validated, or may come from a previous checker instance
545-
// whose types have already been validated.
546-
chkOptsCopy := make([]checker.Option, len(e.chkOpts))
547-
copy(chkOptsCopy, e.chkOpts)
548-
549-
// Copy the declarations if needed.
550-
if chk != nil {
551-
// If the type-checker has already been instantiated, then the e.declarations have been
552-
// validated within the chk instance.
553-
chkOptsCopy = append(chkOptsCopy, checker.ValidatedDeclarations(chk))
554-
}
555-
varsCopy := make([]*decls.VariableDecl, len(e.variables))
556-
copy(varsCopy, e.variables)
557-
558-
// Copy macros and program options
559-
macsCopy := make([]parser.Macro, len(e.macros))
560-
progOptsCopy := make([]ProgramOption, len(e.progOpts))
561-
copy(macsCopy, e.macros)
562-
copy(progOptsCopy, e.progOpts)
539+
prsrOptsCopy := slices.Clone(e.prsrOpts)
540+
chkOptsCopy := slices.Clone(e.chkOpts)
541+
varsCopy := slices.Clone(e.variables)
542+
macsCopy := slices.Clone(e.macros)
543+
progOptsCopy := slices.Clone(e.progOpts)
544+
validatorsCopy := slices.Clone(e.validators)
545+
costOptsCopy := slices.Clone(e.costOptions)
563546

564547
// Copy the adapter / provider if they appear to be mutable.
565548
adapter := e.adapter
@@ -588,12 +571,6 @@ func (e *Env) Extend(opts ...EnvOption) (*Env, error) {
588571
adapter = adapterReg.Copy()
589572
}
590573

591-
validatorsCopy := make([]ASTValidator, len(e.validators))
592-
copy(validatorsCopy, e.validators)
593-
594-
costOptsCopy := make([]checker.CostOption, len(e.costOptions))
595-
copy(costOptsCopy, e.costOptions)
596-
597574
ext := &Env{
598575
parent: e,
599576
Container: e.Container,
@@ -687,25 +664,17 @@ func (e *Env) HasFunction(functionName string) bool {
687664

688665
// Functions returns a shallow copy of the Functions, keyed by function name, that have been configured in the environment.
689666
func (e *Env) Functions() map[string]*decls.FunctionDecl {
690-
shallowCopy := make(map[string]*decls.FunctionDecl, len(e.functions))
691-
for nm, fn := range e.functions {
692-
shallowCopy[nm] = fn
693-
}
694-
return shallowCopy
667+
return maps.Clone(e.functions)
695668
}
696669

697670
// Variables returns a shallow copy of the variables associated with the environment.
698671
func (e *Env) Variables() []*decls.VariableDecl {
699-
shallowCopy := make([]*decls.VariableDecl, len(e.variables))
700-
copy(shallowCopy, e.variables)
701-
return shallowCopy
672+
return slices.Clone(e.variables)
702673
}
703674

704675
// Macros returns a shallow copy of macros associated with the environment.
705676
func (e *Env) Macros() []Macro {
706-
shallowCopy := make([]Macro, len(e.macros))
707-
copy(shallowCopy, e.macros)
708-
return shallowCopy
677+
return slices.Clone(e.macros)
709678
}
710679

711680
// HasValidator returns whether a specific ASTValidator has been configured in the environment.
@@ -718,9 +687,9 @@ func (e *Env) HasValidator(name string) bool {
718687
return false
719688
}
720689

721-
// Validators returns the set of ASTValidators configured on the environment.
690+
// Validators returns a shallow copy of the set of ASTValidators configured on the environment.
722691
func (e *Env) Validators() []ASTValidator {
723-
return e.validators[:]
692+
return slices.Clone(e.validators)
724693
}
725694

726695
// Parse parses the input expression value `txt` to a Ast and/or a set of Issues.
@@ -1007,6 +976,15 @@ func (e *Env) initChecker() (*checker.Env, error) {
1007976
chkOpts = append(chkOpts,
1008977
checker.JSONFieldNames(e.HasFeature(featureJSONFieldNames)))
1009978

979+
if e.parent != nil && e.funcsShared {
980+
parentChk, err := e.parent.initChecker()
981+
if err != nil {
982+
e.setCheckerOrError(nil, err)
983+
return
984+
}
985+
chkOpts = append(chkOpts, checker.ValidatedDeclarations(parentChk))
986+
}
987+
1010988
ce, err := checker.NewEnv(e.Container, e.provider, chkOpts...)
1011989
if err != nil {
1012990
e.setCheckerOrError(nil, err)
@@ -1019,14 +997,16 @@ func (e *Env) initChecker() (*checker.Env, error) {
1019997
return
1020998
}
1021999
// Add the function declarations which are derived from the FunctionDecl instances.
1022-
for _, fn := range e.functions {
1023-
if fn.IsDeclarationDisabled() {
1024-
continue
1025-
}
1026-
err = ce.AddFunctions(fn)
1027-
if err != nil {
1028-
e.setCheckerOrError(nil, err)
1029-
return
1000+
if e.parent == nil || !e.funcsShared {
1001+
for _, fn := range e.functions {
1002+
if fn.IsDeclarationDisabled() {
1003+
continue
1004+
}
1005+
err = ce.AddFunctions(fn)
1006+
if err != nil {
1007+
e.setCheckerOrError(nil, err)
1008+
return
1009+
}
10301010
}
10311011
}
10321012
// Add function declarations here separately.

0 commit comments

Comments
 (0)