github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/tools/syz-testbed/stats.go (about)

     1  // Copyright 2021 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  package main
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/google/syzkaller/pkg/osutil"
    15  	"github.com/google/syzkaller/pkg/stats"
    16  )
    17  
    18  type BugInfo struct {
    19  	Title string
    20  	Logs  []string
    21  }
    22  
    23  type RunResult interface{}
    24  
    25  // The information collected from a syz-manager instance.
    26  type SyzManagerResult struct {
    27  	Bugs        []BugInfo
    28  	StatRecords []StatRecord
    29  }
    30  
    31  type SyzReproResult struct {
    32  	Input       *SyzReproInput
    33  	ReproFound  bool
    34  	CReproFound bool
    35  	ReproTitle  string
    36  	Duration    time.Duration
    37  }
    38  
    39  // A snapshot of syzkaller statistics at a particular time.
    40  type StatRecord map[string]uint64
    41  
    42  // The grouping of single instance results. Taken by all stat generating routines.
    43  type RunResultGroup struct {
    44  	Name    string
    45  	Results []RunResult
    46  }
    47  
    48  // Different "views" of the statistics, e.g. only completed instances or completed + running.
    49  type StatView struct {
    50  	Name   string
    51  	Groups []RunResultGroup
    52  }
    53  
    54  // TODO: we're implementing this functionaity at least the 3rd time (see syz-manager/html
    55  // and tools/reporter). Create a more generic implementation and put it into a globally
    56  // visible package.
    57  func collectBugs(workdir string) ([]BugInfo, error) {
    58  	crashdir := filepath.Join(workdir, "crashes")
    59  	dirs, err := osutil.ListDir(crashdir)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  	bugs := []BugInfo{}
    64  	for _, dir := range dirs {
    65  		bugFolder := filepath.Join(crashdir, dir)
    66  		titleBytes, err := os.ReadFile(filepath.Join(bugFolder, "description"))
    67  		if err != nil {
    68  			return nil, err
    69  		}
    70  		bug := BugInfo{
    71  			Title: strings.TrimSpace(string(titleBytes)),
    72  		}
    73  		files, err := os.ReadDir(bugFolder)
    74  		if err != nil {
    75  			return nil, err
    76  		}
    77  		for _, f := range files {
    78  			if strings.HasPrefix(f.Name(), "log") {
    79  				bug.Logs = append(bug.Logs, filepath.Join(bugFolder, f.Name()))
    80  			}
    81  		}
    82  		bugs = append(bugs, bug)
    83  	}
    84  	return bugs, nil
    85  }
    86  
    87  func readBenches(benchFile string) ([]StatRecord, error) {
    88  	f, err := os.Open(benchFile)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	defer f.Close()
    93  	dec := json.NewDecoder(f)
    94  	ret := []StatRecord{}
    95  	for dec.More() {
    96  		curr := make(StatRecord)
    97  		if err := dec.Decode(&curr); err == nil {
    98  			ret = append(ret, curr)
    99  		}
   100  	}
   101  	return ret, nil
   102  }
   103  
   104  // The input are stat snapshots of different instances taken at the same time.
   105  // This function groups those data points per stat types (e.g. exec total, crashes, etc.).
   106  func groupSamples(records []StatRecord) map[string]*stats.Sample {
   107  	ret := make(map[string]*stats.Sample)
   108  	for _, record := range records {
   109  		for key, value := range record {
   110  			if ret[key] == nil {
   111  				ret[key] = &stats.Sample{}
   112  			}
   113  			ret[key].Xs = append(ret[key].Xs, float64(value))
   114  		}
   115  	}
   116  	return ret
   117  }
   118  
   119  type BugSummary struct {
   120  	title        string
   121  	found        map[string]bool
   122  	resultsCount map[string]int // the number of run results that have found this bug
   123  }
   124  
   125  // If there are several instances belonging to a single checkout, we're interested in the
   126  // set of bugs found by at least one of those instances.
   127  func summarizeBugs(groups []RunResultGroup) ([]*BugSummary, error) {
   128  	bugsMap := make(map[string]*BugSummary)
   129  	for _, group := range groups {
   130  		for _, result := range group.SyzManagerResults() {
   131  			for _, bug := range result.Bugs {
   132  				summary := bugsMap[bug.Title]
   133  				if summary == nil {
   134  					summary = &BugSummary{
   135  						title:        bug.Title,
   136  						found:        make(map[string]bool),
   137  						resultsCount: make(map[string]int),
   138  					}
   139  					bugsMap[bug.Title] = summary
   140  				}
   141  				summary.found[group.Name] = true
   142  				summary.resultsCount[group.Name]++
   143  			}
   144  		}
   145  	}
   146  	summaries := []*BugSummary{}
   147  	for _, value := range bugsMap {
   148  		summaries = append(summaries, value)
   149  	}
   150  	return summaries, nil
   151  }
   152  
   153  // For each checkout, take the union of sets of bugs found by each instance.
   154  // Then output these unions as a single table.
   155  func (view StatView) GenerateBugTable() (*Table, error) {
   156  	table := NewTable("Bug")
   157  	for _, group := range view.Groups {
   158  		table.AddColumn(group.Name)
   159  	}
   160  	summaries, err := summarizeBugs(view.Groups)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	for _, bug := range summaries {
   165  		for _, group := range view.Groups {
   166  			if bug.found[group.Name] {
   167  				table.Set(bug.title, group.Name, NewBoolCell(true))
   168  			}
   169  		}
   170  	}
   171  	return table, nil
   172  }
   173  
   174  func (view StatView) GenerateBugCountsTable() (*Table, error) {
   175  	table := NewTable("Bug")
   176  	for _, group := range view.Groups {
   177  		table.AddColumn(group.Name)
   178  	}
   179  	summaries, err := summarizeBugs(view.Groups)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	for _, bug := range summaries {
   184  		for _, group := range view.Groups {
   185  			if bug.found[group.Name] {
   186  				count := bug.resultsCount[group.Name]
   187  				percent := float64(count) / float64(len(group.Results)) * 100.0
   188  				value := fmt.Sprintf("%v (%.1f%%)", count, percent)
   189  				table.Set(bug.title, group.Name, value)
   190  			}
   191  		}
   192  	}
   193  	return table, nil
   194  }
   195  
   196  func (group RunResultGroup) SyzManagerResults() []*SyzManagerResult {
   197  	ret := []*SyzManagerResult{}
   198  	for _, rawRes := range group.Results {
   199  		res, ok := rawRes.(*SyzManagerResult)
   200  		if ok {
   201  			ret = append(ret, res)
   202  		}
   203  	}
   204  	return ret
   205  }
   206  
   207  func (group RunResultGroup) SyzReproResults() []*SyzReproResult {
   208  	ret := []*SyzReproResult{}
   209  	for _, rawRes := range group.Results {
   210  		res, ok := rawRes.(*SyzReproResult)
   211  		if ok {
   212  			ret = append(ret, res)
   213  		}
   214  	}
   215  	return ret
   216  }
   217  
   218  func (group RunResultGroup) AvgStatRecords() []map[string]uint64 {
   219  	ret := []map[string]uint64{}
   220  	commonLen := group.minResultLength()
   221  	for i := 0; i < commonLen; i++ {
   222  		record := make(map[string]uint64)
   223  		for key, value := range group.groupNthRecord(i) {
   224  			record[key] = uint64(value.Median())
   225  		}
   226  		ret = append(ret, record)
   227  	}
   228  	return ret
   229  }
   230  
   231  func (group RunResultGroup) minResultLength() int {
   232  	if len(group.Results) == 0 {
   233  		return 0
   234  	}
   235  	results := group.SyzManagerResults()
   236  	ret := len(results[0].StatRecords)
   237  	for _, result := range results {
   238  		currLen := len(result.StatRecords)
   239  		if currLen < ret {
   240  			ret = currLen
   241  		}
   242  	}
   243  	return ret
   244  }
   245  
   246  func (group RunResultGroup) groupNthRecord(i int) map[string]*stats.Sample {
   247  	records := []StatRecord{}
   248  	for _, result := range group.SyzManagerResults() {
   249  		records = append(records, result.StatRecords[i])
   250  	}
   251  	return groupSamples(records)
   252  }
   253  
   254  func (view StatView) StatsTable() (*Table, error) {
   255  	return view.AlignedStatsTable("uptime")
   256  }
   257  
   258  func (view StatView) AlignedStatsTable(field string) (*Table, error) {
   259  	// We assume that the stats values are nonnegative.
   260  	var commonValue float64
   261  	for _, group := range view.Groups {
   262  		minLen := group.minResultLength()
   263  		if minLen == 0 {
   264  			continue
   265  		}
   266  		sampleGroup := group.groupNthRecord(minLen - 1)
   267  		sample, ok := sampleGroup[field]
   268  		if !ok {
   269  			return nil, fmt.Errorf("field %v is not found", field)
   270  		}
   271  		currValue := sample.Median()
   272  		if currValue < commonValue || commonValue == 0 {
   273  			commonValue = currValue
   274  		}
   275  	}
   276  	table := NewTable("Property")
   277  	cells := make(map[string]map[string]string)
   278  	for _, group := range view.Groups {
   279  		table.AddColumn(group.Name)
   280  		minLen := group.minResultLength()
   281  		if minLen == 0 {
   282  			// Skip empty groups.
   283  			continue
   284  		}
   285  		// Unwind the samples so that they are aligned on the field value.
   286  		var samples map[string]*stats.Sample
   287  		for i := minLen - 1; i >= 0; i-- {
   288  			candidate := group.groupNthRecord(i)
   289  			// TODO: consider data interpolation.
   290  			if candidate[field].Median() >= commonValue {
   291  				samples = candidate
   292  			} else {
   293  				break
   294  			}
   295  		}
   296  		for key, sample := range samples {
   297  			if _, ok := cells[key]; !ok {
   298  				cells[key] = make(map[string]string)
   299  			}
   300  			table.Set(key, group.Name, NewValueCell(sample))
   301  		}
   302  	}
   303  	return table, nil
   304  }
   305  
   306  func (view StatView) InstanceStatsTable() (*Table, error) {
   307  	newView := StatView{}
   308  	for _, group := range view.Groups {
   309  		for i, result := range group.Results {
   310  			newView.Groups = append(newView.Groups, RunResultGroup{
   311  				Name:    fmt.Sprintf("%s-%d", group.Name, i),
   312  				Results: []RunResult{result},
   313  			})
   314  		}
   315  	}
   316  	return newView.StatsTable()
   317  }
   318  
   319  // How often we find a repro to each crash log.
   320  func (view StatView) GenerateReproSuccessTable() (*Table, error) {
   321  	table := NewTable("Bug")
   322  	for _, group := range view.Groups {
   323  		table.AddColumn(group.Name)
   324  	}
   325  	for _, group := range view.Groups {
   326  		for _, result := range group.SyzReproResults() {
   327  			title := result.Input.Title
   328  			cell, _ := table.Get(title, group.Name).(*RatioCell)
   329  			if cell == nil {
   330  				cell = NewRatioCell(0, 0)
   331  			}
   332  			cell.TotalCount++
   333  			if result.ReproFound {
   334  				cell.TrueCount++
   335  			}
   336  			table.Set(title, group.Name, cell)
   337  		}
   338  	}
   339  	return table, nil
   340  }
   341  
   342  // What share of found repros also have a C repro.
   343  func (view StatView) GenerateCReproSuccessTable() (*Table, error) {
   344  	table := NewTable("Bug")
   345  	for _, group := range view.Groups {
   346  		table.AddColumn(group.Name)
   347  	}
   348  	for _, group := range view.Groups {
   349  		for _, result := range group.SyzReproResults() {
   350  			if !result.ReproFound {
   351  				continue
   352  			}
   353  			title := result.Input.Title
   354  			cell, _ := table.Get(title, group.Name).(*RatioCell)
   355  			if cell == nil {
   356  				cell = NewRatioCell(0, 0)
   357  			}
   358  			cell.TotalCount++
   359  			if result.CReproFound {
   360  				cell.TrueCount++
   361  			}
   362  			table.Set(title, group.Name, cell)
   363  		}
   364  	}
   365  	return table, nil
   366  }
   367  
   368  // What share of found repros also have a C repro.
   369  func (view StatView) GenerateReproDurationTable() (*Table, error) {
   370  	table := NewTable("Bug")
   371  	for _, group := range view.Groups {
   372  		table.AddColumn(group.Name)
   373  	}
   374  	for _, group := range view.Groups {
   375  		samples := make(map[string]*stats.Sample)
   376  		for _, result := range group.SyzReproResults() {
   377  			title := result.Input.Title
   378  			var sample *stats.Sample
   379  			sample, ok := samples[title]
   380  			if !ok {
   381  				sample = &stats.Sample{}
   382  				samples[title] = sample
   383  			}
   384  			sample.Xs = append(sample.Xs, result.Duration.Seconds())
   385  		}
   386  
   387  		for title, sample := range samples {
   388  			table.Set(title, group.Name, NewValueCell(sample))
   389  		}
   390  	}
   391  	return table, nil
   392  }
   393  
   394  // List all repro attempts.
   395  func (view StatView) GenerateReproAttemptsTable() (*Table, error) {
   396  	table := NewTable("Result #", "Bug", "Checkout", "Repro found", "C repro found", "Repro title", "Duration")
   397  	for gid, group := range view.Groups {
   398  		for rid, result := range group.SyzReproResults() {
   399  			table.AddRow(
   400  				fmt.Sprintf("%d-%d", gid, rid),
   401  				result.Input.Title,
   402  				group.Name,
   403  				NewBoolCell(result.ReproFound),
   404  				NewBoolCell(result.CReproFound),
   405  				result.ReproTitle,
   406  				result.Duration.Round(time.Second).String(),
   407  			)
   408  		}
   409  	}
   410  	return table, nil
   411  }
   412  
   413  // Average bench files of several instances into a single bench file.
   414  func (group *RunResultGroup) SaveAvgBenchFile(fileName string) error {
   415  	f, err := os.Create(fileName)
   416  	if err != nil {
   417  		return err
   418  	}
   419  	defer f.Close()
   420  	// In long runs, we collect a lot of stat samples, which results
   421  	// in large and slow to load graphs. A subset of 128-256 data points
   422  	// seems to be a reasonable enough precision.
   423  	records := group.AvgStatRecords()
   424  	const targetRecords = 128
   425  	for len(records) > targetRecords*2 {
   426  		newRecords := make([]map[string]uint64, 0, len(records)/2)
   427  		for i := 0; i < len(records); i += 2 {
   428  			newRecords = append(newRecords, records[i])
   429  		}
   430  		records = newRecords
   431  	}
   432  	for _, averaged := range records {
   433  		data, err := json.MarshalIndent(averaged, "", "  ")
   434  		if err != nil {
   435  			return err
   436  		}
   437  		if _, err := f.Write(append(data, '\n')); err != nil {
   438  			return err
   439  		}
   440  	}
   441  	return nil
   442  }
   443  
   444  func (view *StatView) SaveAvgBenches(benchDir string) ([]string, error) {
   445  	files := []string{}
   446  	for _, group := range view.Groups {
   447  		fileName := filepath.Join(benchDir, fmt.Sprintf("avg_%v.txt", group.Name))
   448  		err := group.SaveAvgBenchFile(fileName)
   449  		if err != nil {
   450  			return nil, err
   451  		}
   452  		files = append(files, fileName)
   453  	}
   454  	return files, nil
   455  }
   456  
   457  func (view *StatView) IsEmpty() bool {
   458  	for _, group := range view.Groups {
   459  		if len(group.Results) > 0 {
   460  			return false
   461  		}
   462  	}
   463  	return true
   464  }