Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
942b1a6
internal/plot: use type for tags
arl Nov 6, 2025
c197ebf
_example: slight change in Work
arl Nov 6, 2025
3bcbd84
Add new /sched/goroutines metrics to goroutines plot
arl Nov 6, 2025
c40bdb2
Add new threads plot
arl Nov 6, 2025
472c6ea
Use cgo time series in test
arl Nov 6, 2025
45019f2
internal/plot: move each plot in its own file
arl Nov 7, 2025
9afda42
internal/plot: each plot registers itself
arl Nov 7, 2025
1ab9874
internal/plot: use rgb color comments for heatmap shades
arl Nov 7, 2025
396edad
internal/plot: fixes and cosmetics
arl Nov 7, 2025
816b963
internal/plot: refactor/simplify plot description
arl Nov 7, 2025
e36a2fb
internal/plot: add prego1.26 indices file
arl Nov 8, 2025
b18706e
internal/plot: generate indices
arl Nov 8, 2025
9a50ebb
CONTRIBUTING.md: fix command 'npm run dev'
arl Nov 8, 2025
271454f
internal/plot: convert more plots
arl Nov 8, 2025
1f89dea
internal/plot: remove sched-events plot
arl Nov 8, 2025
ffcb10b
internal/plot: all plots converted
arl Nov 8, 2025
2613176
internal/plot: use sync.Once for initIndices() to ensure init() order
arl Nov 9, 2025
0b58ab5
internal/plots: remove godebug indices
arl Nov 10, 2025
da97f0d
ci: on linux, test on 3 latest Go versions
arl Nov 10, 2025
1ade7e7
ci: run less often
arl Nov 10, 2025
b7c2b2c
_example: remove go.sum
arl Nov 10, 2025
8918f83
_example: add go.sum
arl Nov 11, 2025
b508341
internal/plot: simplification
arl Nov 11, 2025
80ddb15
internal/plot: simplify description
arl Nov 11, 2025
f57d76f
Cosmetics
arl Nov 11, 2025
b92396d
internal/plot: end refactor registry and List
arl Nov 15, 2025
3ae2a2c
internal/plot: rewrite TestUnusedRuntimeMetrics
arl Nov 15, 2025
3059a88
internal/plot: determine order
arl Nov 15, 2025
b624a52
internal/plot: move stuff in helpers.go
arl Nov 15, 2025
839752d
internal/plot: build histogram layout at "register time"
arl Nov 15, 2025
98d9b15
ci: run linux tests on 1.24, 1.25 and tip
arl Nov 15, 2025
c69637b
Support multiple websocket clients. Add Server.Close()
arl Nov 15, 2025
9f1886c
Add TestWsConcurrent
arl Nov 15, 2025
c1cdfde
Fix test. Init client in Server.init()
arl Nov 15, 2025
e14d98d
fixup! Support multiple websocket clients. Add Server.Close()
arl Nov 15, 2025
d69e06b
Cosmetics
arl Nov 15, 2025
084efb4
Fix goroutine leak. Add synctest test
arl Nov 16, 2025
58fc86f
Cosmetics
arl Nov 16, 2025
5db2f54
Cosmetics
arl Nov 16, 2025
17b9bcf
Accept Root("/") option
arl Nov 16, 2025
8d2f780
internal/plot: deterministic order in TestUnused
arl Nov 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/assets.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Assets
on: [push, pull_request]
on: [pull_request]
jobs:
check-assets:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
on: [push]
on: [pull_request]
name: Coverage
jobs:
coverage:
Expand Down
8 changes: 3 additions & 5 deletions .github/workflows/tests-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ jobs:
tests-linux:
strategy:
matrix:
go-version: [1.22.x, 1.23.x]
go-version: [1.24.x, 1.25.x, tip]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
- name: go mod tidy check
run: go mod tidy -diff
- name: Tests
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The user interface aims to be simple, light and minimal.
To bootstrap the UI for development:
- cd to `internal/static`
- run `npm install`
- run `npm dev` and leave it running
- run `npm run dev` and leave it running
- in another terminal, cd to an example, for example `_example/default`
- run `go mod edit -replace=github.com/arl/statsviz=../../` to build the
example with your local version of the Go code. If you haven't touched to the
Expand Down
8 changes: 8 additions & 0 deletions _example/dev/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
4 changes: 3 additions & 1 deletion _example/work.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ func Work() {
for {
select {
case <-clearTick.C:
m = make(map[int64]any)
if rand.Intn(100) < 5 {
runtime.GC()
}
if rand.Intn(100) < 2 {
m = make(map[int64]any)
}
case ts := <-tick.C:
m[ts.UnixNano()] = newStruct()
}
Expand Down
99 changes: 99 additions & 0 deletions clients.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package statsviz

import (
"context"
"sync"

"github.com/gorilla/websocket"

"github.com/arl/statsviz/internal/plot"
)

type clients struct {
cfg *plot.Config
ctx context.Context

mu sync.RWMutex
m map[*websocket.Conn]chan []byte
}

func newClients(ctx context.Context, cfg *plot.Config) *clients {
return &clients{
m: make(map[*websocket.Conn]chan []byte),
cfg: cfg,
ctx: ctx,
}
}

type wsmsg struct {
Event string `json:"event"`
Data any `json:"data"`
}

func (c *clients) add(conn *websocket.Conn) {
dbglog("adding client")

// Send config first.
err := conn.WriteJSON(wsmsg{Event: "config", Data: c.cfg})
if err != nil {
dbglog("failed to send config: %v", err)
return
}

ch := make(chan []byte)

go func() {
defer func() {
c.mu.Lock()
delete(c.m, conn)
c.mu.Unlock()

dbglog("removed client")
}()

for {
select {
case <-c.ctx.Done():
return
case msg := <-ch:
if err := sendbuf(conn, msg); err != nil {
dbglog("failed to send data: %v", err)
return
}
}
}
}()

c.mu.Lock()
defer c.mu.Unlock()

c.m[conn] = ch
}

func sendbuf(conn *websocket.Conn, buf []byte) error {
w, err := conn.NextWriter(websocket.TextMessage)
if err != nil {
return err
}
_, err1 := w.Write(buf)
err2 := w.Close()
if err1 != nil {
return err1
}
return err2
}

func (c *clients) broadcast(buf []byte) {
c.mu.RLock()
defer c.mu.RUnlock()

for _, ch := range c.m {
select {
case ch <- buf:
default:
// if a client is not keeping up, we
// drop the message for that client.
dbglog("dropping message to client")
}
}
}
69 changes: 35 additions & 34 deletions internal/plot/color.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,45 +20,46 @@ func (c WeightedColor) MarshalJSON() ([]byte, error) {
return []byte(str), nil
}

// https://mdigi.tools/color-shades/
// NOTE: shades obtained from https://mdigi.tools/color-shades/

var BlueShades = []WeightedColor{
{Value: 0.0, Color: color.RGBA{0xea, 0xf8, 0xfd, 1}},
{Value: 0.1, Color: color.RGBA{0xbf, 0xeb, 0xfa, 1}},
{Value: 0.2, Color: color.RGBA{0x94, 0xdd, 0xf6, 1}},
{Value: 0.3, Color: color.RGBA{0x69, 0xd0, 0xf2, 1}},
{Value: 0.4, Color: color.RGBA{0x3f, 0xc2, 0xef, 1}},
{Value: 0.5, Color: color.RGBA{0x14, 0xb5, 0xeb, 1}},
{Value: 0.6, Color: color.RGBA{0x10, 0x94, 0xc0, 1}},
{Value: 0.7, Color: color.RGBA{0x0d, 0x73, 0x96, 1}},
{Value: 0.8, Color: color.RGBA{0x09, 0x52, 0x6b, 1}},
{Value: 0.9, Color: color.RGBA{0x05, 0x31, 0x40, 1}},
{Value: 1.0, Color: color.RGBA{0x02, 0x10, 0x15, 1}},
{Value: 0.0, Color: color.RGBA{0xea, 0xf8, 0xfd, 1}}, // rgb(234, 248, 253)
{Value: 0.1, Color: color.RGBA{0xbf, 0xeb, 0xfa, 1}}, // rgb(191, 235, 250)
{Value: 0.2, Color: color.RGBA{0x94, 0xdd, 0xf6, 1}}, // rgb(148, 221, 246)
{Value: 0.3, Color: color.RGBA{0x69, 0xd0, 0xf2, 1}}, // rgb(105, 208, 242)
{Value: 0.4, Color: color.RGBA{0x3f, 0xc2, 0xef, 1}}, // rgb(63, 194, 239)
{Value: 0.5, Color: color.RGBA{0x14, 0xb5, 0xeb, 1}}, // rgb(20, 181, 235)
{Value: 0.6, Color: color.RGBA{0x10, 0x94, 0xc0, 1}}, // rgb(16, 148, 192)
{Value: 0.7, Color: color.RGBA{0x0d, 0x73, 0x96, 1}}, // rgb(13, 115, 150)
{Value: 0.8, Color: color.RGBA{0x09, 0x52, 0x6b, 1}}, // rgb(9, 82, 107)
{Value: 0.9, Color: color.RGBA{0x05, 0x31, 0x40, 1}}, // rgb(5, 49, 64)
{Value: 1.0, Color: color.RGBA{0x02, 0x10, 0x15, 1}}, // rgb(2, 16, 21)
}

var PinkShades = []WeightedColor{
{Value: 0.0, Color: color.RGBA{0xfe, 0xe7, 0xf3, 1}},
{Value: 0.1, Color: color.RGBA{0xfc, 0xb6, 0xdc, 1}},
{Value: 0.2, Color: color.RGBA{0xf9, 0x85, 0xc5, 1}},
{Value: 0.3, Color: color.RGBA{0xf7, 0x55, 0xae, 1}},
{Value: 0.4, Color: color.RGBA{0xf5, 0x24, 0x96, 1}},
{Value: 0.5, Color: color.RGBA{0xdb, 0x0a, 0x7d, 1}},
{Value: 0.6, Color: color.RGBA{0xaa, 0x08, 0x61, 1}},
{Value: 0.7, Color: color.RGBA{0x7a, 0x06, 0x45, 1}},
{Value: 0.8, Color: color.RGBA{0x49, 0x03, 0x2a, 1}},
{Value: 0.9, Color: color.RGBA{0x18, 0x01, 0x0e, 1}},
{Value: 1.0, Color: color.RGBA{0x00, 0x00, 0x00, 1}},
{Value: 0.0, Color: color.RGBA{0xfe, 0xe7, 0xf3, 1}}, // rgb(254, 231, 243)
{Value: 0.1, Color: color.RGBA{0xfc, 0xb6, 0xdc, 1}}, // rgb(252, 182, 220)
{Value: 0.2, Color: color.RGBA{0xf9, 0x85, 0xc5, 1}}, // rgb(249, 133, 197)
{Value: 0.3, Color: color.RGBA{0xf7, 0x55, 0xae, 1}}, // rgb(247, 85, 174)
{Value: 0.4, Color: color.RGBA{0xf5, 0x24, 0x96, 1}}, // rgb(245, 36, 150)
{Value: 0.5, Color: color.RGBA{0xdb, 0x0a, 0x7d, 1}}, // rgb(219, 10, 125)
{Value: 0.6, Color: color.RGBA{0xaa, 0x08, 0x61, 1}}, // rgb(170, 8, 97)
{Value: 0.7, Color: color.RGBA{0x7a, 0x06, 0x45, 1}}, // rgb(122, 6, 69)
{Value: 0.8, Color: color.RGBA{0x49, 0x03, 0x2a, 1}}, // rgb(73, 3, 42)
{Value: 0.9, Color: color.RGBA{0x18, 0x01, 0x0e, 1}}, // rgb(24, 1, 14)
{Value: 1.0, Color: color.RGBA{0x00, 0x00, 0x00, 1}}, // rgb(0, 0, 0)
}

var GreenShades = []WeightedColor{
{Value: 0.0, Color: color.RGBA{0xed, 0xf7, 0xf2, 0}},
{Value: 0.1, Color: color.RGBA{0xc9, 0xe8, 0xd7, 0}},
{Value: 0.2, Color: color.RGBA{0xa5, 0xd9, 0xbc, 0}},
{Value: 0.3, Color: color.RGBA{0x81, 0xca, 0xa2, 0}},
{Value: 0.4, Color: color.RGBA{0x5e, 0xbb, 0x87, 0}},
{Value: 0.5, Color: color.RGBA{0x44, 0xa1, 0x6e, 0}},
{Value: 0.6, Color: color.RGBA{0x35, 0x7e, 0x55, 0}},
{Value: 0.7, Color: color.RGBA{0x26, 0x5a, 0x3d, 0}},
{Value: 0.8, Color: color.RGBA{0x17, 0x36, 0x25, 0}},
{Value: 0.9, Color: color.RGBA{0x08, 0x12, 0x0c, 0}},
{Value: 1.0, Color: color.RGBA{0x00, 0x00, 0x00, 0}},
{Value: 0.0, Color: color.RGBA{0xed, 0xf7, 0xf2, 0}}, // rgb(237, 247, 242)
{Value: 0.1, Color: color.RGBA{0xc9, 0xe8, 0xd7, 0}}, // rgb(201, 232, 215)
{Value: 0.2, Color: color.RGBA{0xa5, 0xd9, 0xbc, 0}}, // rgb(165, 217, 188)
{Value: 0.3, Color: color.RGBA{0x81, 0xca, 0xa2, 0}}, // rgb(129, 202, 162)
{Value: 0.4, Color: color.RGBA{0x5e, 0xbb, 0x87, 0}}, // rgb(94, 187, 135)
{Value: 0.5, Color: color.RGBA{0x44, 0xa1, 0x6e, 0}}, // rgb(68, 161, 110)
{Value: 0.6, Color: color.RGBA{0x35, 0x7e, 0x55, 0}}, // rgb(53, 126, 85)
{Value: 0.7, Color: color.RGBA{0x26, 0x5a, 0x3d, 0}}, // rgb(38, 90, 61)
{Value: 0.8, Color: color.RGBA{0x17, 0x36, 0x25, 0}}, // rgb(23, 54, 37)
{Value: 0.9, Color: color.RGBA{0x08, 0x12, 0x0c, 0}}, // rgb(8, 18, 12)
{Value: 1.0, Color: color.RGBA{0x00, 0x00, 0x00, 0}}, // rgb(0, 0, 0)
}
52 changes: 52 additions & 0 deletions internal/plot/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package plot

import (
"runtime/debug"
"time"
)

// delta returns a function that computes the delta between successive calls.
func delta[T uint64 | float64]() func(T) T {
first := true
var last T
return func(cur T) T {
delta := cur - last
if first {
delta = 0
first = false
}
last = cur
return delta
}
}

// rate returns a function that computes the rate of change per second.
func rate[T uint64 | float64]() func(time.Time, T) float64 {
var last T
var lastTime time.Time

return func(now time.Time, cur T) float64 {
if lastTime.IsZero() {
last = cur
lastTime = now
return 0
}

t := now.Sub(lastTime).Seconds()
rate := float64(cur-last) / t

last = cur
lastTime = now

return rate
}
}

func goversion() string {
bnfo, ok := debug.ReadBuildInfo()
if ok {
return bnfo.GoVersion
}

return "<unknown version>"
}
8 changes: 8 additions & 0 deletions internal/plot/hist.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,11 @@ func downsampleCounts(h *metrics.Float64Histogram, factor int, slice []uint64) [
// Whatever sum remains, it goes to the last bucket.
return append(slice, sum)
}

func floatseq(n int) []float64 {
seq := make([]float64, n)
for i := range n {
seq[i] = float64(i)
}
return seq
}
66 changes: 66 additions & 0 deletions internal/plot/indices_gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//go:build ignore

package main

import (
"bytes"
"fmt"
"go/format"
"runtime/metrics"
"slices"
"strings"
)

func varname(metric string) string {
name, unit, ok := strings.Cut(metric, ":")
if !ok {
panic("didn't find ':' on " + metric)
}

name = "idx" + name + "_" + unit
name = strings.ReplaceAll(name, "/", "_")
name = strings.ReplaceAll(name, "-", "_")

return name
}

type idxname struct {
idx string
name string
}

// TODO: we could also get the number of buckets for histograms, and preprocess other stuff.

func main() {
var all []idxname
for _, m := range metrics.All() {
if !strings.HasPrefix(m.Name, "/godebug/") {
all = append(all, idxname{varname(m.Name), m.Name})
}
}
slices.SortFunc(all, func(a, b idxname) int {
return strings.Compare(a.idx, b.idx)
})

var out bytes.Buffer

fmt.Fprintln(&out, `package plot
// Code generated by internal/plot/indices_gen.go; DO NOT EDIT.
//lint:file-ignore ST1003 Ignore underscore in generated index names
//lint:file-ignore U1000 Ignore unused indices. they're generated
`)
fmt.Fprintln(&out, `var (`)
for _, m := range all {
fmt.Fprintf(&out, "%s = mustidx(%q)\n", m.idx, m.name)
}
fmt.Fprintln(&out, `)`)

formatted, err := format.Source(out.Bytes())
if err != nil {
fmt.Println("format error:", err)
fmt.Printf("%s", out.String())
panic(err)
}
fmt.Println(string(formatted))
}
21 changes: 21 additions & 0 deletions internal/plot/indices_go1.26.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading