From 0717f92f8d8ad7a6d5327230ba6cf0e364dc93d4 Mon Sep 17 00:00:00 2001 From: Joshua Rich Date: Fri, 10 May 2024 16:13:05 +1000 Subject: [PATCH] fix(agent): :bug: enable more profiling options --- internal/logging/logging.go | 16 ----- internal/logging/profiling.go | 124 ++++++++++++++++++++++++++++++++++ main.go | 15 ++-- 3 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 internal/logging/profiling.go diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 78469dc40..ca19e8ed9 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -6,8 +6,6 @@ package logging import ( - "fmt" - "net/http" _ "net/http/pprof" "os" "path/filepath" @@ -23,20 +21,6 @@ func init() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) } -func SetProfiling() { - go func() { - for i := 6060; i < 6070; i++ { - log.Debug(). - Msgf("Starting profiler web interface on localhost:" + fmt.Sprint(i)) - err := http.ListenAndServe("localhost:"+fmt.Sprint(i), nil) - if err != nil { - log.Debug().Err(err). - Msg("Trouble starting profiler, trying again.") - } - } - }() -} - // SetLoggingLevel sets an appropriate log level and enables profiling if requested. func SetLoggingLevel(level string) { switch level { diff --git a/internal/logging/profiling.go b/internal/logging/profiling.go new file mode 100644 index 000000000..af5c1203b --- /dev/null +++ b/internal/logging/profiling.go @@ -0,0 +1,124 @@ +// Copyright (c) 2024 Joshua Rich +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +package logging + +import ( + "errors" + "fmt" + "net/http" + "os" + "runtime" + "runtime/pprof" + "runtime/trace" + "strconv" + + "github.com/rs/zerolog/log" +) + +type ProfileFlags map[string]string + +func StartProfiling(flags ProfileFlags) error { + for k, v := range flags { + switch k { + case "webui": + webui, err := strconv.ParseBool(v) + if err != nil { + return errors.Join(errors.New("could not interpret webui value"), err) + } + if webui { + go func() { + for i := 6060; i < 6070; i++ { + log.Debug(). + Msgf("Starting profiler web interface on localhost:" + fmt.Sprint(i)) + err := http.ListenAndServe("localhost:"+fmt.Sprint(i), nil) + if err != nil { + log.Debug().Err(err). + Msg("Trouble starting profiler, trying again.") + } + } + }() + } + case "heapprofile": + log.Debug().Msg("Heap profiling enabled.") + case "cpuprofile": + f, err := os.Create(v) + if err != nil { + log.Fatal().Err(err).Msg("Cannot create CPU profile.") + } + if err := pprof.StartCPUProfile(f); err != nil { + log.Fatal().Err(err).Msg("Could not start CPU profiling.") + } + log.Debug().Msg("CPU profiling enabled.") + case "traceprofile": + f, err := os.Create(v) + if err != nil { + log.Fatal().Err(err).Msg("Cannot create trace profile.") + } + if err = trace.Start(f); err != nil { + log.Fatal().Err(err).Msg("Could not start trace profiling.") + } + log.Debug().Msg("Trace profiling enabled.") + default: + return fmt.Errorf("unknown argument for profiling: %s=%s", k, v) + } + } + return nil +} + +func StopProfiling(flags ProfileFlags) error { + for k, v := range flags { + switch k { + case "heapprofile": + f, err := os.Create(v) + if err != nil { + return errors.Join(errors.New("cannot create heap profile file"), err) + } + var ms runtime.MemStats + runtime.ReadMemStats(&ms) + printMemStats(&ms) + if err := pprof.WriteHeapProfile(f); err != nil { + return errors.Join(errors.New("cannot write to heap profile file"), err) + } + _ = f.Close() + log.Debug().Msgf("Heap profile written to %s", v) + case "cpuprofile": + pprof.StopCPUProfile() + case "traceprofile": + trace.Stop() + } + } + return nil +} + +// printMemStats and formatMemory functions are taken from golang-ci source + +func printMemStats(ms *runtime.MemStats) { + log.Debug().Msgf("Mem stats: alloc=%s total_alloc=%s sys=%s "+ + "heap_alloc=%s heap_sys=%s heap_idle=%s heap_released=%s heap_in_use=%s "+ + "stack_in_use=%s stack_sys=%s "+ + "mspan_sys=%s mcache_sys=%s buck_hash_sys=%s gc_sys=%s other_sys=%s "+ + "mallocs_n=%d frees_n=%d heap_objects_n=%d gc_cpu_fraction=%.2f", + formatMemory(ms.Alloc), formatMemory(ms.TotalAlloc), formatMemory(ms.Sys), + formatMemory(ms.HeapAlloc), formatMemory(ms.HeapSys), + formatMemory(ms.HeapIdle), formatMemory(ms.HeapReleased), formatMemory(ms.HeapInuse), + formatMemory(ms.StackInuse), formatMemory(ms.StackSys), + formatMemory(ms.MSpanSys), formatMemory(ms.MCacheSys), formatMemory(ms.BuckHashSys), + formatMemory(ms.GCSys), formatMemory(ms.OtherSys), + ms.Mallocs, ms.Frees, ms.HeapObjects, ms.GCCPUFraction) +} + +func formatMemory(memBytes uint64) string { + const Kb = 1024 + const Mb = Kb * 1024 + + if memBytes < Kb { + return fmt.Sprintf("%db", memBytes) + } + if memBytes < Mb { + return fmt.Sprintf("%dkb", memBytes/Kb) + } + return fmt.Sprintf("%dmb", memBytes/Mb) +} diff --git a/main.go b/main.go index 7d8fca6c1..4d9e0b794 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ package main import ( + "errors" "path/filepath" "syscall" @@ -30,11 +31,10 @@ func (d logLevel) AfterApply() error { return nil } -type profileFlag bool +type profileFlags logging.ProfileFlags -func (d profileFlag) AfterApply() error { - logging.SetProfiling() - return nil +func (d profileFlags) AfterApply() error { + return logging.StartProfiling(logging.ProfileFlags(d)) } type noLogFileFlag bool @@ -47,8 +47,8 @@ func (d noLogFileFlag) AfterApply() error { } type Context struct { + Profile profileFlags AppID string - Profile profileFlag Headless bool } @@ -172,10 +172,10 @@ var CLI struct { Run RunCmd `cmd:"" help:"Run Go Hass Agent."` Reset ResetCmd `cmd:"" help:"Reset Go Hass Agent."` Version VersionCmd `cmd:"" help:"Show the Go Hass Agent version."` + Profile profileFlags `help:"Enable profiling."` AppID string `name:"appid" default:"${defaultAppID}" help:"Specify a custom app id (for debugging)."` LogLevel logLevel `name:"log-level" help:"Set logging level."` Register RegisterCmd `cmd:"" help:"Register with Home Assistant."` - Profile profileFlag `help:"Enable profiling."` NoLog noLogFileFlag `help:"Don't write to a log file."` Headless bool `name:"terminal" help:"Run without a GUI."` } @@ -198,5 +198,8 @@ func main() { kong.Description(preferences.AppDescription) ctx := kong.Parse(&CLI, kong.Bind(), kong.Vars{"defaultAppID": preferences.AppID}) err := ctx.Run(&Context{Headless: CLI.Headless, Profile: CLI.Profile, AppID: CLI.AppID}) + if CLI.Profile != nil { + errors.Join(logging.StopProfiling(logging.ProfileFlags(CLI.Profile)), err) + } ctx.FatalIfErrorf(err) }