From c2b397a9f66ff7b8978bd7a3396a91b11fe1a136 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 29 Dec 2025 00:51:50 -0500 Subject: [PATCH] feat: observer pattern for statistics --- Makefile | 8 +-- cmd/lambda/lambda.go | 19 +++++-- internal/executer/executer.go | 22 +++----- internal/executer/profiler.go | 40 --------------- internal/profiler/profiler.go | 48 +++++++++++++++++ internal/{executer => statistics}/results.go | 2 +- internal/statistics/statistics.go | 30 +++++++++++ pkg/emitter/emitter.go | 54 ++++++++++++++++++++ pkg/lambda/generate_name.go | 2 +- pkg/lambda/get_free_variables.go | 2 +- pkg/set/set.go | 8 +-- 11 files changed, 165 insertions(+), 70 deletions(-) delete mode 100644 internal/executer/profiler.go create mode 100644 internal/profiler/profiler.go rename internal/{executer => statistics}/results.go (96%) create mode 100644 internal/statistics/statistics.go create mode 100644 pkg/emitter/emitter.go diff --git a/Makefile b/Makefile index 74ec1f2..3e2d091 100644 --- a/Makefile +++ b/Makefile @@ -5,16 +5,16 @@ it: @ chmod +x ${BINARY_NAME} simple: it - @ ./lambda.exe -p profile/simple.prof - < ./samples/simple.txt > program.out + @ ./lambda.exe -p profile/cpu.prof - < ./samples/simple.txt > program.out thunk: it - @ ./lambda.exe - < ./samples/thunk.txt > program.out + @ ./lambda.exe -p profile/cpu.prof - < ./samples/thunk.txt > program.out saccharine: it - @ ./lambda.exe - < ./samples/saccharine.txt > program.out + @ ./lambda.exe -p profile/cpu.prof - < ./samples/saccharine.txt > program.out church: it - @ ./lambda.exe - < ./samples/church.txt > program.out + @ ./lambda.exe -p profile/cpu.prof - < ./samples/church.txt > program.out prof: @ go tool pprof -top profile/cpu.prof diff --git a/cmd/lambda/lambda.go b/cmd/lambda/lambda.go index 679a337..eae18fd 100644 --- a/cmd/lambda/lambda.go +++ b/cmd/lambda/lambda.go @@ -7,6 +7,8 @@ import ( "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/internal/profiler" + "git.maximhutz.com/max/lambda/internal/statistics" "git.maximhutz.com/max/lambda/pkg/convert" "git.maximhutz.com/max/lambda/pkg/lambda" "git.maximhutz.com/max/lambda/pkg/saccharine" @@ -41,10 +43,21 @@ func main() { logger.Info("compiled lambda expression", "tree", lambda.Stringify(compiled)) } - executor := executer.New(options) - results, err := executor.Run(&compiled) + process := executer.New(options) + if options.Profile != "" { + profiler := profiler.New(options.Profile) + process.On("start", profiler.Start) + process.On("end", profiler.End) + } + + statistics := &statistics.Profiler{} + process.On("start", statistics.Start) + process.On("step", statistics.Step) + process.On("end", statistics.End) + + process.Run(&compiled) cli.HandleError(err) fmt.Println(lambda.Stringify(compiled)) - fmt.Fprint(os.Stderr, results.String()) + fmt.Fprint(os.Stderr, statistics.Results.String()) } diff --git a/internal/executer/executer.go b/internal/executer/executer.go index 7491435..6ecd0f2 100644 --- a/internal/executer/executer.go +++ b/internal/executer/executer.go @@ -3,47 +3,37 @@ package executer import ( "fmt" "log/slog" - "time" "git.maximhutz.com/max/lambda/internal/config" + "git.maximhutz.com/max/lambda/pkg/emitter" "git.maximhutz.com/max/lambda/pkg/lambda" ) type Executor struct { Config *config.Config + emitter.Emitter[*lambda.Expression] } 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() +func (e Executor) Run(expr *lambda.Expression) { + e.Emit("start", expr) if e.Config.Explanation { fmt.Println(lambda.Stringify(*expr)) } for lambda.ReduceOnce(expr) { + e.Emit("step", 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 + e.Emit("end", expr) } diff --git a/internal/executer/profiler.go b/internal/executer/profiler.go deleted file mode 100644 index ad770a5..0000000 --- a/internal/executer/profiler.go +++ /dev/null @@ -1,40 +0,0 @@ -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/profiler/profiler.go b/internal/profiler/profiler.go new file mode 100644 index 0000000..e8f56a0 --- /dev/null +++ b/internal/profiler/profiler.go @@ -0,0 +1,48 @@ +package profiler + +import ( + "os" + "path/filepath" + "runtime/pprof" + + "git.maximhutz.com/max/lambda/pkg/lambda" +) + +type Profiler struct { + File string + filePointer *os.File + Error error +} + +func New(file string) *Profiler { + return &Profiler{File: file} +} + +func (p *Profiler) Start(*lambda.Expression) { + var absPath string + + absPath, p.Error = filepath.Abs(p.File) + if p.Error != nil { + return + } + + p.Error = os.MkdirAll(filepath.Dir(absPath), 0777) + if p.Error != nil { + return + } + + p.filePointer, p.Error = os.Create(absPath) + if p.Error != nil { + return + } + + p.Error = pprof.StartCPUProfile(p.filePointer) + if p.Error != nil { + return + } +} + +func (p *Profiler) End(*lambda.Expression) { + pprof.StopCPUProfile() + p.filePointer.Close() +} diff --git a/internal/executer/results.go b/internal/statistics/results.go similarity index 96% rename from internal/executer/results.go rename to internal/statistics/results.go index 0e1e0de..a26453b 100644 --- a/internal/executer/results.go +++ b/internal/statistics/results.go @@ -1,4 +1,4 @@ -package executer +package statistics import ( "fmt" diff --git a/internal/statistics/statistics.go b/internal/statistics/statistics.go new file mode 100644 index 0000000..9e3d667 --- /dev/null +++ b/internal/statistics/statistics.go @@ -0,0 +1,30 @@ +package statistics + +import ( + "time" + + "git.maximhutz.com/max/lambda/pkg/lambda" +) + +type Profiler struct { + start time.Time + steps uint64 + Results *Results +} + +func (p *Profiler) Start(*lambda.Expression) { + p.start = time.Now() + p.steps = 0 + p.Results = nil +} + +func (p *Profiler) Step(*lambda.Expression) { + p.steps++ +} + +func (p *Profiler) End(*lambda.Expression) { + p.Results = &Results{ + StepsTaken: p.steps, + TimeElapsed: uint64(time.Since(p.start).Milliseconds()), + } +} diff --git a/pkg/emitter/emitter.go b/pkg/emitter/emitter.go new file mode 100644 index 0000000..451d1f2 --- /dev/null +++ b/pkg/emitter/emitter.go @@ -0,0 +1,54 @@ +package emitter + +import "git.maximhutz.com/max/lambda/pkg/set" + +type Observer[T any] struct { + fn func(T) + message string + emitter *Emitter[T] +} + +type Emitter[T any] struct { + listeners map[string]*set.Set[*Observer[T]] +} + +func Ignore[T any](fn func()) func(T) { + return func(T) { fn() } +} + +func (e *Emitter[T]) On(message string, fn func(T)) *Observer[T] { + observer := &Observer[T]{ + fn: fn, + message: message, + emitter: e, + } + + if e.listeners == nil { + e.listeners = map[string]*set.Set[*Observer[T]]{} + } + + if e.listeners[message] == nil { + e.listeners[message] = set.New[*Observer[T]]() + } + + e.listeners[message].Add(observer) + return observer +} + +func (o *Observer[T]) Off() { + if o.emitter.listeners[o.message] == nil { + return + } + + o.emitter.listeners[o.message].Remove(o) +} + +func (e *Emitter[T]) Emit(message string, value T) { + if e.listeners[message] == nil { + return + } + + for listener := range *e.listeners[message] { + listener.fn(value) + } +} diff --git a/pkg/lambda/generate_name.go b/pkg/lambda/generate_name.go index ffafe76..86097bb 100644 --- a/pkg/lambda/generate_name.go +++ b/pkg/lambda/generate_name.go @@ -6,7 +6,7 @@ import ( "git.maximhutz.com/max/lambda/pkg/set" ) -func GenerateFreshName(used set.Set[string]) string { +func GenerateFreshName(used *set.Set[string]) string { for i := uint64(0); ; i++ { attempt := "_" + string(strconv.AppendUint(nil, i, 10)) diff --git a/pkg/lambda/get_free_variables.go b/pkg/lambda/get_free_variables.go index 3cb8b0e..ec635e9 100644 --- a/pkg/lambda/get_free_variables.go +++ b/pkg/lambda/get_free_variables.go @@ -2,7 +2,7 @@ package lambda import "git.maximhutz.com/max/lambda/pkg/set" -func GetFreeVariables(e Expression) set.Set[string] { +func GetFreeVariables(e Expression) *set.Set[string] { switch e := e.(type) { case *Variable: return set.New(e.Value) diff --git a/pkg/set/set.go b/pkg/set/set.go index dc4ac29..27435d0 100644 --- a/pkg/set/set.go +++ b/pkg/set/set.go @@ -18,8 +18,8 @@ func (s *Set[T]) Remove(items ...T) { } } -func (s *Set[T]) Merge(o Set[T]) { - for item := range o { +func (s *Set[T]) Merge(o *Set[T]) { + for item := range *o { s.Add(item) } } @@ -34,8 +34,8 @@ func (s Set[T]) ToList() []T { return list } -func New[T comparable](items ...T) Set[T] { - result := Set[T]{} +func New[T comparable](items ...T) *Set[T] { + result := &Set[T]{} for _, item := range items { result.Add(item)