From cb5ced1ccb3981173222d3ac261d52350b44b924 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Tue, 23 Jun 2026 23:53:28 +0545 Subject: [PATCH] perf(context): skip unused CEL env funcs RunTemplate constructed every registered CEL env function before evaluating any template, even for simple CEL expressions that did not call those functions. This created avoidable closure and cel.Function allocation churn on cache-hit evaluations. Only append registered CEL env options when the expression calls the corresponding function, while preserving support for legacy registry keys that end in Cel. Add a benchmark for cache-hit CEL evaluation with varying registered function counts. --- bench/template_cel_env_bench_test.go | 94 ++++++++++++++++++++++++++++ context/template.go | 60 +++++++++++++++++- context/template_test.go | 89 ++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 bench/template_cel_env_bench_test.go diff --git a/bench/template_cel_env_bench_test.go b/bench/template_cel_env_bench_test.go new file mode 100644 index 000000000..c7e23cd66 --- /dev/null +++ b/bench/template_cel_env_bench_test.go @@ -0,0 +1,94 @@ +package bench_test + +import ( + "fmt" + "testing" + "time" + + dutyctx "github.com/flanksource/duty/context" + "github.com/flanksource/gomplate/v3" + "github.com/google/cel-go/cel" + celtypes "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +// BenchmarkRunTemplateCELCacheHitRegisteredEnvFuncs isolates the duty-side +// overhead of constructing registered CEL env functions on every RunTemplate +// call. The expression does not call any registered function; the benchmark only +// varies how many functions are registered globally. +func BenchmarkRunTemplateCELCacheHitRegisteredEnvFuncs(b *testing.B) { + env := map[string]any{ + "id": "0192f0a4-1234-7000-8000-aaaaaaaaaaaa", + "namespace": "default", + "name": "nginx-7c5ddbdf54-abcde", + "index": 42, + "config_type": "Kubernetes::Pod", + } + + for _, registeredFuncs := range []int{0, 18, 64} { + b.Run(fmt.Sprintf("registered=%02d", registeredFuncs), func(b *testing.B) { + installBenchmarkCelEnvFuncs(b, registeredFuncs) + + ctx := dutyctx.New() + tmpl := gomplate.Template{ + Expression: `config_type == "Kubernetes::Pod"`, + CacheKey: fmt.Sprintf("benchmark.run-template.cel-env-funcs.%d", registeredFuncs), + CacheTime: time.Hour, + } + + // Warm gomplate's compiled CEL program cache. Any measured allocation after + // this point should be steady-state RunTemplate overhead, not CEL compile. + if _, err := ctx.RunTemplateBool(tmpl, env); err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + ok, err := ctx.RunTemplateBool(tmpl, env) + if err != nil { + b.Fatal(err) + } + if !ok { + b.Fatal("expected expression to evaluate true") + } + } + }) + } +} + +func installBenchmarkCelEnvFuncs(b *testing.B, count int) { + b.Helper() + + oldCelEnvFuncs := dutyctx.CelEnvFuncs + oldTemplateFuncs := dutyctx.TemplateFuncs + + dutyctx.CelEnvFuncs = make(map[string]func(dutyctx.Context) cel.EnvOption, count) + for i := 0; i < count; i++ { + name := fmt.Sprintf("bench.func_%02d", i) + dutyctx.CelEnvFuncs[name] = benchmarkCelEnvFunc(name, fmt.Sprintf("bench_func_%02d_string", i)) + } + dutyctx.TemplateFuncs = nil + + b.Cleanup(func() { + dutyctx.CelEnvFuncs = oldCelEnvFuncs + dutyctx.TemplateFuncs = oldTemplateFuncs + }) +} + +func benchmarkCelEnvFunc(name, overloadID string) func(dutyctx.Context) cel.EnvOption { + return func(ctx dutyctx.Context) cel.EnvOption { + return cel.Function(name, + cel.Overload(overloadID, + []*cel.Type{cel.StringType}, + cel.StringType, + cel.UnaryBinding(func(arg ref.Val) ref.Val { + // Capture ctx like production DB/catalog functions do. This binding is + // intentionally never called by the benchmark expression. + _ = ctx + return celtypes.String(fmt.Sprintf("%s:%v", name, arg.Value())) + }), + ), + ) + } +} diff --git a/context/template.go b/context/template.go index 313ef73dc..d51213c71 100644 --- a/context/template.go +++ b/context/template.go @@ -3,6 +3,7 @@ package context import ( "fmt" "strconv" + "strings" "github.com/flanksource/commons/collections" "github.com/flanksource/commons/logger" @@ -23,9 +24,7 @@ func (k Context) RunTemplate(t gomplate.Template, env map[string]any) (string, e } else { l.V(1).Infof("Running template: %s", t.String()) } - for _, f := range CelEnvFuncs { - t.CelEnvs = append(t.CelEnvs, f(k)) - } + appendReferencedCelEnvFuncs(k, &t) if t.Functions == nil { t.Functions = make(map[string]any) } @@ -78,6 +77,61 @@ func (k Context) RunTemplate(t gomplate.Template, env map[string]any) (string, e return val, nil } +func appendReferencedCelEnvFuncs(ctx Context, t *gomplate.Template) { + if t.Expression == "" || !strings.Contains(t.Expression, "(") { + return + } + + for name, f := range CelEnvFuncs { + if celExpressionCalls(t.Expression, name) || (strings.HasSuffix(name, "Cel") && celExpressionCalls(t.Expression, strings.TrimSuffix(name, "Cel"))) { + t.CelEnvs = append(t.CelEnvs, f(ctx)) + } + } +} + +func celExpressionCalls(expr, name string) bool { + if name == "" { + return false + } + + for offset := 0; offset < len(expr); { + idx := strings.Index(expr[offset:], name) + if idx < 0 { + return false + } + idx += offset + afterName := idx + len(name) + if isCelNameBoundary(expr, idx, afterName) { + for afterName < len(expr) && isCELWhitespace(expr[afterName]) { + afterName++ + } + if afterName < len(expr) && expr[afterName] == '(' { + return true + } + } + offset = idx + len(name) + } + return false +} + +func isCelNameBoundary(expr string, start, end int) bool { + if start > 0 && isCELNameChar(expr[start-1]) { + return false + } + if end < len(expr) && isCELNameChar(expr[end]) { + return false + } + return true +} + +func isCELNameChar(ch byte) bool { + return ch == '.' || ch == '_' || ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') +} + +func isCELWhitespace(ch byte) bool { + return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' +} + func (k Context) RunTemplateBool(t gomplate.Template, env map[string]any) (bool, error) { output, err := k.RunTemplate(t, env) if err != nil { diff --git a/context/template_test.go b/context/template_test.go index 3b55949f9..d5897f6e0 100644 --- a/context/template_test.go +++ b/context/template_test.go @@ -1,12 +1,101 @@ package context import ( + "sync/atomic" "testing" "github.com/flanksource/gomplate/v3" + "github.com/google/cel-go/cel" + celtypes "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" . "github.com/onsi/gomega" ) +func TestRunTemplateOnlyBuildsReferencedCelEnvFuncs(t *testing.T) { + g := NewWithT(t) + oldCelEnvFuncs := CelEnvFuncs + oldTemplateFuncs := TemplateFuncs + defer func() { + CelEnvFuncs = oldCelEnvFuncs + TemplateFuncs = oldTemplateFuncs + }() + + var usedBuilt atomic.Int32 + var unusedBuilt atomic.Int32 + CelEnvFuncs = map[string]func(Context) cel.EnvOption{ + "bench.usedCel": func(ctx Context) cel.EnvOption { + usedBuilt.Add(1) + return cel.Function("bench.used", + cel.Overload("bench_used_string", + []*cel.Type{cel.StringType}, + cel.BoolType, + cel.UnaryBinding(func(arg ref.Val) ref.Val { + return celtypes.Bool(arg.Value() == "default") + }), + ), + ) + }, + "bench.unused": func(ctx Context) cel.EnvOption { + unusedBuilt.Add(1) + return cel.Function("bench.unused", + cel.Overload("bench_unused_string", + []*cel.Type{cel.StringType}, + cel.StringType, + cel.UnaryBinding(func(arg ref.Val) ref.Val { + return celtypes.String(arg.Value().(string)) + }), + ), + ) + }, + } + TemplateFuncs = nil + + ctx := New() + env := map[string]any{"config_namespace": "default"} + + ok, err := ctx.RunTemplateBool(gomplate.Template{ + Expression: `config_namespace == "default"`, + CacheKey: "test.run-template.no-registered-cel-func", + }, env) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + g.Expect(usedBuilt.Load()).To(Equal(int32(0))) + g.Expect(unusedBuilt.Load()).To(Equal(int32(0))) + + ok, err = ctx.RunTemplateBool(gomplate.Template{ + Expression: `bench.used(config_namespace)`, + CacheKey: "test.run-template.referenced-cel-func", + }, env) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + g.Expect(usedBuilt.Load()).To(Equal(int32(1))) + g.Expect(unusedBuilt.Load()).To(Equal(int32(0))) +} + +func TestCelExpressionCalls(t *testing.T) { + tests := []struct { + name string + expr string + fn string + want bool + }{ + {name: "direct namespaced call", expr: `bench.used(config_namespace)`, fn: "bench.used", want: true}, + {name: "whitespace before args", expr: `bench.used (config_namespace)`, fn: "bench.used", want: true}, + {name: "prefix is not a call", expr: `bench.used_extra(config_namespace)`, fn: "bench.used", want: false}, + {name: "suffix is not a call", expr: `other.bench.used(config_namespace)`, fn: "bench.used", want: false}, + {name: "similar db function prefix", expr: `db.external_users_all(scraper_id)`, fn: "db.external_users", want: false}, + {name: "exact db function", expr: `db.external_users_all(scraper_id)`, fn: "db.external_users_all", want: true}, + {name: "no function call", expr: `config_type == "Kubernetes::Pod"`, fn: "db.external_users", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + g.Expect(celExpressionCalls(tt.expr, tt.fn)).To(Equal(tt.want)) + }) + } +} + func TestRunTemplate(t *testing.T) { t.Parallel()