github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/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 "github.com/google/syzkaller/pkg/report" 17 "github.com/google/syzkaller/pkg/report/crash" 18 db "google.golang.org/appengine/v2/datastore" 19 ) 20 21 type uiKernelHealthPage struct { 22 Header *uiHeader 23 Graph *uiGraph 24 } 25 26 type uiBugLifetimesPage struct { 27 Header *uiHeader 28 Lifetimes []uiBugLifetime 29 } 30 31 type uiHistogramPage struct { 32 Title string 33 Header *uiHeader 34 Graph *uiGraph 35 } 36 37 type uiBugLifetime struct { 38 Reported time.Time 39 Fixed float32 40 Fixed1y float32 41 NotFixed float32 42 Introduced float32 43 Introduced1y float32 44 } 45 46 type uiManagersPage struct { 47 Header *uiHeader 48 Managers *uiCheckbox 49 Metrics *uiCheckbox 50 Months *uiSlider 51 Graph *uiGraph 52 } 53 54 type uiCrashesPage struct { 55 Header *uiHeader 56 Graph *uiGraph 57 Regexps *uiMultiInput 58 GraphMonths *uiSlider 59 TableDays *uiSlider 60 Table *uiCrashPageTable 61 } 62 63 type uiGraph struct { 64 Headers []uiGraphHeader 65 Columns []uiGraphColumn 66 } 67 68 type uiGraphHeader struct { 69 Name string 70 Color string 71 } 72 73 type uiGraphColumn struct { 74 Hint string 75 Annotation float32 76 Vals []uiGraphValue 77 } 78 79 type uiGraphValue struct { 80 Val float32 81 IsNull bool 82 Hint string 83 } 84 85 type uiCrashPageTable struct { 86 Title string 87 Rows []*uiCrashSummary 88 } 89 90 type uiCrashSummary struct { 91 Title string 92 Link string 93 Count int 94 Share float32 95 GraphLink string 96 } 97 98 type uiCheckbox struct { 99 ID string 100 Caption string 101 Values []*uiCheckboxValue 102 vals []string 103 } 104 105 type uiCheckboxValue struct { 106 ID string 107 Caption string 108 Selected bool 109 } 110 111 type uiSlider struct { 112 ID string 113 Caption string 114 Val int 115 Min int 116 Max int 117 } 118 119 type uiMultiInput struct { 120 ID string 121 Caption string 122 Vals []string 123 } 124 125 // nolint: dupl 126 func handleKernelHealthGraph(c context.Context, w http.ResponseWriter, r *http.Request) error { 127 hdr, err := commonHeader(c, r, w, "") 128 if err != nil { 129 return err 130 } 131 bugs, err := loadGraphBugs(c, hdr.Namespace) 132 if err != nil { 133 return err 134 } 135 data := &uiKernelHealthPage{ 136 Header: hdr, 137 Graph: createBugsGraph(c, bugs), 138 } 139 return serveTemplate(w, "graph_bugs.html", data) 140 } 141 142 // nolint: dupl 143 func handleGraphLifetimes(c context.Context, w http.ResponseWriter, r *http.Request) error { 144 hdr, err := commonHeader(c, r, w, "") 145 if err != nil { 146 return err 147 } 148 bugs, err := loadGraphBugs(c, hdr.Namespace) 149 if err != nil { 150 return err 151 } 152 153 var jobs []*Job 154 keys, err := db.NewQuery("Job"). 155 Filter("Namespace=", hdr.Namespace). 156 Filter("Type=", JobBisectCause). 157 GetAll(c, &jobs) 158 if err != nil { 159 return err 160 } 161 causeBisects := make(map[string]*Job) 162 for i, job := range jobs { 163 if len(job.Commits) != 1 { 164 continue 165 } 166 causeBisects[keys[i].Parent().StringID()] = job 167 } 168 data := &uiBugLifetimesPage{ 169 Header: hdr, 170 Lifetimes: createBugLifetimes(c, bugs, causeBisects), 171 } 172 return serveTemplate(w, "graph_lifetimes.html", data) 173 } 174 175 // nolint: dupl 176 func handleFoundBugsGraph(c context.Context, w http.ResponseWriter, r *http.Request) error { 177 hdr, err := commonHeader(c, r, w, "") 178 if err != nil { 179 return err 180 } 181 bugs, err := loadStableGraphBugs(c, hdr.Namespace) 182 if err != nil { 183 return err 184 } 185 data := &uiHistogramPage{ 186 Title: hdr.Namespace + " bugs found per month", 187 Header: hdr, 188 Graph: createFoundBugs(c, bugs), 189 } 190 return serveTemplate(w, "graph_histogram.html", data) 191 } 192 193 func loadGraphBugs(c context.Context, ns string) ([]*Bug, error) { 194 filter := func(query *db.Query) *db.Query { 195 return query.Filter("Namespace=", ns) 196 } 197 bugs, _, err := loadAllBugs(c, filter) 198 if err != nil { 199 return nil, err 200 } 201 n := 0 202 fixes := make(map[string]bool) 203 lastReporting := getNsConfig(c, ns).lastActiveReporting() 204 for _, bug := range bugs { 205 if bug.Reporting[lastReporting].Reported.IsZero() { 206 // Bugs with fixing commits are considered public (see Bug.sanitizeAccess). 207 if bug.Status == BugStatusOpen && len(bug.Commits) == 0 { 208 // These bugs are not released yet. 209 continue 210 } 211 bugReporting := lastReportedReporting(bug) 212 if bugReporting == nil || bugReporting.Auto && bug.Status == BugStatusInvalid { 213 // These bugs were auto-obsoleted before getting released. 214 continue 215 } 216 } 217 dup := false 218 for _, com := range bug.Commits { 219 if fixes[com] { 220 dup = true 221 } 222 fixes[com] = true 223 } 224 if dup { 225 continue 226 } 227 bugs[n] = bug 228 n++ 229 } 230 return bugs[:n], nil 231 } 232 233 // loadStableGraphBugs is similar to loadGraphBugs, but it does not remove duplicates and auto-invalidated bugs. 234 // This ensures that the set of bugs does not change much over time. 235 func loadStableGraphBugs(c context.Context, ns string) ([]*Bug, error) { 236 filter := func(query *db.Query) *db.Query { 237 return query.Filter("Namespace=", ns) 238 } 239 bugs, _, err := loadAllBugs(c, filter) 240 if err != nil { 241 return nil, err 242 } 243 n := 0 244 lastReporting := getNsConfig(c, ns).lastActiveReporting() 245 for _, bug := range bugs { 246 if isStableBug(c, bug, lastReporting) { 247 bugs[n] = bug 248 n++ 249 } 250 } 251 return bugs[:n], nil 252 } 253 254 func isStableBug(c context.Context, bug *Bug, lastReporting int) bool { 255 // Bugs with fixing commits are considered public (see Bug.sanitizeAccess). 256 if !bug.Reporting[lastReporting].Reported.IsZero() || 257 bug.Status == BugStatusFixed || len(bug.Commits) != 0 { 258 return true 259 } 260 // Invalid/dup not in the final reporting. 261 if bug.Status != BugStatusOpen { 262 return false 263 } 264 // The bug is still open, but not in the final reporting. 265 // Check if it's delayed from reaching the final reporting only by embargo. 266 for i := range bug.Reporting { 267 if i == lastReporting { 268 return true 269 } 270 if !bug.Reporting[i].Closed.IsZero() { 271 continue 272 } 273 reporting := getNsConfig(c, bug.Namespace).ReportingByName(bug.Reporting[i].Name) 274 if !bug.Reporting[i].Reported.IsZero() && reporting.Embargo != 0 { 275 continue 276 } 277 if reporting.Filter(bug) == FilterSkip { 278 continue 279 } 280 return false 281 } 282 return false 283 } 284 285 func createBugsGraph(c context.Context, bugs []*Bug) *uiGraph { 286 type BugStats struct { 287 Opened int 288 Fixed int 289 Closed int 290 TotalReported int 291 TotalOpen int 292 TotalFixed int 293 TotalClosed int 294 } 295 const timeWeek = 30 * 24 * time.Hour 296 now := timeNow(c) 297 m := make(map[int]*BugStats) 298 maxWeek := 0 299 bugStatsFor := func(t time.Time) *BugStats { 300 week := max(0, int(now.Sub(t)/(30*24*time.Hour))) 301 maxWeek = max(maxWeek, week) 302 bs := m[week] 303 if bs == nil { 304 bs = new(BugStats) 305 m[week] = bs 306 } 307 return bs 308 } 309 for _, bug := range bugs { 310 bugStatsFor(bug.FirstTime).Opened++ 311 if !bug.Closed.IsZero() { 312 if bug.Status == BugStatusFixed { 313 bugStatsFor(bug.Closed).Fixed++ 314 } 315 bugStatsFor(bug.Closed).Closed++ 316 } else if len(bug.Commits) != 0 { 317 bugStatsFor(now).Fixed++ 318 bugStatsFor(now).Closed++ 319 } 320 } 321 var stats []BugStats 322 var prev BugStats 323 for i := maxWeek; i >= 0; i-- { 324 var bs BugStats 325 if p := m[i]; p != nil { 326 bs = *p 327 } 328 bs.TotalReported = prev.TotalReported + bs.Opened 329 bs.TotalFixed = prev.TotalFixed + bs.Fixed 330 bs.TotalClosed = prev.TotalClosed + bs.Closed 331 bs.TotalOpen = bs.TotalReported - bs.TotalClosed 332 stats = append(stats, bs) 333 prev = bs 334 } 335 var columns []uiGraphColumn 336 for week, bs := range stats { 337 col := uiGraphColumn{Hint: now.Add(time.Duration(week-len(stats)+1) * timeWeek).Format("Jan-06")} 338 col.Vals = append(col.Vals, uiGraphValue{Val: float32(bs.TotalOpen)}) 339 col.Vals = append(col.Vals, uiGraphValue{Val: float32(bs.TotalReported)}) 340 col.Vals = append(col.Vals, uiGraphValue{Val: float32(bs.TotalFixed)}) 341 // col.Vals = append(col.Vals, uiGraphValue{Val: float32(bs.Opened)}) 342 // col.Vals = append(col.Vals, uiGraphValue{Val: float32(bs.Fixed)}) 343 columns = append(columns, col) 344 } 345 return &uiGraph{ 346 Headers: []uiGraphHeader{{Name: "open bugs"}, {Name: "total reported"}, {Name: "total fixed"}}, 347 Columns: columns, 348 } 349 } 350 351 func createFoundBugs(c context.Context, bugs []*Bug) *uiGraph { 352 const projected = "projected" 353 // This is linux-specific at the moment, potentially can move to pkg/report/crash 354 // and extend to other OSes. 355 // nolint: lll 356 types := []struct { 357 name string 358 color string 359 pred crash.TypeGroupPred 360 }{ 361 {"KASAN", "Red", crash.Type.IsKASAN}, 362 {"KMSAN", "Gold", crash.Type.IsKMSAN}, 363 {"KCSAN", "Fuchsia", crash.Type.IsKCSAN}, 364 {"mem safety", "OrangeRed", crash.Type.IsMemSafety}, 365 {"mem leak", "MediumSeaGreen", crash.Type.IsMemoryLeak}, 366 {"locking", "DodgerBlue", crash.Type.IsLockingBug}, 367 {"hangs/stalls", "LightSalmon", crash.Type.IsHang}, 368 // This must be at the end, otherwise "BUG:" will match other error types. 369 {"DoS", "Violet", crash.Type.IsDoS}, 370 {"other", "Gray", func(crash.Type) bool { return true }}, 371 {projected, "LightGray", nil}, 372 } 373 var sorted []time.Time 374 months := make(map[time.Time]map[string]int) 375 for _, bug := range bugs { 376 for _, typ := range types { 377 if !typ.pred(report.TitleToCrashType(bug.Title)) { 378 continue 379 } 380 t := bug.FirstTime 381 m := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, time.UTC) 382 if months[m] == nil { 383 months[m] = make(map[string]int) 384 sorted = append(sorted, m) 385 } 386 months[m][typ.name]++ 387 break 388 } 389 } 390 sort.Slice(sorted, func(i, j int) bool { 391 return sorted[i].Before(sorted[j]) 392 }) 393 now := timeNow(c) 394 thisMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) 395 if m := months[thisMonth]; m != nil { 396 total := 0 397 for _, c := range m { 398 total += c 399 } 400 nextMonth := thisMonth.AddDate(0, 1, 0) 401 m[projected] = int(float64(total) / float64(now.Sub(thisMonth)) * float64(nextMonth.Sub(now))) 402 } 403 var headers []uiGraphHeader 404 for _, typ := range types { 405 headers = append(headers, uiGraphHeader{Name: typ.name, Color: typ.color}) 406 } 407 var columns []uiGraphColumn 408 for _, month := range sorted { 409 col := uiGraphColumn{Hint: month.Format("Jan-06")} 410 stats := months[month] 411 for _, typ := range types { 412 val := float32(stats[typ.name]) 413 col.Vals = append(col.Vals, uiGraphValue{Val: val}) 414 col.Annotation += val 415 } 416 columns = append(columns, col) 417 } 418 return &uiGraph{ 419 Headers: headers, 420 Columns: columns, 421 } 422 } 423 424 func createBugLifetimes(c context.Context, bugs []*Bug, causeBisects map[string]*Job) []uiBugLifetime { 425 var res []uiBugLifetime 426 for i, bug := range bugs { 427 if bug.Status >= BugStatusInvalid { 428 continue 429 } 430 reported := bug.FirstTime 431 // Find reporting date to the last reporting stage (where it was reported), 432 // it's a more meaningful date b/c the bug could not be fixed before that. 433 for _, reporting := range bug.Reporting { 434 if reported.Before(reporting.Reported) { 435 reported = reporting.Reported 436 } 437 } 438 ui := uiBugLifetime{} 439 fixed := bug.FixTime 440 if fixed.IsZero() || bug.Status == BugStatusFixed && bug.Closed.Before(fixed) { 441 fixed = bug.Closed 442 } 443 fixedDaysAgo := max(0, float32(fixed.Sub(reported))/float32(24*time.Hour)) 444 if !fixed.IsZero() { 445 // We use fixed date as the X coordiate (date) for fixed bugs to show 446 // how lifetime of bugs fixed at the given date (e.g. lately). 447 ui.Reported = fixed 448 if fixedDaysAgo > 365 { 449 // Cap Y coordinate to 365 to make Y coordinates with the first year 450 // distinguishable in presence of very large Y coordinates 451 // (e.g. if something was fixed/introduced in 5 years). 452 // Add small jitter to min/max values to make close dots distinguishable. 453 ui.Fixed1y = 365 + float32(i%7) 454 } else { 455 ui.Fixed = max(0.2*(1+float32(i%5)), fixedDaysAgo) 456 } 457 } else { 458 ui.Reported = reported 459 ui.NotFixed = 400 - float32(i%10) 460 } 461 res = append(res, ui) 462 ui = uiBugLifetime{ 463 Reported: reported, 464 } 465 if job := causeBisects[bug.keyHash(c)]; job != nil { 466 days := float32(job.Commits[0].Date.Sub(ui.Reported)) / float32(24*time.Hour) 467 if days < -365 { 468 ui.Introduced1y = -365 - float32(i%7) 469 } else { 470 ui.Introduced = min(-0.2*(1+float32(i%5)), days) 471 } 472 } 473 res = append(res, ui) 474 } 475 return res 476 } 477 478 func handleGraphFuzzing(c context.Context, w http.ResponseWriter, r *http.Request) error { 479 hdr, err := commonHeader(c, r, w, "") 480 if err != nil { 481 return err 482 } 483 r.ParseForm() 484 485 allManagers, err := managerList(c, hdr.Namespace) 486 if err != nil { 487 return err 488 } 489 managers, err := createCheckBox(r, "Instances", allManagers) 490 if err != nil { 491 return err 492 } 493 metrics, err := createCheckBox(r, "Metrics", []string{ 494 "MaxCorpus", "MaxCover", "MaxPCs", "TotalFuzzingTime", 495 "TotalCrashes", "CrashTypes", "SuppressedCrashes", "TotalExecs", 496 "ExecsPerSec", "TriagedPCs", "TriagedCoverage"}) 497 if err != nil { 498 return err 499 } 500 data := &uiManagersPage{ 501 Header: hdr, 502 Managers: managers, 503 Metrics: metrics, 504 Months: createSlider(r, "Months", 1, 36), 505 } 506 data.Graph, err = createManagersGraph(c, hdr.Namespace, data.Managers.vals, data.Metrics.vals, data.Months.Val*30) 507 if err != nil { 508 return err 509 } 510 return serveTemplate(w, "graph_fuzzing.html", data) 511 } 512 513 func createManagersGraph(c context.Context, ns string, selManagers, selMetrics []string, days int) (*uiGraph, error) { 514 graph := &uiGraph{} 515 for _, mgr := range selManagers { 516 for _, metric := range selMetrics { 517 graph.Headers = append(graph.Headers, uiGraphHeader{Name: mgr + "-" + metric}) 518 } 519 } 520 now := timeNow(c) 521 const day = 24 * time.Hour 522 // Step 1: fill the whole table with empty values to simplify subsequent logic 523 // when we fill random positions in the table. 524 for date := 0; date <= days; date++ { 525 col := uiGraphColumn{Hint: now.Add(time.Duration(date-days) * day).Format("02-01-2006")} 526 for range selManagers { 527 for range selMetrics { 528 col.Vals = append(col.Vals, uiGraphValue{Hint: "-"}) 529 } 530 } 531 graph.Columns = append(graph.Columns, col) 532 } 533 // Step 2: fill in actual data. 534 for mgrIndex, mgr := range selManagers { 535 parentKey := mgrKey(c, ns, mgr) 536 var stats []*ManagerStats 537 _, err := db.NewQuery("ManagerStats"). 538 Ancestor(parentKey). 539 GetAll(c, &stats) 540 if err != nil { 541 return nil, err 542 } 543 for _, stat := range stats { 544 dayIndex := days - int(now.Sub(dateTime(stat.Date))/day) 545 if dayIndex < 0 || dayIndex > days { 546 continue 547 } 548 for metricIndex, metric := range selMetrics { 549 val, canBeZero := extractMetric(stat, metric) 550 graph.Columns[dayIndex].Vals[mgrIndex*len(selMetrics)+metricIndex] = uiGraphValue{ 551 Val: float32(val), 552 IsNull: !canBeZero && val == 0, 553 Hint: fmt.Sprintf("%.2f", val), 554 } 555 } 556 } 557 } 558 // Step 3: normalize data to [0..100] range. 559 // We visualize radically different values and they all should fit into a single graph. 560 // We normalize the same metric across all managers so that a single metric is still 561 // comparable across different managers. 562 if len(selMetrics) > 1 { 563 for metricIndex := range selMetrics { 564 maxVal := float32(1) 565 for col := range graph.Columns { 566 for mgrIndex := range selManagers { 567 item := graph.Columns[col].Vals[mgrIndex*len(selMetrics)+metricIndex] 568 if item.IsNull { 569 continue 570 } 571 maxVal = max(maxVal, item.Val) 572 } 573 } 574 for col := range graph.Columns { 575 for mgrIndex := range selManagers { 576 graph.Columns[col].Vals[mgrIndex*len(selMetrics)+metricIndex].Val /= maxVal * 100 577 } 578 } 579 } 580 } 581 return graph, nil 582 } 583 584 func extractMetric(stat *ManagerStats, metric string) (val float64, canBeZero bool) { 585 switch metric { 586 case "MaxCorpus": 587 return float64(stat.MaxCorpus), false 588 case "MaxCover": 589 return float64(stat.MaxCover), false 590 case "MaxPCs": 591 return float64(stat.MaxPCs), false 592 case "TotalFuzzingTime": 593 return float64(stat.TotalFuzzingTime), true 594 case "TotalCrashes": 595 return float64(stat.TotalCrashes), true 596 case "CrashTypes": 597 return float64(stat.CrashTypes), true 598 case "SuppressedCrashes": 599 return float64(stat.SuppressedCrashes), true 600 case "TotalExecs": 601 return float64(stat.TotalExecs), true 602 case "ExecsPerSec": 603 timeSec := float64(stat.TotalFuzzingTime) / 1e9 604 if timeSec == 0 { 605 return 0, true 606 } 607 return float64(stat.TotalExecs) / timeSec, true 608 case "TriagedCoverage": 609 return float64(stat.TriagedCoverage), false 610 case "TriagedPCs": 611 return float64(stat.TriagedPCs), false 612 default: 613 panic(fmt.Sprintf("unknown metric %q", metric)) 614 } 615 } 616 617 // createCheckBox additionally validates r.Form data and returns 400 error in case of mismatch. 618 func createCheckBox(r *http.Request, caption string, values []string) (*uiCheckbox, error) { 619 // TODO: turn this into proper ID that can be used in HTML. 620 id := caption 621 for _, formVal := range r.Form[id] { 622 if !stringInList(values, formVal) { 623 return nil, ErrClientBadRequest 624 } 625 } 626 ui := &uiCheckbox{ 627 ID: id, 628 Caption: caption, 629 vals: r.Form[id], 630 } 631 if len(ui.vals) == 0 { 632 ui.vals = []string{values[0]} 633 } 634 for _, val := range values { 635 ui.Values = append(ui.Values, &uiCheckboxValue{ 636 ID: val, 637 // TODO: use this as caption and form ID. 638 Selected: stringInList(ui.vals, val), 639 }) 640 } 641 return ui, nil 642 } 643 644 func createSlider(r *http.Request, caption string, min, max int) *uiSlider { 645 // TODO: turn this into proper ID that can be used in HTML. 646 id := caption 647 ui := &uiSlider{ 648 ID: id, 649 Caption: caption, 650 Val: min, 651 Min: min, 652 Max: max, 653 } 654 if val, _ := strconv.Atoi(r.FormValue(id)); val >= min && val <= max { 655 ui.Val = val 656 } 657 return ui 658 } 659 660 func createMultiInput(r *http.Request, id, caption string) *uiMultiInput { 661 filteredValues := []string{} 662 for _, val := range r.Form[id] { 663 if val == "" { 664 continue 665 } 666 filteredValues = append(filteredValues, val) 667 } 668 return &uiMultiInput{ 669 ID: id, 670 Caption: caption, 671 Vals: filteredValues, 672 } 673 } 674 675 func handleGraphCrashes(c context.Context, w http.ResponseWriter, r *http.Request) error { 676 hdr, err := commonHeader(c, r, w, "") 677 if err != nil { 678 return err 679 } 680 r.ParseForm() 681 682 data := &uiCrashesPage{ 683 Header: hdr, 684 Regexps: createMultiInput(r, "regexp", "Regexps"), 685 GraphMonths: createSlider(r, "Months", 1, 36), 686 TableDays: createSlider(r, "Days", 1, 30), 687 } 688 689 bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { 690 return query.Filter("Namespace=", hdr.Namespace) 691 }) 692 if err != nil { 693 return err 694 } 695 accessLevel := accessLevel(c, r) 696 nbugs := 0 697 for _, bug := range bugs { 698 if accessLevel < bug.sanitizeAccess(c, accessLevel) { 699 continue 700 } 701 bugs[nbugs] = bug 702 nbugs++ 703 } 704 bugs = bugs[:nbugs] 705 if len(data.Regexps.Vals) == 0 { 706 // If no data is passed, then at least show the graph for important crash types. 707 data.Regexps.Vals = []string{"^KASAN", "^KMSAN", "^KCSAN", "^SYZFAIL"} 708 } 709 if r.Form["show-graph"] != nil { 710 data.Graph, err = createCrashesGraph(c, hdr.Namespace, data.Regexps.Vals, data.GraphMonths.Val*30, bugs) 711 if err != nil { 712 return err 713 } 714 } else { 715 data.Table = createCrashesTable(c, hdr.Namespace, data.TableDays.Val, bugs) 716 } 717 return serveTemplate(w, "graph_crashes.html", data) 718 } 719 720 func createCrashesTable(c context.Context, ns string, days int, bugs []*Bug) *uiCrashPageTable { 721 const dayDuration = 24 * time.Hour 722 startDay := timeNow(c).Add(time.Duration(-days+1) * dayDuration) 723 table := &uiCrashPageTable{ 724 Title: fmt.Sprintf("Top crashers of the last %d day(s)", days), 725 } 726 totalCount := 0 727 for _, bug := range bugs { 728 count := 0 729 for _, s := range bug.dailyStatsTail(startDay) { 730 count += s.CrashCount 731 } 732 if count == 0 { 733 continue 734 } 735 totalCount += count 736 titleRegexp := regexp.QuoteMeta(bug.Title) 737 table.Rows = append(table.Rows, &uiCrashSummary{ 738 Title: bug.Title, 739 Link: bugLink(bug.keyHash(c)), 740 GraphLink: "?show-graph=1&Months=1®exp=" + url.QueryEscape(titleRegexp), 741 Count: count, 742 }) 743 } 744 for id := range table.Rows { 745 table.Rows[id].Share = float32(table.Rows[id].Count) / float32(totalCount) * 100.0 746 } 747 // Order by descending crash count. 748 sort.SliceStable(table.Rows, func(i, j int) bool { 749 return table.Rows[i].Count > table.Rows[j].Count 750 }) 751 return table 752 } 753 754 func createCrashesGraph(c context.Context, ns string, regexps []string, days int, bugs []*Bug) (*uiGraph, error) { 755 const dayDuration = 24 * time.Hour 756 graph := &uiGraph{} 757 for _, re := range regexps { 758 graph.Headers = append(graph.Headers, uiGraphHeader{Name: re}) 759 } 760 startDay := timeNow(c).Add(time.Duration(-days) * dayDuration) 761 // Step 1: fill the whole table with empty values. 762 dateToIdx := make(map[int]int) 763 for date := 0; date <= days; date++ { 764 day := startDay.Add(time.Duration(date) * dayDuration) 765 dateToIdx[timeDate(day)] = date 766 col := uiGraphColumn{Hint: day.Format("02-01-2006")} 767 for range regexps { 768 col.Vals = append(col.Vals, uiGraphValue{Hint: "-"}) 769 } 770 graph.Columns = append(graph.Columns, col) 771 } 772 // Step 2: fill in crash counts. 773 totalCounts := make(map[int]int) 774 for _, bug := range bugs { 775 for _, stat := range bug.dailyStatsTail(startDay) { 776 pos, ok := dateToIdx[stat.Date] 777 if !ok { 778 continue 779 } 780 totalCounts[pos] += stat.CrashCount 781 } 782 } 783 for regexpID, val := range regexps { 784 r, err := regexp.Compile(val) 785 if err != nil { 786 return nil, err 787 } 788 for _, bug := range bugs { 789 if !r.MatchString(bug.Title) { 790 continue 791 } 792 for _, stat := range bug.DailyStats { 793 pos, ok := dateToIdx[stat.Date] 794 if !ok { 795 continue 796 } 797 graph.Columns[pos].Vals[regexpID].Val += float32(stat.CrashCount) 798 } 799 } 800 } 801 // Step 3: convert abs values to percents. 802 for date := 0; date <= days; date++ { 803 count := totalCounts[date] 804 if count == 0 { 805 continue 806 } 807 for id := range graph.Columns[date].Vals { 808 value := &graph.Columns[date].Vals[id] 809 abs := value.Val 810 value.Val = abs / float32(count) * 100.0 811 value.Hint = fmt.Sprintf("%.1f%% (%.0f)", value.Val, abs) 812 } 813 } 814 return graph, nil 815 }