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 }