sysbench/cmd/plot/main.go
2026-04-02 20:08:25 +02:00

187 lines
4.4 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"math"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
"codeberg.org/elisiei/sysbench/internal/hyperfine"
)
const (
svgW = 900
padTop = 48
padBottom = 80
padLeft = 200
padRight = 60
barHeight = 28
barGap = 14
tickCount = 6
cornerR = 0
)
var palette = []string{
"#5E81AC", "#88C0D0", "#A3BE8C", "#EBCB8B",
"#D08770", "#BF616A", "#B48EAD", "#8FBCBB",
}
func colour(i int) string { return palette[i%len(palette)] }
func main() {
dir := resolveDir()
src := filepath.Join(dir, "results.json")
dst := filepath.Join(dir, "plot.svg")
data, err := os.ReadFile(src)
if err != nil {
log.Fatalf("read %s: %v", src, err)
}
var result hyperfine.Result
if err := json.Unmarshal(data, &result); err != nil {
log.Fatalf("parse: %v", err)
}
if len(result.Results) == 0 {
log.Fatal("no benchmark results found")
}
svg := render(result.Results, dir)
if err := os.WriteFile(dst, []byte(svg), 0o644); err != nil {
log.Fatalf("write %s: %v", dst, err)
}
fmt.Printf("plot written to %s\n", dst)
pngPath := filepath.Join(dir, "plot.png")
cmd := exec.Command("magick",
"-density", "192",
"-background", "none",
dst,
pngPath,
)
if err := cmd.Run(); err != nil {
log.Fatalf("convert to png: %v", err)
}
fmt.Printf("png written to %s\n", pngPath)
}
func resolveDir() string {
if len(os.Args) > 1 {
return os.Args[1]
}
return filepath.Join("results", time.Now().Format("2006-01-02"))
}
func render(bs []hyperfine.Benchmark, date string) string {
sort.Slice(bs, func(i, j int) bool {
return bs[i].Mean < bs[j].Mean
})
n := len(bs)
chartH := n*(barHeight+barGap) + barGap
height := padTop + chartH + padBottom
maxMean := 0.0
for _, b := range bs {
if b.Mean > maxMean {
maxMean = b.Mean
}
}
scale := niceMax(maxMean, tickCount)
chartW := svgW - padLeft - padRight
var sb strings.Builder
fmt.Fprintf(&sb, `<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" font-family="monospace" font-size="13">`, svgW, height)
fmt.Fprintln(&sb)
fmt.Fprintf(&sb, ` <rect width="%d" height="%d" fill="#2E3440"/>`, svgW, height)
fmt.Fprintln(&sb)
title := fmt.Sprintf("system fetch benchmark — %s", filepath.Base(date))
fmt.Fprintf(&sb, ` <text x="%d" y="28" fill="#ECEFF4" font-size="15" font-weight="bold" text-anchor="middle">%s</text>`, svgW/2, title)
fmt.Fprintln(&sb)
for i := 0; i <= tickCount; i++ {
val := scale * float64(i) / float64(tickCount)
x := padLeft + int(float64(chartW)*val/scale)
yTop := padTop
yBot := padTop + chartH
fmt.Fprintf(&sb, ` <line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#434C5E" stroke-width="1"/>`, x, yTop, x, yBot)
fmt.Fprintln(&sb)
label := fmtTime(val)
fmt.Fprintf(&sb, ` <text x="%d" y="%d" fill="#8FBCBB" text-anchor="middle">%s</text>`, x, padTop+chartH+18, label)
fmt.Fprintln(&sb)
}
fmt.Fprintf(&sb, ` <text x="%d" y="%d" fill="#8FBCBB" text-anchor="middle">time (s)</text>`, padLeft+chartW/2, padTop+chartH+38)
fmt.Fprintln(&sb)
for i, b := range bs {
y := padTop + barGap + i*(barHeight+barGap)
barW := int(float64(chartW) * b.Mean / scale)
if barW < 1 {
barW = 1
}
col := colour(i)
label := trimCommand(b.Command, 28)
fmt.Fprintf(&sb, ` <text x="%d" y="%d" fill="#D8DEE9" text-anchor="end">%s</text>`,
padLeft-8, y+barHeight-8, label)
fmt.Fprintln(&sb)
fmt.Fprintf(&sb, ` <rect x="%d" y="%d" width="%d" height="%d" rx="%d" fill="%s"/>`,
padLeft, y, barW, barHeight, cornerR, col)
fmt.Fprintln(&sb)
fmt.Fprintf(&sb, ` <text x="%d" y="%d" fill="#ECEFF4" font-size="11">%s</text>`,
padLeft+barW+8+(int(float64(chartW)*b.Stddev/scale)), y+barHeight-8, fmtTime(b.Mean))
fmt.Fprintln(&sb)
}
fmt.Fprintln(&sb, `</svg>`)
return sb.String()
}
func niceMax(max float64, ticks int) float64 {
raw := max / float64(ticks)
exp := math.Floor(math.Log10(raw))
mag := math.Pow(10, exp)
nice := math.Ceil(raw/mag) * mag
return nice * float64(ticks)
}
func fmtTime(s float64) string {
switch {
case s == 0:
return "0"
case s < 0.001:
return fmt.Sprintf("%.0fµs", s*1e6)
case s < 1:
return fmt.Sprintf("%.0fms", s*1e3)
default:
return fmt.Sprintf("%.2fs", s)
}
}
func trimCommand(cmd string, max int) string {
parts := strings.Fields(cmd)
base := parts[0]
if idx := strings.LastIndexAny(base, "/\\"); idx >= 0 {
base = base[idx+1:]
}
if len(cmd) <= max {
return cmd
}
if len(base) <= max {
return base
}
return base[:max-1] + "..."
}