github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/tools/syz-benchcmp/benchcmp.go (about) 1 // Copyright 2017 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 // syz-benchcmp visualizes syz-manager benchmarking results. 5 // First, run syz-manager with -bench=old flag. 6 // Then, do experimental modifications and run syz-manager again with -bench=new flag. 7 // Then, run syz-benchcmp old new. 8 package main 9 10 import ( 11 "bufio" 12 "encoding/json" 13 "flag" 14 "fmt" 15 "html/template" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "sort" 20 21 "github.com/google/syzkaller/pkg/tool" 22 ) 23 24 var ( 25 flagAll = flag.Bool("all", false, "draw graphs for all variables") 26 flagOut = flag.String("out", "", "file to save graphs to; if empty, a random name will be generated") 27 flagOver = flag.String("over", "fuzzing", "the variable that lies on the X axis") 28 flagSkip = flag.Int("skip", -30, "skip that many seconds after start (skip first 20% by default)") 29 ) 30 31 type Graph struct { 32 Name string 33 Headers []string 34 Points []Point 35 } 36 37 type Point struct { 38 Time uint64 39 Vals []uint64 40 } 41 42 func main() { 43 flag.Parse() 44 if len(flag.Args()) == 0 { 45 fmt.Fprintf(os.Stderr, "usage: syz-benchcmp [flags] bench_file0 [bench_file1 [bench_file2]]...\n") 46 flag.PrintDefaults() 47 os.Exit(1) 48 } 49 50 var allowedGraphs map[string]bool 51 if !*flagAll { 52 allowedGraphs = map[string]bool{ 53 "coverage": true, 54 "corpus": true, 55 "exec total": true, 56 "crash types": true, 57 } 58 } 59 points := make(map[string][]Point) 60 headers := []string{} 61 for i, fname := range flag.Args() { 62 headers = append(headers, filepath.Base(fname)) 63 data := readFile(fname) 64 addExecSpeed(data) 65 for _, record := range data { 66 for key, value := range record { 67 pt := Point{ 68 Time: record[*flagOver], 69 Vals: make([]uint64, len(flag.Args())), 70 } 71 pt.Vals[i] = value 72 points[key] = append(points[key], pt) 73 } 74 } 75 } 76 graphs := []*Graph{} 77 for key, points := range points { 78 if allowedGraphs != nil && !allowedGraphs[key] { 79 continue 80 } 81 if key == *flagOver { 82 // This graph would be meaningless - just a straight line. 83 continue 84 } 85 graphs = append(graphs, &Graph{ 86 Name: key, 87 Headers: headers, 88 Points: points, 89 }) 90 } 91 sort.Slice(graphs, func(i, j int) bool { 92 return graphs[i].Name < graphs[j].Name 93 }) 94 for _, g := range graphs { 95 if len(g.Points) == 0 { 96 tool.Failf("no data points") 97 } 98 sort.Sort(pointSlice(g.Points)) 99 skipStart(g) 100 restoreMissingPoints(g) 101 } 102 printFinalStats(graphs) 103 display(graphs) 104 } 105 106 func readFile(fname string) (data []map[string]uint64) { 107 f, err := os.Open(fname) 108 if err != nil { 109 tool.Failf("failed to open input file: %v", err) 110 } 111 defer f.Close() 112 dec := json.NewDecoder(bufio.NewReader(f)) 113 for dec.More() { 114 v := make(map[string]uint64) 115 if err := dec.Decode(&v); err != nil { 116 tool.Failf("failed to decode input file %v: %v", fname, err) 117 } 118 data = append(data, v) 119 } 120 return 121 } 122 123 func addExecSpeed(data []map[string]uint64) { 124 // Speed between consecutive samples is very unstable. 125 const ( 126 window = 100 127 step = 10 128 ) 129 for i := window; i < len(data); i += step { 130 cur := data[i] 131 prev := data[i-window] 132 dx := cur["exec total"] - prev["exec total"] 133 dt := cur[*flagOver] - prev[*flagOver] 134 cur["exec speed"] = dx * 1000 / dt 135 } 136 } 137 138 func skipStart(g *Graph) { 139 skipTime := uint64(*flagSkip) 140 if *flagSkip < 0 { 141 // Negative skip means percents. 142 max := g.Points[len(g.Points)-1].Time 143 skipTime = max * -skipTime / 100 144 } 145 if skipTime > 0 { 146 skip := sort.Search(len(g.Points), func(i int) bool { 147 return g.Points[i].Time > skipTime 148 }) 149 g.Points = g.Points[skip:] 150 } 151 } 152 153 func restoreMissingPoints(g *Graph) { 154 for i := range g.Headers { 155 // Find previous and next non-zero point for each zero point, 156 // and restore its value with linear inerpolation. 157 type Pt struct { 158 Time uint64 159 Val uint64 160 } 161 var prev Pt 162 prevs := make(map[uint64]Pt) 163 for _, pt := range g.Points { 164 if pt.Vals[i] != 0 { 165 prev = Pt{pt.Time, pt.Vals[i]} 166 continue 167 } 168 prevs[pt.Time] = prev 169 } 170 var next Pt 171 for pti := len(g.Points) - 1; pti >= 0; pti-- { 172 pt := g.Points[pti] 173 if pt.Vals[i] != 0 { 174 next = Pt{pt.Time, pt.Vals[i]} 175 continue 176 } 177 prev := prevs[pt.Time] 178 if prev.Val == 0 || next.Val == 0 { 179 continue 180 } 181 pt.Vals[i] = prev.Val 182 if next.Time != prev.Time { 183 // Use signed calculations as corpus can go backwards. 184 pt.Vals[i] += uint64(int64(next.Val-prev.Val) * int64(pt.Time-prev.Time) / int64(next.Time-prev.Time)) 185 } 186 } 187 } 188 } 189 190 func printFinalStats(graphs []*Graph) { 191 for i := 1; i < len(graphs[0].Headers); i++ { 192 fmt.Printf("%-12v%16v%16v%16v\n", "", graphs[0].Headers[0], graphs[0].Headers[i], "diff") 193 for _, g := range graphs { 194 lastNonZero := func(x int) uint64 { 195 for j := len(g.Points) - 1; j >= 0; j-- { 196 if v := g.Points[j].Vals[x]; v != 0 { 197 return v 198 } 199 } 200 return 0 201 } 202 old := lastNonZero(0) 203 new := lastNonZero(i) 204 fmt.Printf("%-12v%16v%16v%+16d\n", g.Name, old, new, int64(new-old)) 205 } 206 fmt.Printf("\n") 207 } 208 } 209 210 var axisTitles = map[string]string{ 211 "fuzzing": "Time, sec", 212 } 213 214 func getAxisTitle() string { 215 value, ok := axisTitles[*flagOver] 216 if ok { 217 return value 218 } 219 return *flagOver 220 } 221 222 func display(graphs []*Graph) { 223 var outf *os.File 224 var err error 225 if *flagOut == "" { 226 outf, err = os.CreateTemp("", "*.html") 227 if err != nil { 228 tool.Failf("failed to create temp file: %v", err) 229 } 230 } else { 231 outf, err = os.Create(*flagOut) 232 if err != nil { 233 tool.Failf("failed to create file: %v", err) 234 } 235 } 236 vars := map[string]interface{}{ 237 "Graphs": graphs, 238 "HAxisTitle": getAxisTitle(), 239 } 240 if err := htmlTemplate.Execute(outf, vars); err != nil { 241 tool.Failf("failed to execute template: %v", err) 242 } 243 outf.Close() 244 if err := exec.Command("xdg-open", outf.Name()).Start(); err != nil { 245 tool.Failf("failed to start browser: %v", err) 246 } 247 } 248 249 type pointSlice []Point 250 251 func (a pointSlice) Len() int { return len(a) } 252 func (a pointSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 253 func (a pointSlice) Less(i, j int) bool { return a[i].Time < a[j].Time } 254 255 var htmlTemplate = template.Must( 256 template.New("").Parse(` 257 <!doctype html> 258 <html> 259 <head> 260 <title>Syzkaller Bench</title> 261 <script type="text/javascript" src="https://www.google.com/jsapi"></script> 262 <script type="text/javascript"> 263 google.load("visualization", "1", {packages:["corechart"]}); 264 google.setOnLoadCallback(drawCharts); 265 function drawCharts() { 266 {{range $id, $graph := .Graphs}} 267 { 268 var data = new google.visualization.DataTable(); 269 data.addColumn({type: 'number'}); 270 {{range $graph.Headers}} 271 data.addColumn({type: 'number', label: '{{.}}'}); 272 {{end}} 273 data.addRows([ 274 {{range $graph.Points}} [ {{.Time}}, {{range .Vals}} {{if .}} {{.}} {{end}}, {{end}} 275 ], 276 {{end}} 277 ]); 278 new google.visualization.LineChart(document.getElementById('graph_div_{{$id}}')). 279 draw(data, { 280 title: '{{$graph.Name}}', 281 width: "100%", 282 height: document.documentElement.clientHeight * 0.48, 283 legend: {position: "in"}, 284 focusTarget: "category", 285 hAxis: {title: "{{$.HAxisTitle}}"}, 286 chartArea: {left: "5%", top: "5%", width: "90%", height:"85%"} 287 }) 288 } 289 {{end}} 290 } 291 </script> 292 </head> 293 <body> 294 {{range $id, $graph := .Graphs}}<div id="graph_div_{{$id}}" style="width:50%;display:inline-block;"></div>{{end}} 295 </body> 296 </html> 297 `))