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