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 }