From 4cddc2636309c94eda86c3099e12f10cd8cab6ea Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Fri, 23 Apr 2021 16:32:24 +0200 Subject: [PATCH 1/9] Rudimentary stack profiler Usage: JSONNET_STACK_PROFILE=stacks.out jsonnet main.jsonnet cat stacks.out | flamegraph.pl --hash > flamegraph.svg --- cmd/jsonnet/cmd.go | 3 +++ interpreter.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/cmd/jsonnet/cmd.go b/cmd/jsonnet/cmd.go index ca217abec..a7e9de64f 100644 --- a/cmd/jsonnet/cmd.go +++ b/cmd/jsonnet/cmd.go @@ -397,6 +397,9 @@ func main() { cmd.StartCPUProfile() defer cmd.StopCPUProfile() + jsonnet.StartStackProfile() + defer jsonnet.StopStackProfile() + vm := jsonnet.MakeVM() vm.ErrorFormatter.SetColorFormatter(color.New(color.FgRed).Fprintf) diff --git a/interpreter.go b/interpreter.go index a2ba1e2f3..b948c5dc0 100644 --- a/interpreter.go +++ b/interpreter.go @@ -17,13 +17,18 @@ limitations under the License. package jsonnet import ( + "bufio" "bytes" "fmt" "io" + "log" "math" + "math/rand" + "os" "reflect" "sort" "strconv" + "strings" "github.com/google/go-jsonnet/ast" "github.com/google/go-jsonnet/astgen" @@ -968,7 +973,45 @@ func jsonToValue(i *interpreter, v interface{}) (value, error) { } } +var ( + stackProfileOut *bufio.Writer + stackProfileRatio = 0.01 +) + +func StartStackProfile() { + var err error + + if os.Getenv("JSONNET_STACK_PROFILE") != "" { + stackProfileOutFile, err := os.Create(os.Getenv("JSONNET_STACK_PROFILE")) + if err != nil { + log.Fatal("could not create stack profile: ", err) + } + stackProfileOut = bufio.NewWriter(stackProfileOutFile) + } + + if os.Getenv("JSONNET_STACK_PROFILE_RATIO") != "" { + stackProfileRatio, err = strconv.ParseFloat(os.Getenv("JSONNET_STACK_PROFILE_RATIO"), 64) + if err != nil { + log.Fatal("could not parse stack profile ratio: ", err) + } + } +} + +func StopStackProfile() { + if stackProfileOut != nil { + stackProfileOut.Flush() + } +} + func (i *interpreter) EvalInCleanEnv(env *environment, ast ast.Node, trimmable bool) (value, error) { + if stackProfileOut != nil && rand.Float64() < stackProfileRatio { + stack := []string{} + for _, frame := range i.getCurrentStackTrace() { + stack = append(stack, frame.Loc.String()+":"+frame.Name) + } + fmt.Fprintln(stackProfileOut, strings.Join(stack, ";")+" 1") + } + err := i.newCall(*env, trimmable) if err != nil { return nil, err From cd4115e2af17f86251c84a708765e8b56bc7cb06 Mon Sep 17 00:00:00 2001 From: b13rg <13488329+B13rg@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:50:26 -0700 Subject: [PATCH 2/9] Store stack profiling options in struct --- interpreter.go | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/interpreter.go b/interpreter.go index 275481fea..bd9d2c156 100644 --- a/interpreter.go +++ b/interpreter.go @@ -286,6 +286,12 @@ type interpreter struct { stack callStack evalHook EvalHook + +type StackProfilerOpts struct { + // + stackProfileOut *bufio.Writer + // ration of stack ast to sample (0.0-1.0) + stackProfileRatio float64 } // Map union, b takes precedence when keys collide. @@ -1014,33 +1020,35 @@ func jsonToValue(i *interpreter, v interface{}) (value, error) { } } -var ( - stackProfileOut *bufio.Writer - stackProfileRatio = 0.01 -) - -func StartStackProfile() { - var err error - +func StartStackProfile() StackProfilerOpts { if os.Getenv("JSONNET_STACK_PROFILE") != "" { - stackProfileOutFile, err := os.Create(os.Getenv("JSONNET_STACK_PROFILE")) + file, err := os.Create(os.Getenv("JSONNET_STACK_PROFILE")) if err != nil { log.Fatal("could not create stack profile: ", err) } - stackProfileOut = bufio.NewWriter(stackProfileOutFile) - } - if os.Getenv("JSONNET_STACK_PROFILE_RATIO") != "" { - stackProfileRatio, err = strconv.ParseFloat(os.Getenv("JSONNET_STACK_PROFILE_RATIO"), 64) - if err != nil { - log.Fatal("could not parse stack profile ratio: ", err) + sampleRatio := 0.1 + + if os.Getenv("JSONNET_STACK_PROFILE_RATIO") != "" { + sampleRatio, err = strconv.ParseFloat(os.Getenv("JSONNET_STACK_PROFILE_RATIO"), 64) + if err != nil { + log.Fatal("could not parse stack profile ratio: ", err) + } } + return StackProfilerOpts{ + stackProfileOut: bufio.NewWriter(file), + stackProfileRatio: sampleRatio, + } + } + return StackProfilerOpts{ + stackProfileOut: nil, + stackProfileRatio: 0.1, } } -func StopStackProfile() { - if stackProfileOut != nil { - stackProfileOut.Flush() +func StopStackProfile(opts StackProfilerOpts) { + if opts.stackProfileOut != nil { + opts.stackProfileOut.Flush() } } From c23a8b76e7b2275afd15e54cbe19616323a0998e Mon Sep 17 00:00:00 2001 From: b13rg <13488329+B13rg@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:50:54 -0700 Subject: [PATCH 3/9] set profilerOpts var in main --- cmd/jsonnet/cmd.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/jsonnet/cmd.go b/cmd/jsonnet/cmd.go index 6ee1599c2..debf093f5 100644 --- a/cmd/jsonnet/cmd.go +++ b/cmd/jsonnet/cmd.go @@ -395,8 +395,8 @@ func main() { cmd.StartCPUProfile() defer cmd.StopCPUProfile() - jsonnet.StartStackProfile() - defer jsonnet.StopStackProfile() + profilerOpts := jsonnet.StartStackProfile() + defer jsonnet.StopStackProfile(profilerOpts) vm := jsonnet.MakeVM() vm.ErrorFormatter.SetColorFormatter(color.New(color.FgRed).Fprintf) From 1bfb0c49c8cd3144027c59a4ac7293bf52f71767 Mon Sep 17 00:00:00 2001 From: b13rg <13488329+B13rg@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:51:36 -0700 Subject: [PATCH 4/9] add profilerOpts field to interpreter --- interpreter.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interpreter.go b/interpreter.go index bd9d2c156..07ee79d02 100644 --- a/interpreter.go +++ b/interpreter.go @@ -287,6 +287,9 @@ type interpreter struct { evalHook EvalHook + profilerOpts StackProfilerOpts +} + type StackProfilerOpts struct { // stackProfileOut *bufio.Writer From 5b2e747fde44034115530d904070c6f6f61ebd96 Mon Sep 17 00:00:00 2001 From: b13rg <13488329+B13rg@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:52:05 -0700 Subject: [PATCH 5/9] extract sampling function --- interpreter.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/interpreter.go b/interpreter.go index 07ee79d02..2bee8eaae 100644 --- a/interpreter.go +++ b/interpreter.go @@ -1056,13 +1056,7 @@ func StopStackProfile(opts StackProfilerOpts) { } func (i *interpreter) EvalInCleanEnv(env *environment, ast ast.Node, trimmable bool) (value, error) { - if stackProfileOut != nil && rand.Float64() < stackProfileRatio { - stack := []string{} - for _, frame := range i.getCurrentStackTrace() { - stack = append(stack, frame.Loc.String()+":"+frame.Name) - } - fmt.Fprintln(stackProfileOut, strings.Join(stack, ";")+" 1") - } + i.checkForSampling() err := i.newCall(*env, trimmable) if err != nil { @@ -1080,6 +1074,16 @@ func (i *interpreter) EvalInCleanEnv(env *environment, ast ast.Node, trimmable b return val, nil } +func (i *interpreter) checkForSampling() { + if i.profilerOpts.stackProfileOut != nil && rand.Float64() < i.profilerOpts.stackProfileRatio { + stack := []string{} + for _, frame := range i.getCurrentStackTrace() { + stack = append(stack, frame.Loc.String()+":"+frame.Name) + } + fmt.Fprintln(i.profilerOpts.stackProfileOut, strings.Join(stack, ";")+" 1") + } +} + func (i *interpreter) evaluatePV(ph potentialValue) (value, error) { return ph.getValue(i) } From 11f298ff2d57d76676309b5f09198d6b7d7e9281 Mon Sep 17 00:00:00 2001 From: b13rg <13488329+B13rg@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:55:40 -0700 Subject: [PATCH 6/9] comment --- interpreter.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/interpreter.go b/interpreter.go index 2bee8eaae..76faf64a4 100644 --- a/interpreter.go +++ b/interpreter.go @@ -1023,6 +1023,12 @@ func jsonToValue(i *interpreter, v interface{}) (value, error) { } } +// This parses env variables and configures the VM interpreter stack profiler +// Env vars: +// +// JSONNET_STACK_PROFILE: If set, it will output a stack profile to the file specified. +// +// JSONNET_STACK_PROFILE_RATIO: Determines the ration of stack traces to sample. Default 0.1 func StartStackProfile() StackProfilerOpts { if os.Getenv("JSONNET_STACK_PROFILE") != "" { file, err := os.Create(os.Getenv("JSONNET_STACK_PROFILE")) @@ -1068,6 +1074,8 @@ func (i *interpreter) EvalInCleanEnv(env *environment, ast ast.Node, trimmable b if err != nil { return nil, err } +// Check profiling flags and sample if needed. +// Samples randomly based on interpreter.profilerOpts.stackProfileRatio. i.stack.popIfExists(stackSize) From 488da1e3ad36be51abef0afe1166378c649a3e70 Mon Sep 17 00:00:00 2001 From: b13rg <13488329+B13rg@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:57:28 -0700 Subject: [PATCH 7/9] flatten interpreter evaluate function params --- interpreter.go | 36 ++++++++++++++---------------------- vm.go | 12 ++++++------ 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/interpreter.go b/interpreter.go index 76faf64a4..03d8b4031 100644 --- a/interpreter.go +++ b/interpreter.go @@ -1074,14 +1074,14 @@ func (i *interpreter) EvalInCleanEnv(env *environment, ast ast.Node, trimmable b if err != nil { return nil, err } -// Check profiling flags and sample if needed. -// Samples randomly based on interpreter.profilerOpts.stackProfileRatio. i.stack.popIfExists(stackSize) return val, nil } +// Check profiling flags and sample if needed. +// Samples randomly based on interpreter.profilerOpts.stackProfileRatio. func (i *interpreter) checkForSampling() { if i.profilerOpts.stackProfileOut != nil && rand.Float64() < i.profilerOpts.stackProfileRatio { stack := []string{} @@ -1416,23 +1416,21 @@ func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, error) { return result, nil } -// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead -func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction, - maxStack int, ic *importCache, traceOut io.Writer, stringOutputMode bool, evalHook EvalHook) (string, error) { - - i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut, evalHook) +// Evaluate ast node with the given VM +func evaluate(node ast.Node, vm *VM) (string, error) { + i, err := buildInterpreter(vm.ext, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook, vm.profilerOpts) if err != nil { return "", err } - result, err := evaluateAux(i, node, tla) + result, err := evaluateAux(i, node, vm.tla) if err != nil { return "", err } var buf bytes.Buffer i.stack.setCurrentTrace(manifestationTrace()) - if stringOutputMode { + if vm.StringOutput { err = i.manifestString(&buf, result) } else { err = i.manifestAndSerializeJSON(&buf, result, true, "") @@ -1445,36 +1443,30 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string] return buf.String(), nil } -// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead -func evaluateMulti(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction, - maxStack int, ic *importCache, traceOut io.Writer, stringOutputMode bool, evalHook EvalHook) (map[string]string, error) { - - i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut, evalHook) +func evaluateMulti(node ast.Node, vm *VM) (map[string]string, error) { + i, err := buildInterpreter(vm.ext, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook, vm.profilerOpts) if err != nil { return nil, err } - result, err := evaluateAux(i, node, tla) + result, err := evaluateAux(i, node, vm.tla) if err != nil { return nil, err } i.stack.setCurrentTrace(manifestationTrace()) - manifested, err := i.manifestAndSerializeMulti(result, stringOutputMode) + manifested, err := i.manifestAndSerializeMulti(result, vm.StringOutput) i.stack.clearCurrentTrace() return manifested, err } -// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead -func evaluateStream(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction, - maxStack int, ic *importCache, traceOut io.Writer, evalHook EvalHook) ([]string, error) { - - i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut, evalHook) +func evaluateStream(node ast.Node, vm *VM) ([]string, error) { + i, err := buildInterpreter(vm.ext, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook, vm.profilerOpts) if err != nil { return nil, err } - result, err := evaluateAux(i, node, tla) + result, err := evaluateAux(i, node, vm.tla) if err != nil { return nil, err } diff --git a/vm.go b/vm.go index a6d5932c9..a1cbe18ea 100644 --- a/vm.go +++ b/vm.go @@ -187,7 +187,7 @@ func (vm *VM) Evaluate(node ast.Node) (val string, err error) { err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack()) } }() - return evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) + return evaluate(node, vm) } // EvaluateStream evaluates a Jsonnet program given by an Abstract Syntax Tree @@ -198,7 +198,7 @@ func (vm *VM) EvaluateStream(node ast.Node) (output []string, err error) { err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack()) } }() - return evaluateStream(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook) + return evaluateStream(node, vm) } // EvaluateMulti evaluates a Jsonnet program given by an Abstract Syntax Tree @@ -210,7 +210,7 @@ func (vm *VM) EvaluateMulti(node ast.Node) (output map[string]string, err error) err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack()) } }() - return evaluateMulti(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) + return evaluateMulti(node, vm) } func (vm *VM) evaluateSnippet(diagnosticFileName ast.DiagnosticFileName, filename string, snippet string, kind evalKind) (output interface{}, err error) { @@ -225,11 +225,11 @@ func (vm *VM) evaluateSnippet(diagnosticFileName ast.DiagnosticFileName, filenam } switch kind { case evalKindRegular: - output, err = evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) + output, err = evaluate(node, vm) case evalKindMulti: - output, err = evaluateMulti(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) + output, err = evaluateMulti(node, vm) case evalKindStream: - output, err = evaluateStream(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook) + output, err = evaluateStream(node, vm) } if err != nil { return "", err From cabf6b8fabbb89f4ce1eba934aace7ddad05d5b2 Mon Sep 17 00:00:00 2001 From: b13rg <13488329+B13rg@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:58:17 -0700 Subject: [PATCH 8/9] add profiler opts to VM and interpreter init --- cmd/jsonnet/cmd.go | 2 ++ interpreter.go | 13 +++++++------ vm.go | 8 ++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/cmd/jsonnet/cmd.go b/cmd/jsonnet/cmd.go index debf093f5..58ff3a62f 100644 --- a/cmd/jsonnet/cmd.go +++ b/cmd/jsonnet/cmd.go @@ -401,6 +401,8 @@ func main() { vm := jsonnet.MakeVM() vm.ErrorFormatter.SetColorFormatter(color.New(color.FgRed).Fprintf) + vm.SetStackTraceOut(profilerOpts) + config := makeConfig() jsonnetPath := filepath.SplitList(os.Getenv("JSONNET_PATH")) for i := len(jsonnetPath) - 1; i >= 0; i-- { diff --git a/interpreter.go b/interpreter.go index 03d8b4031..7fbe0c92e 100644 --- a/interpreter.go +++ b/interpreter.go @@ -1339,13 +1339,14 @@ func buildObject(hide ast.ObjectFieldHide, fields map[string]value) *valueObject return makeValueSimpleObject(bindingFrame{}, fieldMap, nil, nil) } -func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*NativeFunction, maxStack int, ic *importCache, traceOut io.Writer, evalHook EvalHook) (*interpreter, error) { +func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*NativeFunction, maxStack int, ic *importCache, traceOut io.Writer, evalHook EvalHook, profilerOpts StackProfilerOpts) (*interpreter, error) { i := interpreter{ - stack: makeCallStack(maxStack), - importCache: ic, - traceOut: traceOut, - nativeFuncs: nativeFuncs, - evalHook: evalHook, + stack: makeCallStack(maxStack), + importCache: ic, + traceOut: traceOut, + nativeFuncs: nativeFuncs, + evalHook: evalHook, + profilerOpts: profilerOpts, } stdObj, err := buildStdObject(&i) diff --git a/vm.go b/vm.go index a1cbe18ea..69a4305b4 100644 --- a/vm.go +++ b/vm.go @@ -46,6 +46,7 @@ type VM struct { //nolint:govet StringOutput bool importCache *importCache traceOut io.Writer + profilerOpts StackProfilerOpts EvalHook EvalHook } @@ -106,6 +107,13 @@ func (vm *VM) SetTraceOut(traceOut io.Writer) { vm.traceOut = traceOut } +// SetStackTraceOut configures options for interpreter stack trace sampling. +// It is used for performance profiling purposes. +// The output can be used to generate flamegraphs of calls stemming from interpreter.EvalInCleanEnv +func (vm *VM) SetStackTraceOut(traceOut StackProfilerOpts) { + vm.profilerOpts = traceOut +} + // ExtVar binds a Jsonnet external var to the given value. func (vm *VM) ExtVar(key string, val string) { vm.ext[key] = vmExt{value: val, kind: extKindVar} From 38c7c5ec9030c170a7728c56f044470771a3e8eb Mon Sep 17 00:00:00 2001 From: b13rg <13488329+B13rg@users.noreply.github.com> Date: Sat, 5 Apr 2025 22:00:28 -0700 Subject: [PATCH 9/9] flatten interpreter generation --- interpreter.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/interpreter.go b/interpreter.go index 7fbe0c92e..c9dadf432 100644 --- a/interpreter.go +++ b/interpreter.go @@ -1339,14 +1339,14 @@ func buildObject(hide ast.ObjectFieldHide, fields map[string]value) *valueObject return makeValueSimpleObject(bindingFrame{}, fieldMap, nil, nil) } -func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*NativeFunction, maxStack int, ic *importCache, traceOut io.Writer, evalHook EvalHook, profilerOpts StackProfilerOpts) (*interpreter, error) { +func buildInterpreter(vm *VM) (*interpreter, error) { i := interpreter{ - stack: makeCallStack(maxStack), - importCache: ic, - traceOut: traceOut, - nativeFuncs: nativeFuncs, - evalHook: evalHook, - profilerOpts: profilerOpts, + stack: makeCallStack(vm.MaxStack), + importCache: vm.importCache, + traceOut: vm.traceOut, + nativeFuncs: vm.nativeFuncs, + evalHook: vm.EvalHook, + profilerOpts: vm.profilerOpts, } stdObj, err := buildStdObject(&i) @@ -1356,7 +1356,7 @@ func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*NativeFunction, maxS i.baseStd = stdObj - i.extVars = prepareExtVars(&i, ext, "extvar") + i.extVars = prepareExtVars(&i, vm.ext, "extvar") return &i, nil } @@ -1419,7 +1419,7 @@ func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, error) { // Evaluate ast node with the given VM func evaluate(node ast.Node, vm *VM) (string, error) { - i, err := buildInterpreter(vm.ext, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook, vm.profilerOpts) + i, err := buildInterpreter(vm) if err != nil { return "", err } @@ -1445,7 +1445,7 @@ func evaluate(node ast.Node, vm *VM) (string, error) { } func evaluateMulti(node ast.Node, vm *VM) (map[string]string, error) { - i, err := buildInterpreter(vm.ext, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook, vm.profilerOpts) + i, err := buildInterpreter(vm) if err != nil { return nil, err } @@ -1462,7 +1462,7 @@ func evaluateMulti(node ast.Node, vm *VM) (map[string]string, error) { } func evaluateStream(node ast.Node, vm *VM) ([]string, error) { - i, err := buildInterpreter(vm.ext, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook, vm.profilerOpts) + i, err := buildInterpreter(vm) if err != nil { return nil, err }