Source file src/math/big/calibrate_graph.go

     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   string    // title above graph
    43  	Geomean []Point   // geomean line
    44  	Lines   [][]Point // normalized data lines
    45  	XAxis   string    // x-axis label
    46  	YAxis   string    // 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 := make(map[string]string)
   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 []string) []float64 {
   165  	floats := make([]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  

View as plain text