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