diff --git a/go.mod b/go.mod
index 0f9297e..4c85806 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,8 @@ toolchain go1.22.5
require (
gfx.cafe/util/go v1.14.0
+ github.com/DataDog/gostackparse v0.7.0
+ github.com/a-h/templ v0.2.778
github.com/alecthomas/kong v1.2.1
github.com/go-chi/chi/v5 v5.1.0
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e
@@ -14,8 +16,6 @@ require (
)
require (
- github.com/chzyer/readline v1.5.1 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd // indirect
- golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.19.0 // indirect
)
diff --git a/go.sum b/go.sum
index 7601334..e9c1de1 100644
--- a/go.sum
+++ b/go.sum
@@ -1,59 +1,41 @@
-gfx.cafe/util/go v1.10.0 h1:2J2QK3lBm9lZcjlQqWCDaR+SNNSgiT/yXdRH8PenY6o=
-gfx.cafe/util/go v1.10.0/go.mod h1:xzge01gF6zKOM3VztFGWuFlmlFX00pPkonZ3YU7NRSU=
-gfx.cafe/util/go v1.13.0 h1:ZNBFsHVKx3Nspb6l1MTwsj/lnV1M+0AhvlHbjtfj6s0=
-gfx.cafe/util/go v1.13.0/go.mod h1:69P+MuE7Bwz69cVgCTMG4adySzirioh3X5IFpE6MNcY=
-gfx.cafe/util/go v1.13.1 h1:hEkAexZUlBQZSQG21HkS0l/eqeuh0rY9kjcOwjGlwKI=
-gfx.cafe/util/go v1.13.1/go.mod h1:69P+MuE7Bwz69cVgCTMG4adySzirioh3X5IFpE6MNcY=
gfx.cafe/util/go v1.14.0 h1:W15T4ZQSraASbl1eNZmF8pEsM435uAEIxTBzx691vRg=
gfx.cafe/util/go v1.14.0/go.mod h1:s6KPm1PUsYFL00XoMOI+Y2iorVPZ+9yBc1WWUjI0n9Y=
-github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
-github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4=
+github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
+github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
+github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
-github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
-github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
+github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q=
github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
-github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
-github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
-github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
-github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
-github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
-github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+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=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
-github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g=
-github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
-github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
-github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
-github.com/google/pprof v0.0.0-20240903155634-a8630aee4ab9 h1:q5g0N9eal4bmJwXHC5z0QCKs8qhS35hFfq0BAYsIwZI=
-github.com/google/pprof v0.0.0-20240903155634-a8630aee4ab9/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
-github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ=
-github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e h1:v7R0PZoC2p1KWQmv1+GqCXQe59Ab1TkDF8Y9Lg2W6m4=
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
-github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE=
-github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
-github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
-github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd h1:EVX1s+XNss9jkRW9K6XGJn2jL2lB1h5H804oKPsxOec=
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
-github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
-github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+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/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
-golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
-golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/goroutineinstance/instance.go b/pkg/goroutineinstance/instance.go
new file mode 100644
index 0000000..fcdb138
--- /dev/null
+++ b/pkg/goroutineinstance/instance.go
@@ -0,0 +1,49 @@
+package goroutineinstance
+
+import (
+ "encoding/json"
+ "flag"
+ "log"
+ "net/http"
+ "sync"
+
+ "github.com/DataDog/gostackparse"
+ "github.com/a-h/templ"
+ "github.com/go-chi/chi/v5"
+ "github.com/rs/xid"
+ "tuxpa.in/a/pprofweb/pkg/goroutineinstance/site"
+)
+
+type Instance struct {
+ id xid.ID
+ dat []*gostackparse.Goroutine
+
+ mu sync.RWMutex
+
+ once sync.Once
+ handler http.Handler
+}
+
+func (i *Instance) Id() xid.ID {
+ return i.id
+}
+func (i *Instance) ServeHTTP(tw http.ResponseWriter, tr *http.Request) {
+ i.once.Do(func() {
+ r := chi.NewRouter()
+ indexHandler := templ.Handler(site.Index())
+ r.Get("/", indexHandler.ServeHTTP)
+ r.Get("/raw", func(w http.ResponseWriter, r *http.Request) {
+ json.NewEncoder(w).Encode(i.dat)
+ })
+ i.handler = r
+ })
+ log.Printf("serve goroutine instance %s %s", i.id, tr.URL.String())
+ i.handler.ServeHTTP(tw, tr)
+}
+
+// Mostly copied from https://github.com/google/pprof/blob/master/internal/driver/flags.go
+type pprofFlags struct {
+ args []string
+ s flag.FlagSet
+ usage []string
+}
diff --git a/pkg/goroutineinstance/instancestore.go b/pkg/goroutineinstance/instancestore.go
new file mode 100644
index 0000000..aa00a7f
--- /dev/null
+++ b/pkg/goroutineinstance/instancestore.go
@@ -0,0 +1,84 @@
+package goroutineinstance
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "gfx.cafe/util/go/generic"
+ "github.com/DataDog/gostackparse"
+ "github.com/rs/xid"
+ "github.com/spf13/afero"
+)
+
+type InstanceStore interface {
+ NewInstance(xs []*gostackparse.Goroutine) (*Instance, error)
+ GetInstance(id xid.ID) (*Instance, error)
+}
+
+type fileBasedInstanceStore struct {
+ expiry time.Duration
+ fs afero.Fs
+
+ cache generic.Map[xid.ID, *Instance]
+}
+
+func NewFileBasedInstanceStore(fs afero.Fs, expiry time.Duration) *fileBasedInstanceStore {
+ return &fileBasedInstanceStore{
+ fs: fs,
+ expiry: expiry,
+ }
+}
+
+func (o *fileBasedInstanceStore) NewInstance(dat []*gostackparse.Goroutine) (*Instance, error) {
+ // save instance
+ id := xid.New()
+ i := &Instance{
+ id: id,
+ dat: dat,
+ }
+
+ jsonDat, err := json.Marshal(dat)
+ if err != nil {
+ return nil, err
+ }
+
+ file, err := o.fs.Create(id.String())
+ if err != nil {
+ return nil, err
+ }
+ _, err = file.Write(jsonDat)
+ if err != nil {
+ file.Close()
+ return nil, err
+ }
+ err = file.Close()
+ if err != nil {
+ return nil, err
+ }
+ o.cache.Store(id, i)
+ return i, nil
+}
+
+func (o *fileBasedInstanceStore) GetInstance(id xid.ID) (*Instance, error) {
+ if i, ok := o.cache.Load(id); ok {
+ return i, nil
+ }
+ if o.expiry > 0 {
+ if id.Time().After(time.Now().Add(o.expiry)) {
+ o.fs.Remove(id.String())
+ return nil, fmt.Errorf("goroutine expired")
+ }
+ }
+ bts, err := afero.ReadFile(o.fs, id.String())
+ if err != nil {
+ return nil, err
+ }
+ i := &Instance{id: id}
+ err = json.Unmarshal(bts, &i.dat)
+ if err != nil {
+ return nil, err
+ }
+ o.cache.Store(id, i)
+ return i, nil
+}
diff --git a/pkg/goroutineinstance/site/generate.go b/pkg/goroutineinstance/site/generate.go
new file mode 100644
index 0000000..f00dd96
--- /dev/null
+++ b/pkg/goroutineinstance/site/generate.go
@@ -0,0 +1,3 @@
+package site
+
+//go:generate go run github.com/a-h/templ/cmd/templ@latest generate
diff --git a/pkg/goroutineinstance/site/index.templ b/pkg/goroutineinstance/site/index.templ
new file mode 100644
index 0000000..6acc2f8
--- /dev/null
+++ b/pkg/goroutineinstance/site/index.templ
@@ -0,0 +1,11 @@
+package site
+
+
+
+templ Index() {
+ @layout() {
+
+ gmorning
+
+ }
+}
diff --git a/pkg/goroutineinstance/site/index_templ.go b/pkg/goroutineinstance/site/index_templ.go
new file mode 100644
index 0000000..e237b18
--- /dev/null
+++ b/pkg/goroutineinstance/site/index_templ.go
@@ -0,0 +1,58 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.2.778
+package site
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+func Index() templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("hi
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+ templ_7745c5c3_Err = layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/pkg/goroutineinstance/site/layout.templ b/pkg/goroutineinstance/site/layout.templ
new file mode 100644
index 0000000..e2ee7d5
--- /dev/null
+++ b/pkg/goroutineinstance/site/layout.templ
@@ -0,0 +1,16 @@
+package site
+
+
+templ layout(contents templ.Component) {
+
+
+
+ goroutine explorer
+
+
+
+ @contents
+
+
+
+}
diff --git a/pkg/goroutineinstance/site/layout_templ.go b/pkg/goroutineinstance/site/layout_templ.go
new file mode 100644
index 0000000..7638c9e
--- /dev/null
+++ b/pkg/goroutineinstance/site/layout_templ.go
@@ -0,0 +1,48 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.2.778
+package site
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+func layout(contents templ.Component) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("goroutine explorer")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = contents.Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/pkg/pprofweb/pprofweb.go b/pkg/pprofweb/pprofweb.go
index f34c35e..7af136b 100644
--- a/pkg/pprofweb/pprofweb.go
+++ b/pkg/pprofweb/pprofweb.go
@@ -1,24 +1,21 @@
package pprofweb
import (
+ "bytes"
"errors"
- "flag"
"fmt"
"io"
"log"
"net/http"
- "os"
- "path"
"strings"
- "sync"
"time"
- "gfx.cafe/util/go/generic"
+ "github.com/DataDog/gostackparse"
"github.com/go-chi/chi/v5"
- "github.com/google/pprof/driver"
"github.com/rs/xid"
"github.com/spf13/afero"
- "tuxpa.in/a/pprofweb/pkg/flagset"
+ "tuxpa.in/a/pprofweb/pkg/goroutineinstance"
+ "tuxpa.in/a/pprofweb/pkg/profileinstance"
)
type Config struct {
@@ -27,9 +24,8 @@ type Config struct {
}
type Server struct {
- fs afero.Fs
-
- instances generic.Map[xid.ID, *Instance]
+ profiles profileinstance.InstanceStore
+ goroutines goroutineinstance.InstanceStore
Config
}
@@ -37,8 +33,9 @@ type Server struct {
func NewServer(fs afero.Fs, c Config) *Server {
srv := afero.NewCacheOnReadFs(fs, afero.NewMemMapFs(), 24*time.Hour)
s := &Server{
- fs: srv,
- Config: c,
+ profiles: profileinstance.NewFileBasedInstanceStore(srv, c.Expire),
+ goroutines: goroutineinstance.NewFileBasedInstanceStore(srv, c.Expire),
+ Config: c,
}
if s.MaxUploadSize == 0 {
@@ -53,21 +50,29 @@ func (s *Server) HandleHTTP() func(chi.Router) {
return func(r chi.Router) {
r.HandleFunc("/", rootHandler)
r.HandleFunc("/upload", s.HandleUpload)
- r.HandleFunc("/{xid}", s.ServeInstance)
- r.HandleFunc("/{xid}/*", s.ServeInstance)
-
+ r.Route("/profile", func(r chi.Router) {
+ r.Route("/{xid}", func(r chi.Router) {
+ r.HandleFunc("/", s.ServeProfileInstance)
+ r.HandleFunc("/*", s.ServeProfileInstance)
+ })
+ })
+ r.Route("/goroutine", func(r chi.Router) {
+ r.Route("/{xid}", func(r chi.Router) {
+ r.HandleFunc("/", s.ServeGoroutineInstance)
+ r.HandleFunc("/*", s.ServeGoroutineInstance)
+ })
+ })
}
}
-
-func (s *Server) ServeInstance(w http.ResponseWriter, r *http.Request) {
- err := s.serveInstance(w, r)
+func (s *Server) ServeGoroutineInstance(w http.ResponseWriter, r *http.Request) {
+ err := s.serveGoroutineInstance(w, r)
if err != nil {
http.Error(w, "instance not found: "+err.Error(), http.StatusNotFound)
return
}
}
-func (s *Server) serveInstance(w http.ResponseWriter, r *http.Request) error {
+func (s *Server) serveGoroutineInstance(w http.ResponseWriter, r *http.Request) error {
log.Printf("serveInstance %s %s", r.Method, r.URL.String())
sid := chi.URLParam(r, "xid")
if sid == "" {
@@ -77,7 +82,7 @@ func (s *Server) serveInstance(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.New("invalid id")
}
- inst, err := s.GetInstance(id)
+ inst, err := s.goroutines.GetInstance(id)
if err != nil {
return errors.New("instance not found: " + err.Error())
}
@@ -85,56 +90,30 @@ func (s *Server) serveInstance(w http.ResponseWriter, r *http.Request) error {
return nil
}
-func (s *Server) NewInstance(xs []byte) (*Instance, error) {
- // save instance
- id := xid.New()
- i := &Instance{
- id: id,
- dat: xs,
- }
-
- file, err := s.fs.Create(id.String())
+func (s *Server) ServeProfileInstance(w http.ResponseWriter, r *http.Request) {
+ err := s.serveProfileInstance(w, r)
if err != nil {
- return nil, err
+ http.Error(w, "instance not found: "+err.Error(), http.StatusNotFound)
+ return
}
- _, err = file.Write(xs)
- if err != nil {
- file.Close()
- return nil, err
- }
- err = file.Close()
- if err != nil {
- return nil, err
- }
- err = i.startServer()
- if err != nil {
- return nil, err
- }
- s.instances.Store(id, i)
- return i, nil
}
-func (s *Server) GetInstance(id xid.ID) (*Instance, error) {
- if i, ok := s.instances.Load(id); ok {
- return i, nil
+func (s *Server) serveProfileInstance(w http.ResponseWriter, r *http.Request) error {
+ log.Printf("serveInstance %s %s", r.Method, r.URL.String())
+ sid := chi.URLParam(r, "xid")
+ if sid == "" {
+ return errors.New("no id sent")
}
- if s.Expire > 0 {
- if id.Time().After(time.Now().Add(s.Expire)) {
- s.fs.Remove(id.String())
- return nil, fmt.Errorf("profile expired")
- }
- }
- bts, err := afero.ReadFile(s.fs, id.String())
+ id, err := xid.FromString(sid)
if err != nil {
- return nil, err
+ return errors.New("invalid id")
}
- i := &Instance{id: id, dat: bts}
- err = i.startServer()
+ inst, err := s.profiles.GetInstance(id)
if err != nil {
- return nil, err
+ return errors.New("instance not found: " + err.Error())
}
- s.instances.Store(id, i)
- return i, nil
+ inst.ServeHTTP(w, r)
+ return nil
}
func (s *Server) HandleUpload(w http.ResponseWriter, r *http.Request) {
@@ -165,66 +144,40 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) error {
return err
}
- instance, err := s.NewInstance(bts)
- if err != nil {
- return err
+ // sanity check that allows us to be a little lazy.
+ if len(bts) < 128 {
+ return errors.New("profile too small")
+ }
+
+ var redirString string
+ // check if it is a goroutine dump
+ if bytes.HasPrefix(bts, []byte("goroutine")) {
+ // goroutine dump. we should immediately try to parse it
+ routines, errs := gostackparse.Parse(bytes.NewReader(bts))
+ if errs != nil {
+ return fmt.Errorf("error parsing goroutine dump: %w", err)
+ }
+ if len(routines) == 0 {
+ return errors.New("no goroutines found")
+ }
+ instance, err := s.goroutines.NewInstance(routines)
+ if err != nil {
+ return err
+ }
+ redirString = "goroutine/" + instance.Id().String()
+ } else {
+ instance, err := s.profiles.NewInstance(bts)
+ if err != nil {
+ return err
+ }
+ redirString = "profile/" + instance.Id().String()
}
for _, h := range r.Header.Values("Accept") {
if strings.Contains(h, "text/html") {
- http.Redirect(w, r, instance.id.String()+"/", http.StatusSeeOther)
+ http.Redirect(w, r, redirString, http.StatusSeeOther)
return nil
}
}
- w.Write([]byte(instance.id.String()))
+ w.Write([]byte(redirString))
return nil
}
-
-type Instance struct {
- id xid.ID
- dat []byte
-
- handler http.Handler
-
- mu sync.RWMutex
-}
-
-func (i *Instance) startServer() error {
- file, err := os.CreateTemp("", i.id.String())
- if err != nil {
- return err
- }
- file.Write(i.dat)
- flags := &flagset.Set{
- Argz: []string{"-http=localhost:0", "-no_browser", file.Name()},
- }
- options := &driver.Options{
- Flagset: flags,
- HTTPServer: i.startHTTP,
- }
- if err := driver.PProf(options); err != nil {
- return err
- }
- return nil
-}
-func (i *Instance) startHTTP(args *driver.HTTPServerArgs) error {
- r := chi.NewRouter()
- for pattern, handler := range args.Handlers {
- jpat := "/" + path.Join(i.id.String(), pattern)
- r.Handle(jpat, handler)
- r.Handle(jpat+"/", handler)
- }
- i.handler = r
- return nil
-}
-
-func (i *Instance) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- log.Printf("serveInstanceHTTP %s %s", i.id, r.URL.String())
- i.handler.ServeHTTP(w, r)
-}
-
-// Mostly copied from https://github.com/google/pprof/blob/master/internal/driver/flags.go
-type pprofFlags struct {
- args []string
- s flag.FlagSet
- usage []string
-}
diff --git a/pkg/profileinstance/instance.go b/pkg/profileinstance/instance.go
new file mode 100644
index 0000000..9695033
--- /dev/null
+++ b/pkg/profileinstance/instance.go
@@ -0,0 +1,69 @@
+package profileinstance
+
+import (
+ "flag"
+ "log"
+ "net/http"
+ "os"
+ "path"
+ "sync"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/google/pprof/driver"
+ "github.com/rs/xid"
+ "tuxpa.in/a/pprofweb/pkg/flagset"
+)
+
+type Instance struct {
+ id xid.ID
+ dat []byte
+
+ handler http.Handler
+
+ mu sync.RWMutex
+}
+
+func (i *Instance) Id() xid.ID {
+ return i.id
+}
+
+func (i *Instance) startServer() error {
+ file, err := os.CreateTemp("", i.id.String())
+ if err != nil {
+ return err
+ }
+ file.Write(i.dat)
+ flags := &flagset.Set{
+ Argz: []string{"-http=localhost.invalid:0", "-no_browser", file.Name()},
+ }
+ options := &driver.Options{
+ Flagset: flags,
+ HTTPServer: i.startHTTP,
+ }
+ if err := driver.PProf(options); err != nil {
+ return err
+ }
+ return nil
+}
+func (i *Instance) startHTTP(args *driver.HTTPServerArgs) error {
+ r := chi.NewRouter()
+ for pattern, handler := range args.Handlers {
+ jpat := path.Join(pattern)
+ r.Handle(jpat, handler)
+ r.Handle(jpat+"/", handler)
+ }
+ i.handler = r
+ return nil
+}
+
+func (i *Instance) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ log.Printf("serveInstanceHTTP %s %s", i.id, r.URL.String())
+ i.handler.ServeHTTP(w, r)
+}
+
+// Mostly copied from https://github.com/google/pprof/blob/master/internal/driver/flags.go
+type pprofFlags struct {
+ args []string
+ s flag.FlagSet
+ usage []string
+}
diff --git a/pkg/profileinstance/instancestore.go b/pkg/profileinstance/instancestore.go
new file mode 100644
index 0000000..5e9a133
--- /dev/null
+++ b/pkg/profileinstance/instancestore.go
@@ -0,0 +1,81 @@
+package profileinstance
+
+import (
+ "fmt"
+ "time"
+
+ "gfx.cafe/util/go/generic"
+ "github.com/rs/xid"
+ "github.com/spf13/afero"
+)
+
+type InstanceStore interface {
+ NewInstance(xs []byte) (*Instance, error)
+ GetInstance(id xid.ID) (*Instance, error)
+}
+
+type fileBasedInstanceStore struct {
+ expiry time.Duration
+ fs afero.Fs
+
+ cache generic.Map[xid.ID, *Instance]
+}
+
+func NewFileBasedInstanceStore(fs afero.Fs, expiry time.Duration) *fileBasedInstanceStore {
+ return &fileBasedInstanceStore{
+ fs: fs,
+ expiry: expiry,
+ }
+}
+
+func (o *fileBasedInstanceStore) NewInstance(xs []byte) (*Instance, error) {
+ // save instance
+ id := xid.New()
+ i := &Instance{
+ id: id,
+ dat: xs,
+ }
+
+ file, err := o.fs.Create(id.String())
+ if err != nil {
+ return nil, err
+ }
+ _, err = file.Write(xs)
+ if err != nil {
+ file.Close()
+ return nil, err
+ }
+ err = file.Close()
+ if err != nil {
+ return nil, err
+ }
+ err = i.startServer()
+ if err != nil {
+ return nil, err
+ }
+ o.cache.Store(id, i)
+ return i, nil
+}
+
+func (o *fileBasedInstanceStore) GetInstance(id xid.ID) (*Instance, error) {
+ if i, ok := o.cache.Load(id); ok {
+ return i, nil
+ }
+ if o.expiry > 0 {
+ if id.Time().After(time.Now().Add(o.expiry)) {
+ o.fs.Remove(id.String())
+ return nil, fmt.Errorf("profile expired")
+ }
+ }
+ bts, err := afero.ReadFile(o.fs, id.String())
+ if err != nil {
+ return nil, err
+ }
+ i := &Instance{id: id, dat: bts}
+ err = i.startServer()
+ if err != nil {
+ return nil, err
+ }
+ o.cache.Store(id, i)
+ return i, nil
+}