github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/graphs.go (about) 1 // Copyright 2020 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 "context" 8 "fmt" 9 "net/http" 10 "net/url" 11 "regexp" 12 "sort" 13 "strconv" 14 "time" 15 16 db "google.golang.org/appengine/v2/datastore" 17 ) 18 19 type uiKernelHealthPage struct { 20 Header *uiHeader 21 Graph *uiGraph 22 } 23 24 type uiBugLifetimesPage struct { 25 Header *uiHeader 26 Lifetimes []uiBugLifetime 27 } 28 29 type uiBugLifetime struct { 30 Reported time.Time 31 Fixed float32 32 Fixed1y float32 33 NotFixed float32 34 Introduced float32 35 Introduced1y float32 36 } 37 38 type uiManagersPage struct { 39 Header *uiHeader 40 Managers *uiCheckbox 41 Metrics *uiCheckbox 42 Months *uiSlider 43 Graph *uiGraph 44 } 45 46 type uiCrashesPage struct { 47 Header *uiHeader 48 Graph *uiGraph 49 Regexps *uiMultiInput 50 GraphMonths *uiSlider 51 TableDays *uiSlider 52 Table *uiCrashPageTable 53 } 54 55 type uiGraph struct { 56 Headers []string 57 Columns []uiGraphColumn 58 } 59 60 type uiGraphColumn struct { 61 Hint string 62 Vals []uiGraphValue 63 } 64 65 type uiGraphValue struct { 66 Val float32 67 IsNull bool 68 Hint string 69 } 70 71 type uiCrashPageTable struct { 72 Title string 73 Rows []*uiCrashSummary 74 } 75 76 type uiCrashSummary struct { 77 Title string 78 Link string 79 Count int 80 Share float32 81 GraphLink string 82 } 83 84 type uiCheckbox struct { 85 ID string 86 Caption string 87 Values []*uiCheckboxValue 88 vals []string 89 } 90 91 type uiCheckboxValue struct { 92 ID string 93 Caption string 94 Selected bool 95 } 96 97 type uiSlider struct { 98 ID string 99 Caption string 100 Val int 101 Min int 102 Max int 103 } 104 105 type uiMultiInput struct { 106 ID string 107 Caption string 108 Vals []string 109 } 110 111 // nolint: dupl 112 func handleKernelHealthGraph(c context.Context, w http.ResponseWriter, r *http.Request) error { 113 hdr, err := commonHeader(c, r, w, "") 114 if err != nil { 115 return err 116 } 117 bugs, err := loadGraphBugs(c, hdr.Namespace) 118 if err != nil { 119 return err 120 } 121 data := &uiKernelHealthPage{ 122 Header: hdr, 123 Graph: createBugsGraph(c, bugs), 124 } 125 return serveTemplate(w, "graph_bugs.html", data) 126 } 127 128 // nolint: dupl 129 func handleGraphLifetimes(c context.Context, w http.ResponseWriter, r *http.Request) error { 130 hdr, err := commonHeader(c, r, w, "") 131 if err != nil { 132 return err 133 } 134 bugs, err := loadGraphBugs(c, hdr.Namespace) 135 if err != nil { 136 return err 137 } 138 139 var jobs []*Job 140 keys, err := db.NewQuery("Job"). 141 Filter("Namespace=", hdr.Namespace). 142 Filter("Type=", JobBisectCause). 143 GetAll(c, &jobs) 144 if err != nil { 145 return err 146 } 147 causeBisects := make(map[string]*Job) 148 for i, job := range jobs { 149 if len(job.Commits) != 1 { 150 continue 151 } 152 causeBisects[keys[i].Parent().StringID()] = job 153 } 154 data := &uiBugLifetimesPage{ 155 Header: hdr, 156 Lifetimes: createBugLifetimes(c, bugs, causeBisects), 157 } 158 return serveTemplate(w, "graph_lifetimes.html", data) 159 } 160 161 func loadGraphBugs(c context.Context, ns string) ([]*Bug, error) { 162 filter := func(query *db.Query) *db.Query { 163 return query.Filter("Namespace=", ns) 164 } 165 bugs, _, err := loadAllBugs(c, filter) 166 if err != nil { 167 return nil, err 168 } 169 n := 0 170 fixes := make(map[string]bool) 171 lastReporting := getNsConfig(c, ns).lastActiveReporting() 172 for _, bug := range bugs { 173 if bug.Reporting[lastReporting].Reported.IsZero() { 174 if bug.Status == BugStatusOpen { 175 // These bugs are not released yet. 176 continue 177 } 178 bugReporting := lastReportedReporting(bug) 179 if bugReporting == nil || bugReporting.Auto && bug.Status == BugStatusInvalid { 180 // These bugs were auto-obsoleted before getting released. 181 continue 182 } 183 } 184 dup := false 185 for _, com := range bug.Commits { 186 if fixes[com] { 187 dup = true 188 } 189 fixes[com] = true 190 } 191 if dup { 192 continue 193 } 194 bugs[n] = bug 195 n++ 196 } 197 return bugs[:n], nil 198 } 199 200 func createBugsGraph(c context.Context, bugs []*Bug) *uiGraph { 201 type BugStats struct { 202 Opened int 203 Fixed int 204 Closed int 205 TotalReported int 206 TotalOpen int 207 TotalFixed int 208 TotalClosed int 209 } 210 const timeWeek = 30 * 24 * time.Hour 211 now := timeNow(c) 212 m := make(map[int]*BugStats) 213 maxWeek := 0 214 bugStatsFor := func(t time.Time) *BugStats { 215 week := int(now.Sub(t) / (30 * 24 * time.Hour)) 216 if week < 0 { 217 week = 0 218 } 219 if maxWeek < week { 220 maxWeek = week 221 } 222 bs := m[week] 223 if bs == nil { 224 bs = new(BugStats) 225 m[week] = bs 226 } 227 return bs 228 } 229 for _, bug := range bugs { 230 bugStatsFor(bug.FirstTime).Opened++ 231 if !bug.Closed.IsZero() { 232 if bug.Status == BugStatusFixed { 233 bugStatsFor(bug.Closed).Fixed++ 234 } 235 bugStatsFor(bug.Closed).Closed++ 236 } else if len(bug.Commits) != 0 { 237 bugStatsFor(now).Fixed++ 238 bugStatsFor(now).Closed++ 239 } 240 } 241 var stats []BugStats 242 var prev BugStats 243 for i := maxWeek; i >= 0; i-- { 244 var bs BugStats 245 if p := m[i]; p != nil { 246 bs = *p 247 } 248 bs.TotalReported = prev.TotalReported + bs.Opened 249 bs.TotalFixed = prev.TotalFixed + bs.Fixed 250 bs.TotalClosed = prev.TotalClosed + bs.Closed 251 bs.TotalOpen = bs.TotalReported - bs.TotalClosed 252 stats = append(stats, bs) 253 prev = bs 254 } 255 var columns []uiGraphColumn 256 for week, bs := range stats { 257 col := uiGraphColumn{Hint: now.Add(time.Duration(week-len(stats)+1) * timeWeek).Format("Jan-06")} 258 col.Vals = append(col.Vals, uiGraphValue{Val: float32(bs.TotalOpen)}) 259 col.Vals = append(col.Vals, uiGraphValue{Val: float32(bs.TotalReported)}) 260 col.Vals = append(col.Vals, uiGraphValue{Val: float32(bs.TotalFixed)}) 261 // col.Vals = append(col.Vals, uiGraphValue{Val: float32(bs.Opened)}) 262 // col.Vals = append(col.Vals, uiGraphValue{Val: float32(bs.Fixed)}) 263 columns = append(columns, col) 264 } 265 return &uiGraph{ 266 Headers: []string{"open bugs", "total reported", "total fixed"}, 267 Columns: columns, 268 } 269 } 270 271 func createBugLifetimes(c context.Context, bugs []*Bug, causeBisects map[string]*Job) []uiBugLifetime { 272 var res []uiBugLifetime 273 for i, bug := range bugs { 274 ui := uiBugLifetime{ 275 // TODO: this is not the time when it was reported to the final reporting. 276 Reported: bug.FirstTime, 277 } 278 if bug.Status >= BugStatusInvalid { 279 continue 280 } 281 fixed := bug.FixTime 282 if fixed.IsZero() || bug.Status == BugStatusFixed && bug.Closed.Before(fixed) { 283 fixed = bug.Closed 284 } 285 if !fixed.IsZero() { 286 days := float32(fixed.Sub(ui.Reported)) / float32(24*time.Hour) 287 if days > 365 { 288 ui.Fixed1y = 365 + float32(i%7) 289 } else { 290 if days <= 0 { 291 days = 0.1 292 } 293 ui.Fixed = days 294 } 295 } else { 296 ui.NotFixed = 400 - float32(i%7) 297 } 298 if job := causeBisects[bug.keyHash(c)]; job != nil { 299 days := float32(job.Commits[0].Date.Sub(ui.Reported)) / float32(24*time.Hour) 300 if days < -365 { 301 ui.Introduced1y = -365 - float32(i%7) 302 } else { 303 if days >= 0 { 304 days = -0.1 305 } 306 ui.Introduced = days 307 } 308 } 309 res = append(res, ui) 310 } 311 return res 312 } 313 314 func handleGraphFuzzing(c context.Context, w http.ResponseWriter, r *http.Request) error { 315 hdr, err := commonHeader(c, r, w, "") 316 if err != nil { 317 return err 318 } 319 r.ParseForm() 320 321 allManagers, err := managerList(c, hdr.Namespace) 322 if err != nil { 323 return err 324 } 325 managers, err := createCheckBox(r, "Instances", allManagers) 326 if err != nil { 327 return err 328 } 329 metrics, err := createCheckBox(r, "Metrics", []string{ 330 "MaxCorpus", "MaxCover", "MaxPCs", "TotalFuzzingTime", 331 "TotalCrashes", "CrashTypes", "SuppressedCrashes", "TotalExecs", 332 "ExecsPerSec", "TriagedPCs", "TriagedCoverage"}) 333 if err != nil { 334 return err 335 } 336 data := &uiManagersPage{ 337 Header: hdr, 338 Managers: managers, 339 Metrics: metrics, 340 Months: createSlider(r, "Months", 1, 36), 341 } 342 data.Graph, err = createManagersGraph(c, hdr.Namespace, data.Managers.vals, data.Metrics.vals, data.Months.Val*30) 343 if err != nil { 344 return err 345 } 346 return serveTemplate(w, "graph_fuzzing.html", data) 347 } 348 349 func createManagersGraph(c context.Context, ns string, selManagers, selMetrics []string, days int) (*uiGraph, error) { 350 graph := &uiGraph{} 351 for _, mgr := range selManagers { 352 for _, metric := range selMetrics { 353 graph.Headers = append(graph.Headers, mgr+"-"+metric) 354 } 355 } 356 now := timeNow(c) 357 const day = 24 * time.Hour 358 // Step 1: fill the whole table with empty values to simplify subsequent logic 359 // when we fill random positions in the table. 360 for date := 0; date <= days; date++ { 361 col := uiGraphColumn{Hint: now.Add(time.Duration(date-days) * day).Format("02-01-2006")} 362 for range selManagers { 363 for range selMetrics { 364 col.Vals = append(col.Vals, uiGraphValue{Hint: "-"}) 365 } 366 } 367 graph.Columns = append(graph.Columns, col) 368 } 369 // Step 2: fill in actual data. 370 for mgrIndex, mgr := range selManagers { 371 parentKey := mgrKey(c, ns, mgr) 372 var stats []*ManagerStats 373 _, err := db.NewQuery("ManagerStats"). 374 Ancestor(parentKey). 375 GetAll(c, &stats) 376 if err != nil { 377 return nil, err 378 } 379 for _, stat := range stats { 380 dayIndex := days - int(now.Sub(dateTime(stat.Date))/day) 381 if dayIndex < 0 || dayIndex > days { 382 continue 383 } 384 for metricIndex, metric := range selMetrics { 385 val, canBeZero := extractMetric(stat, metric) 386 graph.Columns[dayIndex].Vals[mgrIndex*len(selMetrics)+metricIndex] = uiGraphValue{ 387 Val: float32(val), 388 IsNull: !canBeZero && val == 0, 389 Hint: fmt.Sprintf("%.2f", val), 390 } 391 } 392 } 393 } 394 // Step 3: normalize data to [0..100] range. 395 // We visualize radically different values and they all should fit into a single graph. 396 // We normalize the same metric across all managers so that a single metric is still 397 // comparable across different managers. 398 if len(selMetrics) > 1 { 399 for metricIndex := range selMetrics { 400 max := float32(1) 401 for col := range graph.Columns { 402 for mgrIndex := range selManagers { 403 item := graph.Columns[col].Vals[mgrIndex*len(selMetrics)+metricIndex] 404 if item.IsNull { 405 continue 406 } 407 val := item.Val 408 if max < val { 409 max = val 410 } 411 } 412 } 413 for col := range graph.Columns { 414 for mgrIndex := range selManagers { 415 graph.Columns[col].Vals[mgrIndex*len(selMetrics)+metricIndex].Val /= max * 100 416 } 417 } 418 } 419 } 420 return graph, nil 421 } 422 423 func extractMetric(stat *ManagerStats, metric string) (val float64, canBeZero bool) { 424 switch metric { 425 case "MaxCorpus": 426 return float64(stat.MaxCorpus), false 427 case "MaxCover": 428 return float64(stat.MaxCover), false 429 case "MaxPCs": 430 return float64(stat.MaxPCs), false 431 case "TotalFuzzingTime": 432 return float64(stat.TotalFuzzingTime), true 433 case "TotalCrashes": 434 return float64(stat.TotalCrashes), true 435 case "CrashTypes": 436 return float64(stat.CrashTypes), true 437 case "SuppressedCrashes": 438 return float64(stat.SuppressedCrashes), true 439 case "TotalExecs": 440 return float64(stat.TotalExecs), true 441 case "ExecsPerSec": 442 timeSec := float64(stat.TotalFuzzingTime) / 1e9 443 if timeSec == 0 { 444 return 0, true 445 } 446 return float64(stat.TotalExecs) / timeSec, true 447 case "TriagedCoverage": 448 return float64(stat.TriagedCoverage), false 449 case "TriagedPCs": 450 return float64(stat.TriagedPCs), false 451 default: 452 panic(fmt.Sprintf("unknown metric %q", metric)) 453 } 454 } 455 456 // createCheckBox additionally validates r.Form data and returns 400 error in case of mismatch. 457 func createCheckBox(r *http.Request, caption string, values []string) (*uiCheckbox, error) { 458 // TODO: turn this into proper ID that can be used in HTML. 459 id := caption 460 for _, formVal := range r.Form[id] { 461 if !stringInList(values, formVal) { 462 return nil, ErrClientBadRequest 463 } 464 } 465 ui := &uiCheckbox{ 466 ID: id, 467 Caption: caption, 468 vals: r.Form[id], 469 } 470 if len(ui.vals) == 0 { 471 ui.vals = []string{values[0]} 472 } 473 for _, val := range values { 474 ui.Values = append(ui.Values, &uiCheckboxValue{ 475 ID: val, 476 // TODO: use this as caption and form ID. 477 Selected: stringInList(ui.vals, val), 478 }) 479 } 480 return ui, nil 481 } 482 483 func createSlider(r *http.Request, caption string, min, max int) *uiSlider { 484 // TODO: turn this into proper ID that can be used in HTML. 485 id := caption 486 ui := &uiSlider{ 487 ID: id, 488 Caption: caption, 489 Val: min, 490 Min: min, 491 Max: max, 492 } 493 if val, _ := strconv.Atoi(r.FormValue(id)); val >= min && val <= max { 494 ui.Val = val 495 } 496 return ui 497 } 498 499 func createMultiInput(r *http.Request, id, caption string) *uiMultiInput { 500 filteredValues := []string{} 501 for _, val := range r.Form[id] { 502 if val == "" { 503 continue 504 } 505 filteredValues = append(filteredValues, val) 506 } 507 return &uiMultiInput{ 508 ID: id, 509 Caption: caption, 510 Vals: filteredValues, 511 } 512 } 513 514 func handleGraphCrashes(c context.Context, w http.ResponseWriter, r *http.Request) error { 515 hdr, err := commonHeader(c, r, w, "") 516 if err != nil { 517 return err 518 } 519 r.ParseForm() 520 521 data := &uiCrashesPage{ 522 Header: hdr, 523 Regexps: createMultiInput(r, "regexp", "Regexps"), 524 GraphMonths: createSlider(r, "Months", 1, 36), 525 TableDays: createSlider(r, "Days", 1, 30), 526 } 527 528 bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { 529 return query.Filter("Namespace=", hdr.Namespace) 530 }) 531 if err != nil { 532 return err 533 } 534 accessLevel := accessLevel(c, r) 535 nbugs := 0 536 for _, bug := range bugs { 537 if accessLevel < bug.sanitizeAccess(c, accessLevel) { 538 continue 539 } 540 bugs[nbugs] = bug 541 nbugs++ 542 } 543 bugs = bugs[:nbugs] 544 if len(data.Regexps.Vals) == 0 { 545 // If no data is passed, then at least show the graph for important crash types. 546 data.Regexps.Vals = []string{"^KASAN", "^KMSAN", "^KCSAN", "^SYZFAIL"} 547 } 548 if r.Form["show-graph"] != nil { 549 data.Graph, err = createCrashesGraph(c, hdr.Namespace, data.Regexps.Vals, data.GraphMonths.Val*30, bugs) 550 if err != nil { 551 return err 552 } 553 } else { 554 data.Table = createCrashesTable(c, hdr.Namespace, data.TableDays.Val, bugs) 555 } 556 return serveTemplate(w, "graph_crashes.html", data) 557 } 558 559 func createCrashesTable(c context.Context, ns string, days int, bugs []*Bug) *uiCrashPageTable { 560 const dayDuration = 24 * time.Hour 561 startDay := timeNow(c).Add(time.Duration(-days+1) * dayDuration) 562 table := &uiCrashPageTable{ 563 Title: fmt.Sprintf("Top crashers of the last %d day(s)", days), 564 } 565 totalCount := 0 566 for _, bug := range bugs { 567 count := 0 568 for _, s := range bug.dailyStatsTail(startDay) { 569 count += s.CrashCount 570 } 571 if count == 0 { 572 continue 573 } 574 totalCount += count 575 titleRegexp := regexp.QuoteMeta(bug.Title) 576 table.Rows = append(table.Rows, &uiCrashSummary{ 577 Title: bug.Title, 578 Link: bugLink(bug.keyHash(c)), 579 GraphLink: "?show-graph=1&Months=1®exp=" + url.QueryEscape(titleRegexp), 580 Count: count, 581 }) 582 } 583 for id := range table.Rows { 584 table.Rows[id].Share = float32(table.Rows[id].Count) / float32(totalCount) * 100.0 585 } 586 // Order by descending crash count. 587 sort.SliceStable(table.Rows, func(i, j int) bool { 588 return table.Rows[i].Count > table.Rows[j].Count 589 }) 590 return table 591 } 592 593 func createCrashesGraph(c context.Context, ns string, regexps []string, days int, bugs []*Bug) (*uiGraph, error) { 594 const dayDuration = 24 * time.Hour 595 graph := &uiGraph{Headers: regexps} 596 startDay := timeNow(c).Add(time.Duration(-days) * dayDuration) 597 // Step 1: fill the whole table with empty values. 598 dateToIdx := make(map[int]int) 599 for date := 0; date <= days; date++ { 600 day := startDay.Add(time.Duration(date) * dayDuration) 601 dateToIdx[timeDate(day)] = date 602 col := uiGraphColumn{Hint: day.Format("02-01-2006")} 603 for range regexps { 604 col.Vals = append(col.Vals, uiGraphValue{Hint: "-"}) 605 } 606 graph.Columns = append(graph.Columns, col) 607 } 608 // Step 2: fill in crash counts. 609 totalCounts := make(map[int]int) 610 for _, bug := range bugs { 611 for _, stat := range bug.dailyStatsTail(startDay) { 612 pos, ok := dateToIdx[stat.Date] 613 if !ok { 614 continue 615 } 616 totalCounts[pos] += stat.CrashCount 617 } 618 } 619 for regexpID, val := range regexps { 620 r, err := regexp.Compile(val) 621 if err != nil { 622 return nil, err 623 } 624 for _, bug := range bugs { 625 if !r.MatchString(bug.Title) { 626 continue 627 } 628 for _, stat := range bug.DailyStats { 629 pos, ok := dateToIdx[stat.Date] 630 if !ok { 631 continue 632 } 633 graph.Columns[pos].Vals[regexpID].Val += float32(stat.CrashCount) 634 } 635 } 636 } 637 // Step 3: convert abs values to percents. 638 for date := 0; date <= days; date++ { 639 count := totalCounts[date] 640 if count == 0 { 641 continue 642 } 643 for id := range graph.Columns[date].Vals { 644 value := &graph.Columns[date].Vals[id] 645 abs := value.Val 646 value.Val = abs / float32(count) * 100.0 647 value.Hint = fmt.Sprintf("%.1f%% (%.0f)", value.Val, abs) 648 } 649 } 650 return graph, nil 651 }