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&regexp=" + 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  }