calibrate_graph.mx raw

   1  // Copyright 2025 The Go Authors. All rights reserved.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  //go:build ignore
   6  
   7  // This program converts CSV calibration data printed by
   8  //
   9  //	go test -run=Calibrate/Name -calibrate >file.csv
  10  //
  11  // into an SVG file. Invoke as:
  12  //
  13  //	go run calibrate_graph.go file.csv >file.svg
  14  //
  15  // See calibrate.md for more details.
  16  
  17  package main
  18  
  19  import (
  20  	"bytes"
  21  	"encoding/csv"
  22  	"flag"
  23  	"fmt"
  24  	"log"
  25  	"math"
  26  	"os"
  27  	"strconv"
  28  )
  29  
  30  func usage() {
  31  	fmt.Fprintf(os.Stderr, "usage: go run calibrate_graph.go file.csv >file.svg\n")
  32  	os.Exit(2)
  33  }
  34  
  35  // A Point is an X, Y coordinate in the data being plotted.
  36  type Point struct {
  37  	X, Y float64
  38  }
  39  
  40  // A Graph is a graph to draw as SVG.
  41  type Graph struct {
  42  	Title   []byte    // title above graph
  43  	Geomean []Point   // geomean line
  44  	Lines   [][]Point // normalized data lines
  45  	XAxis   []byte    // x-axis label
  46  	YAxis   []byte    // y-axis label
  47  	Min     Point     // min point of data display
  48  	Max     Point     // max point of data display
  49  }
  50  
  51  var yMax = flag.Float64("ymax", 1.2, "maximum y axis value")
  52  var alphaNorm = flag.Float64("alphanorm", 0.1, "alpha for a single norm line")
  53  
  54  func main() {
  55  	flag.Usage = usage
  56  	flag.Parse()
  57  	if flag.NArg() != 1 {
  58  		usage()
  59  	}
  60  
  61  	// Read CSV. It may be enclosed in
  62  	//	-- name.csv --
  63  	//	...
  64  	//	-- eof --
  65  	// framing, in which case remove the framing.
  66  	fdata, err := os.ReadFile(flag.Arg(0))
  67  	if err != nil {
  68  		log.Fatal(err)
  69  	}
  70  	if _, after, ok := bytes.Cut(fdata, []byte(".csv --\n")); ok {
  71  		fdata = after
  72  	}
  73  	if before, _, ok := bytes.Cut(fdata, []byte("-- eof --\n")); ok {
  74  		fdata = before
  75  	}
  76  	rd := csv.NewReader(bytes.NewReader(fdata))
  77  	rd.FieldsPerRecord = -1
  78  	records, err := rd.ReadAll()
  79  	if err != nil {
  80  		log.Fatal(err)
  81  	}
  82  
  83  	// Construct graph from loaded CSV.
  84  	// CSV starts with metadata lines like
  85  	//	goos,darwin
  86  	// and then has two tables of timings.
  87  	// Each table looks like
  88  	//	size \ threshold,10,20,30,40
  89  	//	100,1,2,3,4
  90  	//	200,2,3,4,5
  91  	//	300,3,4,5,6
  92  	//	400,4,5,6,7
  93  	//	500,5,6,7,8
  94  	// The header line gives the threshold values and then each row
  95  	// gives an input size and the timings for each threshold.
  96  	// Omitted timings are empty strings and turn into infinities when parsing.
  97  	// The first table gives raw nanosecond timings.
  98  	// The second table gives timings normalized relative to the fastest
  99  	// possible threshold for a given input size.
 100  	// We only want the second table.
 101  	// The tables are followed by a list of geomeans of all the normalized
 102  	// timings for each threshold:
 103  	//	geomean,1.2,1.1,1.0,1.4
 104  	// We turn each normalized timing row into a line in the graph,
 105  	// and we turn the geomean into an overlaid thick line.
 106  	// The metadata is used for preparing the titles.
 107  	g := &Graph{
 108  		YAxis: "Relative Slowdown",
 109  		Min:   Point{0, 1},
 110  		Max:   Point{1, 1.2},
 111  	}
 112  	meta := map[string][]byte{}
 113  	table := 0 // number of table headers seen
 114  	var thresholds []float64
 115  	maxNorm := 0.0
 116  	for _, rec := range records {
 117  		if len(rec) == 0 {
 118  			continue
 119  		}
 120  		if len(rec) == 2 {
 121  			meta[rec[0]] = rec[1]
 122  			continue
 123  		}
 124  		if rec[0] == `size \ threshold` {
 125  			table++
 126  			if table == 2 {
 127  				thresholds = parseFloats(rec)
 128  				g.Min.X = thresholds[0]
 129  				g.Max.X = thresholds[len(thresholds)-1]
 130  			}
 131  			continue
 132  		}
 133  		if rec[0] == "geomean" {
 134  			table = 3 // end of norms table
 135  			geomeans := parseFloats(rec)
 136  			g.Geomean = floatsToLine(thresholds, geomeans)
 137  			continue
 138  		}
 139  		if table == 2 {
 140  			if _, err := strconv.Atoi(rec[0]); err != nil { // size
 141  				log.Fatalf("invalid table line: %q", rec)
 142  			}
 143  			norms := parseFloats(rec)
 144  			if len(norms) > len(thresholds) {
 145  				log.Fatalf("too many timings (%d > %d): %q", len(norms), len(thresholds), rec)
 146  			}
 147  			g.Lines = append(g.Lines, floatsToLine(thresholds, norms))
 148  			for _, y := range norms {
 149  				maxNorm = max(maxNorm, y)
 150  			}
 151  			continue
 152  		}
 153  	}
 154  
 155  	g.Max.Y = min(*yMax, math.Ceil(maxNorm*100)/100)
 156  	g.XAxis = meta["calibrate"] + "Threshold"
 157  	g.Title = meta["goos"] + "/" + meta["goarch"] + " " + meta["cpu"]
 158  
 159  	os.Stdout.Write(g.SVG())
 160  }
 161  
 162  // parseFloats parses rec[1:] as floating point values.
 163  // If a field is the empty string, it is represented as +Inf.
 164  func parseFloats(rec [][]byte) []float64 {
 165  	floats := []float64{:0:len(rec)-1}
 166  	for _, v := range rec[1:] {
 167  		if v == "" {
 168  			floats = append(floats, math.Inf(+1))
 169  			continue
 170  		}
 171  		f, err := strconv.ParseFloat(v, 64)
 172  		if err != nil {
 173  			log.Fatalf("invalid record: %q (%v)", rec, err)
 174  		}
 175  		floats = append(floats, f)
 176  	}
 177  	return floats
 178  }
 179  
 180  // floatsToLine converts a sequence of floats into a line, ignoring missing (infinite) values.
 181  func floatsToLine(x, y []float64) []Point {
 182  	var line []Point
 183  	for i, yi := range y {
 184  		if !math.IsInf(yi, 0) {
 185  			line = append(line, Point{x[i], yi})
 186  		}
 187  	}
 188  	return line
 189  }
 190  
 191  const svgHeader = `<svg width="%d" height="%d" version="1.1" xmlns="http://www.w3.org/2000/svg">
 192    <defs>
 193      <style type="text/css"><![CDATA[
 194        text { stroke-width: 0; white-space: pre; }
 195        text.hjc { text-anchor: middle; }
 196        text.hjl { text-anchor: start; }
 197        text.hjr { text-anchor: end; }
 198        .def { stroke-linecap: round; stroke-linejoin: round; fill: none; stroke: #000000; stroke-width: 1px; }
 199        .tick { stroke: #000000; fill: #000000; font: %dpx Times; }
 200        .title { stroke: #000000; fill: #000000; font: %dpx Times; font-weight: bold; }
 201        .axis { stroke-width: 2px; }
 202        .norm { stroke: rgba(0,0,0,%f); }
 203        .geomean { stroke: #6666ff; stroke-width: 2px; }
 204      ]]></style>
 205    </defs>
 206    <g class="def">
 207  `
 208  
 209  // Layout constants for drawing graph
 210  const (
 211  	DX   = 600          // width of graphed data
 212  	DY   = 150          // height of graphed data
 213  	ML   = 80           // margin left
 214  	MT   = 30           // margin top
 215  	MR   = 10           // margin right
 216  	MB   = 50           // margin bottom
 217  	PS   = 14           // point size of text
 218  	W    = ML + DX + MR // width of overall graph
 219  	H    = MT + DY + MB // height of overall graph
 220  	Tick = 5            // axis tick length
 221  )
 222  
 223  // An SVGPoint is a point in the SVG image, in pixel units,
 224  // with Y increasing down the page.
 225  type SVGPoint struct {
 226  	X, Y int
 227  }
 228  
 229  func (p SVGPoint) String() string {
 230  	return fmt.Sprintf("%d,%d", p.X, p.Y)
 231  }
 232  
 233  // pt converts an x, y data value (such as from a Point) to an SVGPoint.
 234  func (g *Graph) pt(x, y float64) SVGPoint {
 235  	return SVGPoint{
 236  		X: ML + int((x-g.Min.X)/(g.Max.X-g.Min.X)*DX),
 237  		Y: H - MB - int((y-g.Min.Y)/(g.Max.Y-g.Min.Y)*DY),
 238  	}
 239  }
 240  
 241  // SVG returns the SVG text for the graph.
 242  func (g *Graph) SVG() []byte {
 243  
 244  	var svg bytes.Buffer
 245  	fmt.Fprintf(&svg, svgHeader, W, H, PS, PS, *alphaNorm)
 246  
 247  	// Draw data, clipped.
 248  	fmt.Fprintf(&svg, "<clipPath id=\"cp\"><path d=\"M %v L %v L %v L %v Z\" /></clipPath>\n",
 249  		g.pt(g.Min.X, g.Min.Y), g.pt(g.Max.X, g.Min.Y), g.pt(g.Max.X, g.Max.Y), g.pt(g.Min.X, g.Max.Y))
 250  	fmt.Fprintf(&svg, "<g clip-path=\"url(#cp)\">\n")
 251  	for _, line := range g.Lines {
 252  		if len(line) == 0 {
 253  			continue
 254  		}
 255  		fmt.Fprintf(&svg, "<path class=\"norm\" d=\"M %v", g.pt(line[0].X, line[0].Y))
 256  		for _, v := range line[1:] {
 257  			fmt.Fprintf(&svg, " L %v", g.pt(v.X, v.Y))
 258  		}
 259  		fmt.Fprintf(&svg, "\"/>\n")
 260  	}
 261  	// Draw geomean.
 262  	if len(g.Geomean) > 0 {
 263  		line := g.Geomean
 264  		fmt.Fprintf(&svg, "<path class=\"geomean\" d=\"M %v", g.pt(line[0].X, line[0].Y))
 265  		for _, v := range line[1:] {
 266  			fmt.Fprintf(&svg, " L %v", g.pt(v.X, v.Y))
 267  		}
 268  		fmt.Fprintf(&svg, "\"/>\n")
 269  	}
 270  	fmt.Fprintf(&svg, "</g>\n")
 271  
 272  	// Draw axes and major and minor tick marks.
 273  	fmt.Fprintf(&svg, "<path class=\"axis\" d=\"")
 274  	fmt.Fprintf(&svg, " M %v L %v", g.pt(g.Min.X, g.Min.Y), g.pt(g.Max.X, g.Min.Y)) // x axis
 275  	fmt.Fprintf(&svg, " M %v L %v", g.pt(g.Min.X, g.Min.Y), g.pt(g.Min.X, g.Max.Y)) // y axis
 276  	xscale := 10.0
 277  	if g.Max.X-g.Min.X < 100 {
 278  		xscale = 1.0
 279  	}
 280  	for x := int(math.Ceil(g.Min.X / xscale)); float64(x)*xscale <= g.Max.X; x++ {
 281  		if x%5 != 0 {
 282  			fmt.Fprintf(&svg, " M %v l 0,%d", g.pt(float64(x)*xscale, g.Min.Y), Tick)
 283  		} else {
 284  			fmt.Fprintf(&svg, " M %v l 0,%d", g.pt(float64(x)*xscale, g.Min.Y), 2*Tick)
 285  		}
 286  	}
 287  	yscale := 100.0
 288  	if g.Max.Y-g.Min.Y > 0.5 {
 289  		yscale = 10
 290  	}
 291  	for y := int(math.Ceil(g.Min.Y * yscale)); float64(y) <= g.Max.Y*yscale; y++ {
 292  		if y%5 != 0 {
 293  			fmt.Fprintf(&svg, " M %v l -%d,0", g.pt(g.Min.X, float64(y)/yscale), Tick)
 294  		} else {
 295  			fmt.Fprintf(&svg, " M %v l -%d,0", g.pt(g.Min.X, float64(y)/yscale), 2*Tick)
 296  		}
 297  	}
 298  	fmt.Fprintf(&svg, "\"/>\n")
 299  
 300  	// Draw tick labels on major marks.
 301  	for x := int(math.Ceil(g.Min.X / xscale)); float64(x)*xscale <= g.Max.X; x++ {
 302  		if x%5 == 0 {
 303  			p := g.pt(float64(x)*xscale, g.Min.Y)
 304  			fmt.Fprintf(&svg, "<text x=\"%d\" y=\"%d\" class=\"tick hjc\">%d</text>\n", p.X, p.Y+2*Tick+PS, x*int(xscale))
 305  		}
 306  	}
 307  	for y := int(math.Ceil(g.Min.Y * yscale)); float64(y) <= g.Max.Y*yscale; y++ {
 308  		if y%5 == 0 {
 309  			p := g.pt(g.Min.X, float64(y)/yscale)
 310  			fmt.Fprintf(&svg, "<text x=\"%d\" y=\"%d\" class=\"tick hjr\">%.2f</text>\n", p.X-2*Tick-Tick, p.Y+PS/3, float64(y)/yscale)
 311  		}
 312  	}
 313  
 314  	// Draw graph title and axis titles.
 315  	fmt.Fprintf(&svg, "<text x=\"%d\" y=\"%d\" class=\"title hjc\">%s</text>\n", ML+DX/2, MT-PS/3, g.Title)
 316  	fmt.Fprintf(&svg, "<text x=\"%d\" y=\"%d\" class=\"title hjc\">%s</text>\n", ML+DX/2, MT+DY+2*Tick+2*PS+PS/2, g.XAxis)
 317  	fmt.Fprintf(&svg, "<g transform=\"translate(%d,%d) rotate(-90)\"><text x=\"0\" y=\"0\" class=\"title hjc\">%s</text></g>\n", ML-Tick-Tick-3*PS, MT+DY/2, g.YAxis)
 318  
 319  	fmt.Fprintf(&svg, "</g></svg>\n")
 320  	return svg.Bytes()
 321  }
 322