diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go index fae806d3318..621a251a10e 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go @@ -19,6 +19,7 @@ package cel import ( "fmt" "strings" + "sync" "time" "github.com/google/cel-go/cel" @@ -66,6 +67,27 @@ type CompilationResult struct { MaxCost uint64 } +var ( + initEnvOnce sync.Once + initEnv *cel.Env + initEnvErr error +) + +func getBaseEnv() (*cel.Env, error) { + initEnvOnce.Do(func() { + var opts []cel.EnvOption + opts = append(opts, cel.HomogeneousAggregateLiterals()) + // Validate function declarations once during base env initialization, + // so they don't need to be evaluated each time a CEL rule is compiled. + // This is a relatively expensive operation. + opts = append(opts, cel.EagerlyValidateDeclarations(true)) + opts = append(opts, library.ExtensionLibs...) + + initEnv, initEnvErr = cel.NewEnv(opts...) + }) + return initEnv, initEnvErr +} + // Compile compiles all the XValidations rules (without recursing into the schema) and returns a slice containing a // CompilationResult for each ValidationRule, or an error. // Each CompilationResult may contain: @@ -82,13 +104,11 @@ func Compile(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) ([] var propDecls []*expr.Decl var root *celmodel.DeclType var ok bool - env, err := cel.NewEnv( - cel.HomogeneousAggregateLiterals(), - ) + baseEnv, err := getBaseEnv() if err != nil { return nil, err } - reg := celmodel.NewRegistry(env) + reg := celmodel.NewRegistry(baseEnv) scopedTypeName := generateUniqueSelfTypeName() rt, err := celmodel.NewRuleTypes(scopedTypeName, s, isResourceRoot, reg) if err != nil { @@ -97,7 +117,7 @@ func Compile(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) ([] if rt == nil { return nil, nil } - opts, err := rt.EnvOptions(env.TypeProvider()) + opts, err := rt.EnvOptions(baseEnv.TypeProvider()) if err != nil { return nil, err } @@ -111,9 +131,8 @@ func Compile(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) ([] } propDecls = append(propDecls, decls.NewVar(ScopedVarName, root.ExprType())) propDecls = append(propDecls, decls.NewVar(OldScopedVarName, root.ExprType())) - opts = append(opts, cel.Declarations(propDecls...), cel.HomogeneousAggregateLiterals()) - opts = append(opts, library.ExtensionLibs...) - env, err = env.Extend(opts...) + opts = append(opts, cel.Declarations(propDecls...)) + env, err := baseEnv.Extend(opts...) if err != nil { return nil, err } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go index c0b914dae99..8f7d9df52a9 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go @@ -1605,3 +1605,19 @@ func TestCostEstimation(t *testing.T) { }) } } + +func BenchmarkCompile(b *testing.B) { + _, err := getBaseEnv() // prime the baseEnv + if err != nil { + b.Fatal(err) + } + s := genArrayWithRule("number", "true")(nil) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Compile(s, false, uint64(math.MaxInt64)) + if err != nil { + b.Fatal(err) + } + } +}