Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
ae0b68febb | |||
3bd1fec3e5 | |||
42cbdd3feb | |||
6680bbc3fa | |||
2e3929c4b3 | |||
14f292a814 | |||
0387418b27 | |||
edd8f091ff | |||
f39a1c1f02 | |||
1fcfcf7c5f | |||
addcc20c21 | |||
c22bdf9c97 | |||
a05f054ea1 | |||
6dd281ea18 | |||
e9f27bb967 | |||
87e838968b | |||
3575d7b719 | |||
27e2e2d363 | |||
7144acb3c8 | |||
ce3546e1e4 | |||
623cde9ee3 | |||
e0fa8029f9 | |||
bcafc6e408 | |||
4b4a3a80f8 | |||
096015015f | |||
144542074b |
40
.gitea/workflows/commit_tag.yml
Normal file
40
.gitea/workflows/commit_tag.yml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
name: commit-tag
|
||||||
|
on:
|
||||||
|
push
|
||||||
|
jobs:
|
||||||
|
commit-tag-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
env:
|
||||||
|
DOCKER_LATEST: nightly
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: https://tuxpa.in
|
||||||
|
username: actions_token
|
||||||
|
password: ${{ secrets.ACTIONS_TOKEN }}
|
||||||
|
- name: Get Meta
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
|
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
tuxpa.in/a/pprofweb:${{ steps.meta.outputs.REPO_VERSION }}
|
||||||
|
|
42
.gitea/workflows/release_tag.yml
Normal file
42
.gitea/workflows/release_tag.yml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
name: release-tag
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
jobs:
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
env:
|
||||||
|
DOCKER_LATEST: nightly
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: https://tuxpa.in
|
||||||
|
username: actions_token
|
||||||
|
password: ${{ secrets.ACTIONS_TOKEN }}
|
||||||
|
- name: Get Meta
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
|
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
tuxpa.in/a/pprofweb:${{ gitea.ref_name }}
|
||||||
|
|
16
Dockerfile
16
Dockerfile
@ -1,19 +1,13 @@
|
|||||||
# Extract graphviz and dependencies
|
# Extract graphviz and dependencies
|
||||||
FROM golang:1.19.1-bullseye AS deb_extractor
|
FROM golang:1.24-bookworm AS builder
|
||||||
RUN cd /tmp && \
|
|
||||||
apt-get update && apt-get download \
|
|
||||||
graphviz libgvc6 libcgraph6 libltdl7 libxdot4 libcdt5 libpathplan4 libexpat1 zlib1g && \
|
|
||||||
mkdir /dpkg && \
|
|
||||||
for deb in *.deb; do dpkg --extract $deb /dpkg || exit 10; done
|
|
||||||
|
|
||||||
FROM golang:1.19.1-bullseye AS builder
|
|
||||||
COPY . /go/src/pprofweb/
|
COPY . /go/src/pprofweb/
|
||||||
WORKDIR /go/src/pprofweb
|
WORKDIR /go/src/pprofweb
|
||||||
RUN go build -o server ./webserver
|
RUN go build -o server ./pkg/webserver
|
||||||
|
|
||||||
|
FROM debian:12 AS run
|
||||||
|
RUN apt-get update && apt-get install -y graphviz libgvc6 libcgraph6 libltdl7 libxdot4 libcdt5 libpathplan4 libexpat1 zlib1g
|
||||||
|
|
||||||
FROM gcr.io/distroless/base-debian11:latest AS run
|
|
||||||
COPY --from=builder /go/src/pprofweb/server /pprofweb
|
COPY --from=builder /go/src/pprofweb/server /pprofweb
|
||||||
COPY --from=deb_extractor /dpkg /
|
|
||||||
# Configure dot plugins
|
# Configure dot plugins
|
||||||
RUN ["dot", "-c"]
|
RUN ["dot", "-c"]
|
||||||
|
|
||||||
|
@ -49,6 +49,10 @@ func (c *Profile) Run(ctx Context) error {
|
|||||||
if c.Time > 0 {
|
if c.Time > 0 {
|
||||||
q.Add("seconds", strconv.Itoa(c.Time))
|
q.Add("seconds", strconv.Itoa(c.Time))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Mode == "goroutine" {
|
||||||
|
q.Add("debug", "2")
|
||||||
|
}
|
||||||
req.URL.RawQuery = q.Encode()
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
fmt.Printf("getting %s profile from %s...\n", c.Mode, req.URL)
|
fmt.Printf("getting %s profile from %s...\n", c.Mode, req.URL)
|
||||||
|
21
go.mod
21
go.mod
@ -1,19 +1,20 @@
|
|||||||
module tuxpa.in/a/pprofweb
|
module tuxpa.in/a/pprofweb
|
||||||
|
|
||||||
go 1.21.4
|
go 1.22.0
|
||||||
|
|
||||||
toolchain go1.22.0
|
toolchain go1.22.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gfx.cafe/util/go v1.10.0
|
gfx.cafe/util/go v1.14.0
|
||||||
github.com/alecthomas/kong v0.7.1
|
github.com/DataDog/gostackparse v0.7.0
|
||||||
github.com/go-chi/chi/v5 v5.0.12
|
github.com/alecthomas/kong v1.8.1
|
||||||
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
github.com/rs/xid v1.5.0
|
github.com/google/pprof v0.0.0-20250208200701-d0013a598941
|
||||||
github.com/spf13/afero v1.11.0
|
github.com/rs/xid v1.6.0
|
||||||
|
github.com/spf13/afero v1.12.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 // indirect
|
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
)
|
)
|
||||||
|
55
go.sum
55
go.sum
@ -1,22 +1,37 @@
|
|||||||
gfx.cafe/util/go v1.10.0 h1:2J2QK3lBm9lZcjlQqWCDaR+SNNSgiT/yXdRH8PenY6o=
|
gfx.cafe/util/go v1.14.0 h1:W15T4ZQSraASbl1eNZmF8pEsM435uAEIxTBzx691vRg=
|
||||||
gfx.cafe/util/go v1.10.0/go.mod h1:xzge01gF6zKOM3VztFGWuFlmlFX00pPkonZ3YU7NRSU=
|
gfx.cafe/util/go v1.14.0/go.mod h1:s6KPm1PUsYFL00XoMOI+Y2iorVPZ+9yBc1WWUjI0n9Y=
|
||||||
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
|
github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4=
|
||||||
github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
|
github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
|
||||||
github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
|
github.com/alecthomas/kong v1.8.1 h1:6aamvWBE/REnR/BCq10EcozmcpUPc5aGI1lPAWdB0EE=
|
||||||
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
github.com/alecthomas/kong v1.8.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
|
||||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
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.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
|
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc=
|
||||||
|
github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
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/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-20240912202439-0a2b6291aafd h1:EVX1s+XNss9jkRW9K6XGJn2jL2lB1h5H804oKPsxOec=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||||
|
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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
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=
|
||||||
|
2
main.go
2
main.go
@ -7,8 +7,8 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"tuxpa.in/a/pprofweb/cli"
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
|
"tuxpa.in/a/pprofweb/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
14
pkg/goroutineinstance/bundle.go
Normal file
14
pkg/goroutineinstance/bundle.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package goroutineinstance
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed site/index.html
|
||||||
|
var IndexHtmlEmbed []byte
|
||||||
|
|
||||||
|
//go:embed site/bundle.js
|
||||||
|
var BundleJsEmbed []byte
|
||||||
|
|
||||||
|
//go:embed site/bundle.js.map
|
||||||
|
var BundleJsMapEmbed []byte
|
55
pkg/goroutineinstance/instance.go
Normal file
55
pkg/goroutineinstance/instance.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package goroutineinstance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/DataDog/gostackparse"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
r.Use(middleware.StripSlashes)
|
||||||
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, path.Join(r.URL.Path, "app"), http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
r.Get("/app", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(IndexHtmlEmbed)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
84
pkg/goroutineinstance/instancestore.go
Normal file
84
pkg/goroutineinstance/instancestore.go
Normal file
@ -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
|
||||||
|
}
|
1
pkg/goroutineinstance/site/.gitignore
vendored
Normal file
1
pkg/goroutineinstance/site/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
2
pkg/goroutineinstance/site/bundle.js
Normal file
2
pkg/goroutineinstance/site/bundle.js
Normal file
File diff suppressed because one or more lines are too long
7
pkg/goroutineinstance/site/bundle.js.map
Normal file
7
pkg/goroutineinstance/site/bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
11
pkg/goroutineinstance/site/esbuild.config.js
Normal file
11
pkg/goroutineinstance/site/esbuild.config.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { build } from 'esbuild'
|
||||||
|
|
||||||
|
build({
|
||||||
|
entryPoints: ['src/index.tsx'],
|
||||||
|
bundle: true,
|
||||||
|
minify: true,
|
||||||
|
sourcemap: true,
|
||||||
|
target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
|
||||||
|
outfile: 'bundle.js',
|
||||||
|
plugins: [],
|
||||||
|
})
|
4
pkg/goroutineinstance/site/generate.go
Normal file
4
pkg/goroutineinstance/site/generate.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package site
|
||||||
|
|
||||||
|
//go:generate npm i
|
||||||
|
//go:generate npm run build
|
12
pkg/goroutineinstance/site/index.html
Normal file
12
pkg/goroutineinstance/site/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>goroutine explorer</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script defer src="/static/goroutine/bundle.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root">
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
477
pkg/goroutineinstance/site/package-lock.json
generated
Normal file
477
pkg/goroutineinstance/site/package-lock.json
generated
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
{
|
||||||
|
"name": "site",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "site",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@preact/signals": "^1.3.0",
|
||||||
|
"esbuild": "^0.24.0",
|
||||||
|
"preact": "^10.24.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@preact/signals": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-EOMeg42SlLS72dhoq6Vjq08havnLseWmPQ8A0YsgIAqMgWgx7V1a39+Pxo6i7SY5NwJtH4849JogFq3M67AzWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@preact/signals-core": "^1.7.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact": "10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@preact/signals-core": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.24.0",
|
||||||
|
"@esbuild/android-arm": "0.24.0",
|
||||||
|
"@esbuild/android-arm64": "0.24.0",
|
||||||
|
"@esbuild/android-x64": "0.24.0",
|
||||||
|
"@esbuild/darwin-arm64": "0.24.0",
|
||||||
|
"@esbuild/darwin-x64": "0.24.0",
|
||||||
|
"@esbuild/freebsd-arm64": "0.24.0",
|
||||||
|
"@esbuild/freebsd-x64": "0.24.0",
|
||||||
|
"@esbuild/linux-arm": "0.24.0",
|
||||||
|
"@esbuild/linux-arm64": "0.24.0",
|
||||||
|
"@esbuild/linux-ia32": "0.24.0",
|
||||||
|
"@esbuild/linux-loong64": "0.24.0",
|
||||||
|
"@esbuild/linux-mips64el": "0.24.0",
|
||||||
|
"@esbuild/linux-ppc64": "0.24.0",
|
||||||
|
"@esbuild/linux-riscv64": "0.24.0",
|
||||||
|
"@esbuild/linux-s390x": "0.24.0",
|
||||||
|
"@esbuild/linux-x64": "0.24.0",
|
||||||
|
"@esbuild/netbsd-x64": "0.24.0",
|
||||||
|
"@esbuild/openbsd-arm64": "0.24.0",
|
||||||
|
"@esbuild/openbsd-x64": "0.24.0",
|
||||||
|
"@esbuild/sunos-x64": "0.24.0",
|
||||||
|
"@esbuild/win32-arm64": "0.24.0",
|
||||||
|
"@esbuild/win32-ia32": "0.24.0",
|
||||||
|
"@esbuild/win32-x64": "0.24.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.24.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
||||||
|
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
pkg/goroutineinstance/site/package.json
Normal file
18
pkg/goroutineinstance/site/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "site",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "node esbuild.config.js"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@preact/signals": "^1.3.0",
|
||||||
|
"esbuild": "^0.24.0",
|
||||||
|
"preact": "^10.24.3"
|
||||||
|
}
|
||||||
|
}
|
101
pkg/goroutineinstance/site/src/index.tsx
Normal file
101
pkg/goroutineinstance/site/src/index.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import {render} from "preact";
|
||||||
|
import {useEffect, useState} from "preact/hooks";
|
||||||
|
import { Frame, Goroutine } from "./lib/goroutines";
|
||||||
|
import { useSignal, useComputed } from "@preact/signals";
|
||||||
|
|
||||||
|
const FrameView = (props: { frame: Frame }) => {
|
||||||
|
const {frame} = props
|
||||||
|
return <>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<div>
|
||||||
|
{frame.Func}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{frame.File}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{frame.Line}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function Main() {
|
||||||
|
//const [goroutines, setGoroutines] = useState<Goroutine[] | undefined>(undefined)
|
||||||
|
const goroutines = useSignal<Goroutine[]>([])
|
||||||
|
|
||||||
|
|
||||||
|
const fetchGoroutines = async () => {
|
||||||
|
const res = await fetch("./raw")
|
||||||
|
const json = await res.json()
|
||||||
|
let routines: Goroutine[] = json
|
||||||
|
routines = routines.map(x=>{
|
||||||
|
// divide by 1000000
|
||||||
|
x.Wait = Number(BigInt(x.Wait) / BigInt(1000000))
|
||||||
|
return x
|
||||||
|
})
|
||||||
|
goroutines.value = routines
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchGoroutines()
|
||||||
|
},[])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!goroutines) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(goroutines)
|
||||||
|
},[goroutines])
|
||||||
|
if(!goroutines) {
|
||||||
|
return <>
|
||||||
|
<div>
|
||||||
|
fetching
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
return <>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<div>
|
||||||
|
length:
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{goroutines.value.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{goroutines.value.map((x, idx)=>{
|
||||||
|
|
||||||
|
|
||||||
|
return <div id={`id-${x.ID}`} class="flex flex-row gap-2">
|
||||||
|
<div>
|
||||||
|
{x.ID}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{x.Wait}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{x.Stack}
|
||||||
|
</div>
|
||||||
|
{x.CreatedBy &&
|
||||||
|
<div>
|
||||||
|
<FrameView frame={x.CreatedBy}/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const rootElement = document.getElementById("root")
|
||||||
|
if(rootElement) {
|
||||||
|
render(<Main/>, rootElement);
|
||||||
|
}else {
|
||||||
|
console.error("no root element found")
|
||||||
|
}
|
37
pkg/goroutineinstance/site/src/lib/goroutines.ts
Normal file
37
pkg/goroutineinstance/site/src/lib/goroutines.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export interface Frame {
|
||||||
|
// Func is the name of the function, including package name, e.g. "main.main"
|
||||||
|
// or "net/http.(*Server).Serve".
|
||||||
|
Func: string;
|
||||||
|
// File is the absolute path of source file e.g.
|
||||||
|
// "/go/src/example.org/example/main.go".
|
||||||
|
File: string;
|
||||||
|
// Line is the line number of inside of the source file that was active when
|
||||||
|
// the sample was taken.
|
||||||
|
Line: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Goroutine {
|
||||||
|
// ID is the goroutine id (aka `goid`).
|
||||||
|
ID: number;
|
||||||
|
// State is the `atomicstatus` of the goroutine, or if "waiting" the
|
||||||
|
// `waitreason`.
|
||||||
|
State: string;
|
||||||
|
// Wait is the approximate duration a goroutine has been waiting or in a
|
||||||
|
// syscall as determined by the first gc after the wait started. Aka
|
||||||
|
// `waitsince`.
|
||||||
|
Wait: number;
|
||||||
|
// LockedToThread is true if the goroutine is locked by a thread, aka
|
||||||
|
// `lockedm`.
|
||||||
|
LockedToThread: boolean;
|
||||||
|
// Stack is the stack trace of the goroutine.
|
||||||
|
Stack: Frame[];
|
||||||
|
// FramesElided is true if the stack trace contains a message indicating that
|
||||||
|
// additional frames were elided. This happens when the stack depth exceeds
|
||||||
|
// 100.
|
||||||
|
FramesElided: boolean;
|
||||||
|
// CreatedBy is the frame that created this goroutine, nil for main().
|
||||||
|
CreatedBy: Frame | null;
|
||||||
|
// Ancestors are the Goroutines that created this goroutine.
|
||||||
|
// See GODEBUG=tracebackancestors=n in https://pkg.go.dev/runtime.
|
||||||
|
Ancestor?: Goroutine; // The `json:"Ancestor,omitempty"` tag is represented by the optional `?` in TypeScript
|
||||||
|
}
|
16
pkg/goroutineinstance/site/tsconfig.json
Normal file
16
pkg/goroutineinstance/site/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact",
|
||||||
|
"lib": [
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"transform": "typia/lib/transform"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
193
pkg/pprofweb/pprofweb.go
Normal file
193
pkg/pprofweb/pprofweb.go
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
package pprofweb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/DataDog/gostackparse"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"tuxpa.in/a/pprofweb/pkg/goroutineinstance"
|
||||||
|
"tuxpa.in/a/pprofweb/pkg/profileinstance"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Expire time.Duration
|
||||||
|
MaxUploadSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
profiles profileinstance.InstanceStore
|
||||||
|
goroutines goroutineinstance.InstanceStore
|
||||||
|
|
||||||
|
Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(fs afero.Fs, c Config) *Server {
|
||||||
|
srv := afero.NewCacheOnReadFs(fs, afero.NewMemMapFs(), 24*time.Hour)
|
||||||
|
s := &Server{
|
||||||
|
profiles: profileinstance.NewFileBasedInstanceStore(srv, c.Expire),
|
||||||
|
goroutines: goroutineinstance.NewFileBasedInstanceStore(srv, c.Expire),
|
||||||
|
Config: c,
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.MaxUploadSize == 0 {
|
||||||
|
s.MaxUploadSize = 32 << 20
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// handler returns a handler that servers the pprof web UI.
|
||||||
|
func (s *Server) HandleHTTP() func(chi.Router) {
|
||||||
|
return func(r chi.Router) {
|
||||||
|
r.HandleFunc("/", rootHandler)
|
||||||
|
r.HandleFunc("/upload", s.HandleUpload)
|
||||||
|
r.Route("/profile", func(r chi.Router) {
|
||||||
|
r.Route("/{xid}", func(r chi.Router) {
|
||||||
|
r.HandleFunc("/", s.ServeProfileInstance)
|
||||||
|
r.HandleFunc("/*", s.ServeProfileInstance)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
r.Route("/static/goroutine", func(r chi.Router) {
|
||||||
|
r.Get("/bundle.js", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
|
w.Write(goroutineinstance.BundleJsEmbed)
|
||||||
|
})
|
||||||
|
r.Get("/bundle.js.map", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(goroutineinstance.BundleJsMapEmbed)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
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) 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) 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 == "" {
|
||||||
|
return errors.New("no id sent")
|
||||||
|
}
|
||||||
|
id, err := xid.FromString(sid)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid id")
|
||||||
|
}
|
||||||
|
inst, err := s.goroutines.GetInstance(id)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("instance not found: " + err.Error())
|
||||||
|
}
|
||||||
|
inst.ServeHTTP(w, r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ServeProfileInstance(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := s.serveProfileInstance(w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "instance not found: "+err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
id, err := xid.FromString(sid)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid id")
|
||||||
|
}
|
||||||
|
inst, err := s.profiles.GetInstance(id)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("instance not found: " + err.Error())
|
||||||
|
}
|
||||||
|
inst.ServeHTTP(w, r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) HandleUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("uploadHandler %s %s", r.Method, r.URL.String())
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "wrong method", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := s.handleUpload(w, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("upload error: %s", err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
if err := r.ParseMultipartForm(int64(s.MaxUploadSize)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
uploadedFile, _, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer uploadedFile.Close()
|
||||||
|
bts, err := io.ReadAll(uploadedFile)
|
||||||
|
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, redirString, http.StatusSeeOther)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Write([]byte(redirString))
|
||||||
|
return nil
|
||||||
|
}
|
69
pkg/profileinstance/instance.go
Normal file
69
pkg/profileinstance/instance.go
Normal file
@ -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
|
||||||
|
}
|
81
pkg/profileinstance/instancestore.go
Normal file
81
pkg/profileinstance/instancestore.go
Normal file
@ -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
|
||||||
|
}
|
@ -5,10 +5,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"tuxpa.in/a/pprofweb/pprofweb"
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
"tuxpa.in/a/pprofweb/pkg/pprofweb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
@ -1,230 +0,0 @@
|
|||||||
package pprofweb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gfx.cafe/util/go/generic"
|
|
||||||
"tuxpa.in/a/pprofweb/flagset"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/google/pprof/driver"
|
|
||||||
"github.com/rs/xid"
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Expire time.Duration
|
|
||||||
MaxUploadSize int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
fs afero.Fs
|
|
||||||
|
|
||||||
instances generic.Map[xid.ID, *Instance]
|
|
||||||
|
|
||||||
Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServer(fs afero.Fs, c Config) *Server {
|
|
||||||
srv := afero.NewCacheOnReadFs(fs, afero.NewMemMapFs(), 24*time.Hour)
|
|
||||||
s := &Server{
|
|
||||||
fs: srv,
|
|
||||||
Config: c,
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.MaxUploadSize == 0 {
|
|
||||||
s.MaxUploadSize = 32 << 20
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// handler returns a handler that servers the pprof web UI.
|
|
||||||
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)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) ServeInstance(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := s.serveInstance(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 {
|
|
||||||
log.Printf("serveInstance %s %s", r.Method, r.URL.String())
|
|
||||||
sid := chi.URLParam(r, "xid")
|
|
||||||
if sid == "" {
|
|
||||||
return errors.New("no id sent")
|
|
||||||
}
|
|
||||||
id, err := xid.FromString(sid)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid id")
|
|
||||||
}
|
|
||||||
inst, err := s.GetInstance(id)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("instance not found: " + err.Error())
|
|
||||||
}
|
|
||||||
inst.ServeHTTP(w, r)
|
|
||||||
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())
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
i := &Instance{id: id, dat: bts}
|
|
||||||
err = i.startServer()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.instances.Store(id, i)
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) HandleUpload(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("uploadHandler %s %s", r.Method, r.URL.String())
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "wrong method", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := s.handleUpload(w, r)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("upload error: %s", err.Error())
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
if err := r.ParseMultipartForm(int64(s.MaxUploadSize)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
uploadedFile, _, err := r.FormFile("file")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer uploadedFile.Close()
|
|
||||||
bts, err := io.ReadAll(uploadedFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
instance, err := s.NewInstance(bts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, h := range r.Header.Values("Accept") {
|
|
||||||
if strings.Contains(h, "text/html") {
|
|
||||||
http.Redirect(w, r, instance.id.String()+"/", http.StatusSeeOther)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.Write([]byte(instance.id.String()))
|
|
||||||
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
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user