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  `))