diff --git a/expr.go b/expr.go index 0325f81f8..2ee5beb08 100644 --- a/expr.go +++ b/expr.go @@ -194,6 +194,13 @@ func Timezone(name string) Option { }) } +// Profile enable profiling of the program execution. +func Profile() Option { + return func(c *conf.Config) { + c.Profile = true + } +} + // Compile parses and compiles given input expression to bytecode program. func Compile(input string, ops ...Option) (*vm.Program, error) { config := conf.CreateNew() diff --git a/profile/.gitignore b/profile/.gitignore new file mode 100644 index 000000000..1f5688de0 --- /dev/null +++ b/profile/.gitignore @@ -0,0 +1,2 @@ +*.pprof +*.out \ No newline at end of file diff --git a/profile/go.mod b/profile/go.mod new file mode 100644 index 000000000..fa00ba838 --- /dev/null +++ b/profile/go.mod @@ -0,0 +1,10 @@ +module github.com/expr-lang/expr/profile + +go 1.22 + +require ( + github.com/expr-lang/expr v0.0.0 + github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 +) + +replace github.com/expr-lang/expr => ../ diff --git a/profile/go.sum b/profile/go.sum new file mode 100644 index 000000000..f04910c3a --- /dev/null +++ b/profile/go.sum @@ -0,0 +1,2 @@ +github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= +github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= diff --git a/profile/profile.go b/profile/profile.go new file mode 100644 index 000000000..46bd3b098 --- /dev/null +++ b/profile/profile.go @@ -0,0 +1,129 @@ +package profile + +import ( + "bytes" + "fmt" + "os" + + "github.com/google/pprof/profile" + + "github.com/expr-lang/expr/vm" +) + +// GeneratePprofProfile generates a pprof-formatted profile file based on the Span structure. +// Parameters: +// - rootSpan: The root Span structure, containing information about the expression's runtime and hierarchical structure. +// - filePath: The path to save the generated pprof file. +// +// Returns: +// - An error if an error occurs during the generation process; otherwise, returns nil. +func GeneratePprofProfile(rootSpan *vm.Span, filePath string) error { + // Create a new pprof configuration file + p := &profile.Profile{ + // Define the type and unit of the sampling period, here it's CPU time in nanoseconds + PeriodType: &profile.ValueType{ + Type: "cpu", + Unit: "nanoseconds", + }, + // Define the type and unit of the sampling data, also CPU time in nanoseconds + SampleType: []*profile.ValueType{ + { + Type: "cpu", + Unit: "nanoseconds", + }, + }, + } + + // Create maps to store function and location information + // The key is the function name, and the value is the profile.Function structure + functionMap := make(map[string]*profile.Function) + // The key is a combination of the function name and the expression, and the value is the profile.Location structure + locationMap := make(map[string]*profile.Location) + + // Recursively traverse the Span structure + var traverse func(span *vm.Span, parentLocation *profile.Location) + traverse = func(span *vm.Span, parentLocation *profile.Location) { + // Get or create function and location information + // Retrieve the function name from the Span structure + functionName := span.Name + // Check if the function name already exists in the functionMap + if _, ok := functionMap[functionName]; !ok { + // If not, create a new profile.Function structure + functionMap[functionName] = &profile.Function{ + // Assign a unique ID + ID: uint64(len(p.Function) + 1), + // Function name + Name: functionName, + // System function name, same as the function name here + SystemName: functionName, + } + // Add the new function information to the pprof configuration file + p.Function = append(p.Function, functionMap[functionName]) + } + + // Generate the key for the location information, a combination of the function name and the expression + locationKey := fmt.Sprintf("%s:%s", functionName, span.Expression) + // Check if the location information already exists in the locationMap + if _, ok := locationMap[locationKey]; !ok { + // If not, create a new profile.Location structure + locationMap[locationKey] = &profile.Location{ + // Assign a unique ID + ID: uint64(len(p.Location) + 1), + // Line number information for the location + Line: []profile.Line{ + { + // Associated function information + Function: functionMap[functionName], + // Line number is 1 + Line: 1, + }, + }, + } + // Add the new location information to the pprof configuration file + p.Location = append(p.Location, locationMap[locationKey]) + } + + // Create sample information + sample := &profile.Sample{ + // The value of the sample, i.e., the duration of the Span converted to nanoseconds + Value: []int64{int64(span.Duration)}, + // Location information for the sample + Location: []*profile.Location{ + locationMap[locationKey], + }, + } + // If there is parent location information, add it to the sample's location information + if parentLocation != nil { + sample.Location = append([]*profile.Location{parentLocation}, sample.Location...) + } + // Add the new sample information to the pprof configuration file + p.Sample = append(p.Sample, sample) + + // Recursively process child Spans + for _, child := range span.Children { + // Recursively call the traverse function to process child Spans + traverse(child, locationMap[locationKey]) + } + } + + // Start traversing from the root Span + traverse(rootSpan, nil) + + // Write to the pprof file + // Create a byte buffer to store the content of the pprof file + var buf bytes.Buffer + // Write the pprof configuration file to the buffer + if err := p.Write(&buf); err != nil { + // If an error occurs during the writing process, return the error information + return err + } + + // Write the content of the buffer to the specified file + if err := os.WriteFile(filePath, buf.Bytes(), 0644); err != nil { + // If an error occurs during the file writing process, return the error information + return err + } + + // No errors occurred, return nil + return nil +} diff --git a/profile/profile_test.go b/profile/profile_test.go new file mode 100644 index 000000000..2d33d2318 --- /dev/null +++ b/profile/profile_test.go @@ -0,0 +1,38 @@ +package profile_test + +import ( + "testing" + "time" + + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/profile" + "github.com/expr-lang/expr/vm" +) + +func TestProfileExample(t *testing.T) { + prg, err := expr.Compile(`(a + b) + (a + c) + func1() + func2()`, + expr.Profile(), + expr.Env(map[string]any{ + "a": int64(1), + "b": int64(1), + "c": int64(1), + }), + expr.Function("func1", func(params ...any) (any, error) { + time.Sleep(time.Second) + return 3, nil + }), + expr.Function("func2", func(params ...any) (any, error) { + time.Sleep(time.Second) + return 4, nil + }), + ) + if err != nil { + t.Error(err) + } + out, err := expr.Run(prg, map[string]any{"a": int64(3), "b": int64(2), "c": int64(3)}) + if err != nil { + t.Error(err) + } + t.Log(out) + t.Logf("%s", profile.GeneratePprofProfile(vm.GetSpan(prg), "./profile.pprof")) +}