187 lines
4.4 KiB
Go
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] + "..."
|
|
}
|