diff options
| -rw-r--r-- | README.md | 16 | ||||
| -rw-r--r-- | config.example.yaml | 5 | ||||
| -rw-r--r-- | config/config.go | 17 | ||||
| -rw-r--r-- | docker-compose.yml | 16 | ||||
| -rw-r--r-- | go.mod | 9 | ||||
| -rw-r--r-- | go.sum | 16 | ||||
| -rw-r--r-- | main.go | 36 |
7 files changed, 103 insertions, 12 deletions
@@ -26,8 +26,15 @@ go run . -extract-osm france-latest.osm.pbf # Extract mountain passes from a Fra go run . -backfill # Recompute and persist all ride computed values go run . -backtest # Benchmark computed vs stored ride values go run . -pprof # Expose pprof endpoints at `/debug/pprof` +docker compose up -d pyroscope # Start the local Pyroscope server on http://localhost:4040 ``` +Continuous profiling with Pyroscope: the app pushes CPU, memory, and goroutine +profiles every 10s when `pyroscope.enabled` is set in `config.yaml`. Run +`docker compose up -d pyroscope` to start the ingest server, then browse +`http://localhost:4040` to inspect the `biking_home` application profile. + + Configuration is stored in `config.yaml`. Start from `config.example.yaml` and set the Strava credentials before launching the server. ## Strava API @@ -149,17 +156,8 @@ Other endpoints: go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap # allocs ``` -Optional continuous profiling with Pyroscope (local docker, ~1-2% overhead): - -```bash -docker run --rm -p 4040:4040 grafana/pyroscope -``` - -Add `github.com/grafana/pyroscope-go` and call `pyroscope.Start` with `ServerAddress: "http://localhost:4040"` and `route` label — gives the same flamegraphs but over time. Keep it only if pprof snapshots are not enough. - ## TODO -- pyroscope - screaming archi + hexagon - Compute estimated power, speed - For a ride, scatter, one point per 100m segment. Slope vs speed diff --git a/config.example.yaml b/config.example.yaml index a4f099d..c4b22a2 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -11,6 +11,11 @@ storage: official_climb: match_radius_m: 100 +pyroscope: + enabled: false + application_name: biking_home + server_address: http://localhost:4040 + strava: client_id: "" client_secret: "" diff --git a/config/config.go b/config/config.go index 6fe9048..f95d3f8 100644 --- a/config/config.go +++ b/config/config.go @@ -16,6 +16,7 @@ type Config struct { Storage StorageConfig `yaml:"storage"` Strava StravaConfig `yaml:"strava"` OfficialClimb OfficialClimbConfig `yaml:"official_climb"` + Pyroscope PyroscopeConfig `yaml:"pyroscope"` } type DatabaseConfig struct { @@ -43,6 +44,12 @@ type OfficialClimbConfig struct { MatchRadiusM float64 `yaml:"match_radius_m"` } +type PyroscopeConfig struct { + Enabled bool `yaml:"enabled"` + ApplicationName string `yaml:"application_name"` + ServerAddress string `yaml:"server_address"` +} + func Default() Config { return Config{ Database: DatabaseConfig{Path: "biking_home.db"}, @@ -52,6 +59,10 @@ func Default() Config { }, Storage: StorageConfig{GPXDir: "data/rides"}, OfficialClimb: OfficialClimbConfig{MatchRadiusM: official_climb.DefaultMatchRadiusM}, + Pyroscope: PyroscopeConfig{ + ApplicationName: "biking_home", + ServerAddress: "http://localhost:4040", + }, } } @@ -73,6 +84,12 @@ func Load(filename string) (Config, error) { if config.Storage.GPXDir == "" { return Config{}, fmt.Errorf("config %q: storage.gpx_dir is required", filename) } + if config.Pyroscope.Enabled && config.Pyroscope.ApplicationName == "" { + return Config{}, fmt.Errorf("config %q: pyroscope.application_name is required when pyroscope is enabled", filename) + } + if config.Pyroscope.Enabled && config.Pyroscope.ServerAddress == "" { + return Config{}, fmt.Errorf("config %q: pyroscope.server_address is required when pyroscope is enabled", filename) + } if math.IsNaN(config.OfficialClimb.MatchRadiusM) || math.IsInf(config.OfficialClimb.MatchRadiusM, 0) || config.OfficialClimb.MatchRadiusM <= 0 { return Config{}, fmt.Errorf("config %q: official_climb.match_radius_m must be greater than zero", filename) } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6605075 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + pyroscope: + image: grafana/pyroscope:latest + container_name: pyroscope + ports: + - "4040:4040" + command: + - server + - --storage-path + - /home/pyroscope + volumes: + - pyroscope-data:/home/pyroscope + restart: unless-stopped + +volumes: + pyroscope-data: @@ -9,7 +9,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.16 github.com/paulmach/osm v0.9.0 github.com/schollz/progressbar/v3 v3.18.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/tkrajina/gpxgo v1.4.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -33,9 +33,12 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect + github.com/grafana/pyroscope-go v1.4.2 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.11 // indirect github.com/joho/godotenv v1.4.0 // indirect github.com/kataras/i18n v0.0.8 // indirect - github.com/kr/pretty v0.1.0 // indirect + github.com/klauspost/compress v1.18.7 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/lib/pq v1.10.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -66,7 +69,7 @@ require ( golang.org/x/text v0.41.0 // indirect golang.org/x/tools v0.49.0 // indirect google.golang.org/protobuf v1.36.10 // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) @@ -30,6 +30,7 @@ github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -47,6 +48,10 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/grafana/pyroscope-go v1.4.2 h1:0LW5HrUJXgGr9zF5gITP/HaFXN9/LsMiwlgVJAK75l0= +github.com/grafana/pyroscope-go v1.4.2/go.mod h1:Ej13Jr05rRJrjWvrrFhfh6gGYXtfibuukOs3Tl3Y7QQ= +github.com/grafana/pyroscope-go/godeltaprof v0.1.11 h1:el5LYpXissAiCKZ5/6yjlr6mhYVV6Cp5lahTocxraXM= +github.com/grafana/pyroscope-go/godeltaprof v0.1.11/go.mod h1:jl1V8M4cWsXciROCPIDDG7CtjSjT/ECbp6eLVuMxYRI= github.com/jftuga/geodist v1.0.0 h1:PFPQlZtj10u8ETAYTyxE0DWMl1bwA+Xzrqb4+oLkkC0= github.com/jftuga/geodist v1.0.0/go.mod h1:BohEDxpZ8S5ADAxW/9EKPSKWOVl0+3wHENIT40m4UO4= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= @@ -54,8 +59,13 @@ github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kataras/i18n v0.0.8 h1:thiDRqq4fN2sQOK5CwR5Yh1yT7swP6B3wnadILhqjhk= github.com/kataras/i18n v0.0.8/go.mod h1:M/yRAqQ3Y7z2oSpotxG/+nPrgsJLas6t0kEJfIJk98E= +github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= +github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -87,10 +97,12 @@ github.com/paulmach/protoscan v0.2.1 h1:rM0FpcTjUMvPUNk2BhPJrreDKetq43ChnL+x1sRg github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= @@ -104,6 +116,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tkrajina/gpxgo v1.4.0 h1:cSD5uSwy3VZuNFieTEZLyRnuIwhonQEkGPkPGW4XNag= github.com/tkrajina/gpxgo v1.4.0/go.mod h1:BXSMfUAvKiEhMEXAFM2NvNsbjsSvp394mOvdcNjettg= github.com/urfave/cli/v2 v2.24.2 h1:q1VA+ofZ8SWfEKB9xXHUD4QZaeI9e+ItEqSbfH2JBXk= @@ -199,6 +213,8 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -3,7 +3,9 @@ package main import ( "database/sql" "flag" + "log/slog" + "github.com/grafana/pyroscope-go" "github.com/martinlehoux/biking_home/cli" "github.com/martinlehoux/biking_home/config" "github.com/martinlehoux/kagamigo/kcore" @@ -18,8 +20,42 @@ func main() { flag.Parse() appConfig, err := config.Load(*configFile) kcore.Expect(err, "failed to load configuration") + stopProfiler, err := startPyroscope(appConfig.Pyroscope) + if err != nil { + kcore.Expect(err, "failed to start pyroscope") + } + if stopProfiler != nil { + defer stopProfiler() + } db, err := sql.Open("sqlite3", appConfig.Database.Path) kcore.Expect(err, "failed to open database") defer db.Close() cli.Run(db, *configFile, appConfig) } + +// startPyroscope starts the continuous profiler when enabled in the config and +// returns a stop function to flush and detach the profiler on shutdown, or nil +// when pyroscope is disabled. +func startPyroscope(appConfig config.PyroscopeConfig) (func(), error) { + if !appConfig.Enabled { + return nil, nil + } + profiler, err := pyroscope.Start(pyroscope.Config{ + ApplicationName: appConfig.ApplicationName, + ServerAddress: appConfig.ServerAddress, + Logger: pyroscope.StandardLogger, + ProfileTypes: []pyroscope.ProfileType{ + pyroscope.ProfileCPU, + pyroscope.ProfileAllocSpace, + pyroscope.ProfileAllocObjects, + pyroscope.ProfileInuseSpace, + pyroscope.ProfileInuseObjects, + pyroscope.ProfileGoroutines, + }, + }) + if err != nil { + return nil, err + } + slog.Info("pyroscope enabled", "server", appConfig.ServerAddress, "application", appConfig.ApplicationName) + return func() { _ = profiler.Stop() }, nil +} |
