From e9dc3fe1710ee862830bd0ce89dd1e313f6e9efd Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 28 Dec 2025 22:52:10 -0500 Subject: [PATCH] feat: added optional profiling --- Makefile | 2 +- cmd/lambda/lambda.go | 38 +++-------------------- internal/cli/arguments.go | 37 ---------------------- internal/config/config.go | 10 +++--- internal/config/parse_from_args.go | 2 ++ internal/executer/executer.go | 49 ++++++++++++++++++++++++++++++ internal/executer/profiler.go | 40 ++++++++++++++++++++++++ internal/executer/results.go | 23 ++++++++++++++ 8 files changed, 124 insertions(+), 77 deletions(-) delete mode 100644 internal/cli/arguments.go create mode 100644 internal/executer/executer.go create mode 100644 internal/executer/profiler.go create mode 100644 internal/executer/results.go diff --git a/Makefile b/Makefile index 55ca7b8..74ec1f2 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ it: @ chmod +x ${BINARY_NAME} simple: it - @ ./lambda.exe - < ./samples/simple.txt > program.out + @ ./lambda.exe -p profile/simple.prof - < ./samples/simple.txt > program.out thunk: it @ ./lambda.exe - < ./samples/thunk.txt > program.out diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index be7ced2..679a337 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -3,11 +3,10 @@ package main import ( "fmt" "os" - "runtime/pprof" - "time" "git.maximhutz.com/max/lambda/internal/cli" "git.maximhutz.com/max/lambda/internal/config" + "git.maximhutz.com/max/lambda/internal/executer" "git.maximhutz.com/max/lambda/pkg/convert" "git.maximhutz.com/max/lambda/pkg/lambda" "git.maximhutz.com/max/lambda/pkg/saccharine" @@ -15,14 +14,6 @@ import ( // Run main application. func main() { - f, err := os.Create("profile/cpu.prof") - cli.HandleError(err) - defer f.Close() - - err = pprof.StartCPUProfile(f) - cli.HandleError(err) - defer pprof.StopCPUProfile() - options, err := config.FromArgs() cli.HandleError(err) @@ -50,29 +41,10 @@ func main() { logger.Info("compiled lambda expression", "tree", lambda.Stringify(compiled)) } - // Reduce expression. - start := time.Now() - - if options.Explanation { - fmt.Println(lambda.Stringify(compiled)) - } - - steps := 0 - - for lambda.ReduceOnce(&compiled) { - if options.Verbose { - logger.Info("reduction", "tree", lambda.Stringify(compiled)) - } - if options.Explanation { - fmt.Println(" =", lambda.Stringify(compiled)) - } - steps++ - } - - elapsed := time.Since(start).Milliseconds() + executor := executer.New(options) + results, err := executor.Run(&compiled) + cli.HandleError(err) fmt.Println(lambda.Stringify(compiled)) - fmt.Fprintln(os.Stderr, "Time Spent:", elapsed, "ms") - fmt.Fprintln(os.Stderr, "Steps:", steps) - fmt.Fprintln(os.Stderr, "Speed:", float32(steps)/(float32(elapsed)/1000), "ops") + fmt.Fprint(os.Stderr, results.String()) } diff --git a/internal/cli/arguments.go b/internal/cli/arguments.go deleted file mode 100644 index 261b4f0..0000000 --- a/internal/cli/arguments.go +++ /dev/null @@ -1,37 +0,0 @@ -package cli - -import ( - "flag" - "fmt" -) - -// Arguments given to program. -type Options struct { - // The source code given to the program. - Input string - // Whether or not to print debug logs. - Verbose bool - // Whether or not to print an explanation of the reduction. - Explanation bool -} - -// Extract the program configuration from the command-line arguments. -func ParseOptions() (*Options, error) { - // Parse flags. - verbose := flag.Bool("v", false, "Verbosity. If set, the program will print logs.") - explanation := flag.Bool("x", false, "Explanation. Whether or not to show all reduction steps.") - flag.Parse() - - // Parse non-flag arguments. - if flag.NArg() == 0 { - return nil, fmt.Errorf("no input given") - } else if flag.NArg() > 1 { - return nil, fmt.Errorf("more than 1 command-line argument") - } - - return &Options{ - Input: flag.Arg(0), - Verbose: *verbose, - Explanation: *explanation, - }, nil -} diff --git a/internal/config/config.go b/internal/config/config.go index cd74cef..75030fb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,10 +2,8 @@ package config // Arguments given to program. type Config struct { - // The source code given to the program. - Source Source - // Whether or not to print debug logs. - Verbose bool - // Whether or not to print an explanation of the reduction. - Explanation bool + Source Source // The source code given to the program. + Verbose bool // Whether or not to print debug logs. + Explanation bool // Whether or not to print an explanation of the reduction. + Profile string // If not nil, print a CPU profile during execution. } diff --git a/internal/config/parse_from_args.go b/internal/config/parse_from_args.go index 97dcd6d..8b92d9f 100644 --- a/internal/config/parse_from_args.go +++ b/internal/config/parse_from_args.go @@ -10,6 +10,7 @@ func FromArgs() (*Config, error) { // Parse flags. verbose := flag.Bool("v", false, "Verbosity. If set, the program will print logs.") explanation := flag.Bool("x", false, "Explanation. Whether or not to show all reduction steps.") + profile := flag.String("p", "", "CPU profiling. If set, the program will run a performance profile during execution.") flag.Parse() // Parse non-flag arguments. @@ -31,5 +32,6 @@ func FromArgs() (*Config, error) { Source: source, Verbose: *verbose, Explanation: *explanation, + Profile: *profile, }, nil } diff --git a/internal/executer/executer.go b/internal/executer/executer.go new file mode 100644 index 0000000..7491435 --- /dev/null +++ b/internal/executer/executer.go @@ -0,0 +1,49 @@ +package executer + +import ( + "fmt" + "log/slog" + "time" + + "git.maximhutz.com/max/lambda/internal/config" + "git.maximhutz.com/max/lambda/pkg/lambda" +) + +type Executor struct { + Config *config.Config +} + +func New(config *config.Config) *Executor { + return &Executor{Config: config} +} + +func (e Executor) Run(expr *lambda.Expression) (*Results, error) { + results := &Results{} + + if e.Config.Profile != "" { + profiler := Profiler{File: e.Config.Profile} + if err := profiler.Start(); err != nil { + return nil, err + } + defer profiler.End() + } + + start := time.Now() + + if e.Config.Explanation { + fmt.Println(lambda.Stringify(*expr)) + } + + for lambda.ReduceOnce(expr) { + if e.Config.Verbose { + slog.Info("reduction", "tree", lambda.Stringify(*expr)) + } + if e.Config.Explanation { + fmt.Println(" =", lambda.Stringify(*expr)) + } + results.StepsTaken++ + } + + results.TimeElapsed = uint64(time.Since(start).Milliseconds()) + return results, nil +} diff --git a/internal/executer/profiler.go b/internal/executer/profiler.go new file mode 100644 index 0000000..ad770a5 --- /dev/null +++ b/internal/executer/profiler.go @@ -0,0 +1,40 @@ +package executer + +import ( + "os" + "path/filepath" + "runtime/pprof" +) + +type Profiler struct { + File string + filePointer *os.File +} + +func (p *Profiler) Start() error { + absPath, err := filepath.Abs(p.File) + if err != nil { + return err + } + + err = os.MkdirAll(filepath.Dir(absPath), 0777) + if err != nil { + return err + } + + p.filePointer, err = os.Create(absPath) + if err != nil { + return err + } + + if err = pprof.StartCPUProfile(p.filePointer); err != nil { + return err + } + + return nil +} + +func (p *Profiler) End() { + pprof.StopCPUProfile() + p.filePointer.Close() +} diff --git a/internal/executer/results.go b/internal/executer/results.go new file mode 100644 index 0000000..0e1e0de --- /dev/null +++ b/internal/executer/results.go @@ -0,0 +1,23 @@ +package executer + +import ( + "fmt" + "strings" +) + +type Results struct { + StepsTaken uint64 // Number of steps taken during execution. + TimeElapsed uint64 // The time (ms) taken for execution to complete. +} + +func (r Results) OpsPerSecond() float32 { + return float32(r.StepsTaken) / (float32(r.TimeElapsed) / 1000) +} + +func (r Results) String() string { + builder := strings.Builder{} + fmt.Fprintln(&builder, "Time Spent:", r.TimeElapsed, "ms") + fmt.Fprintln(&builder, "Steps:", r.StepsTaken) + fmt.Fprintln(&builder, "Speed:", r.OpsPerSecond(), "ops") + return builder.String() +}