aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2023-08-16 17:52:41 +0200
committerAleksandr Nogikh <nogikh@google.com>2023-10-09 10:22:42 +0000
commit28021a51a672ec3f58e4e738005baaa98676521b (patch)
tree23d7d8e71cdf2e6ebbd6a1ee6d50e721aae6f02b
parent22d57370e2f9ac0581d3a6257030c79b3d758855 (diff)
dashboard: export bugs for stats instead of analysis
Building a proper stats analysis tool is definitely out of syzkaller dashboard's scope. Delete the code that did not turn out to be useful in practice and replace it with an export of bug info relevant for statistics.
-rw-r--r--dashboard/app/main.go84
-rw-r--r--dashboard/app/stats.go285
-rw-r--r--dashboard/app/stats_test.go66
-rw-r--r--pkg/stats/syzbotstats/bug.go29
4 files changed, 139 insertions, 325 deletions
diff --git a/dashboard/app/main.go b/dashboard/app/main.go
index 229eb29bd..2a4baa69b 100644
--- a/dashboard/app/main.go
+++ b/dashboard/app/main.go
@@ -5,6 +5,7 @@ package main
import (
"bytes"
+ "encoding/json"
"fmt"
"html/template"
"net/http"
@@ -14,7 +15,6 @@ import (
"sort"
"strconv"
"strings"
- "text/tabwriter"
"time"
"cloud.google.com/go/logging"
@@ -63,7 +63,7 @@ func initHTTPHandlers() {
http.Handle("/"+ns+"/graph/fuzzing", handlerWrapper(handleGraphFuzzing))
http.Handle("/"+ns+"/graph/crashes", handlerWrapper(handleGraphCrashes))
http.Handle("/"+ns+"/repos", handlerWrapper(handleRepos))
- http.Handle("/"+ns+"/bug-stats", handlerWrapper(handleBugStats))
+ http.Handle("/"+ns+"/bug-summaries", handlerWrapper(handleBugSummaries))
http.Handle("/"+ns+"/subsystems", handlerWrapper(handleSubsystemsList))
http.Handle("/"+ns+"/backports", handlerWrapper(handleBackports))
http.Handle("/"+ns+"/s/", handlerWrapper(handleSubsystemPage))
@@ -1275,7 +1275,7 @@ func getBugDiscussionsUI(c context.Context, bug *Bug) ([]*uiBugDiscussion, error
return list, nil
}
-func handleBugStats(c context.Context, w http.ResponseWriter, r *http.Request) error {
+func handleBugSummaries(c context.Context, w http.ResponseWriter, r *http.Request) error {
if accessLevel(c, r) != AccessAdmin {
return fmt.Errorf("admin only")
}
@@ -1283,78 +1283,16 @@ func handleBugStats(c context.Context, w http.ResponseWriter, r *http.Request) e
if err != nil {
return err
}
- inputs, err := allBugInputs(c, hdr.Namespace)
- if err != nil {
- return fmt.Errorf("failed to query bugs: %w", err)
- }
-
- const days = 100
- reports := []struct {
- name string
- stat stats
- since time.Time
- }{
- {
- name: "Effect of strace among bugs with repro since May 2022",
- stat: newStatsFilter(newStraceEffect(days), bugsHaveRepro),
- since: time.Date(2022, 5, 1, 0, 0, 0, 0, time.UTC),
- },
- {
- name: "Effect of reproducer since May 2022",
- stat: newReproEffect(days),
- since: time.Date(2022, 5, 1, 0, 0, 0, 0, time.UTC),
- },
- {
- name: "Effect of reproducer since 2020",
- stat: newReproEffect(days),
- since: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
- },
- {
- name: "Effect of the presence of build assets since Oct 2022",
- stat: newAssetEffect(days),
- since: time.Date(2022, 10, 1, 0, 0, 0, 0, time.UTC),
- },
- {
- name: "Effect of cause bisection among bugs with repro since 2022",
- stat: newStatsFilter(newBisectCauseEffect(days), bugsHaveRepro),
- since: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
- },
- }
-
- // Set up common filters.
- allReportings := config.Namespaces[hdr.Namespace].Reporting
- commonFilters := []statsFilter{
- bugsNoLater(timeNow(c), days), // yet make sure bugs are old enough
- bugsInReportingStage(allReportings[len(allReportings)-1].Name), // only bugs from the last stage
+ stage := r.FormValue("stage")
+ if stage == "" {
+ return fmt.Errorf("stage must be specified")
}
- for i, report := range reports {
- reports[i].stat = newStatsFilter(report.stat, commonFilters...)
- reports[i].stat = newStatsFilter(reports[i].stat, bugsNoEarlier(report.since))
- }
-
- // Prepare the data.
- for _, input := range inputs {
- for _, report := range reports {
- report.stat.Record(input)
- }
- }
- // Print the results.
- w.Header().Set("Content-Type", "text/plain; charset=utf-8")
- for _, report := range reports {
- fmt.Fprintf(w, "%s\n==============\n", report.name)
- wTab := tabwriter.NewWriter(w, 0, 0, 1, ' ', tabwriter.Debug)
- table := report.stat.Collect().([][]string)
- for _, row := range table {
- for _, cell := range row {
- fmt.Fprintf(wTab, "%s\t", cell)
- }
- fmt.Fprintf(wTab, "\n")
- }
- wTab.Flush()
- fmt.Fprintf(w, "\n\n")
+ list, err := getBugSummaries(c, hdr.Namespace, stage)
+ if err != nil {
+ return err
}
-
- return nil
+ w.Header().Set("Content-Type", "application/json")
+ return json.NewEncoder(w).Encode(list)
}
func isJSONRequested(request *http.Request) bool {
diff --git a/dashboard/app/stats.go b/dashboard/app/stats.go
index 9d1ecbea9..adcb24643 100644
--- a/dashboard/app/stats.go
+++ b/dashboard/app/stats.go
@@ -4,44 +4,17 @@
package main
import (
+ "errors"
"fmt"
"time"
"github.com/google/syzkaller/dashboard/dashapi"
+ "github.com/google/syzkaller/pkg/stats/syzbotstats"
"golang.org/x/net/context"
+ "google.golang.org/appengine/v2"
db "google.golang.org/appengine/v2/datastore"
)
-type stats interface {
- Record(input *bugInput)
- Collect() interface{}
-}
-
-// statsFilterStruct allows to embed input filtering to stats collection.
-type statsFilterStruct struct {
- nested stats
- filters []statsFilter
-}
-
-type statsFilter func(input *bugInput) bool
-
-func newStatsFilter(nested stats, filters ...statsFilter) stats {
- return &statsFilterStruct{nested: nested, filters: filters}
-}
-
-func (sf *statsFilterStruct) Record(input *bugInput) {
- for _, filter := range sf.filters {
- if !filter(input) {
- return
- }
- }
- sf.nested.Record(input)
-}
-
-func (sf *statsFilterStruct) Collect() interface{} {
- return sf.nested.Collect()
-}
-
// bugInput structure contains the information for collecting all bug-related statistics.
type bugInput struct {
bug *Bug
@@ -50,17 +23,6 @@ type bugInput struct {
build *Build
}
-func (bi *bugInput) foundAt() time.Time {
- return bi.bug.FirstTime
-}
-
-func (bi *bugInput) reportedAt() time.Time {
- if bi.bugReporting == nil {
- return time.Time{}
- }
- return bi.bugReporting.Reported
-}
-
func (bi *bugInput) fixedAt() time.Time {
closeTime := time.Time{}
if bi.bug.Status == BugStatusFixed {
@@ -74,53 +36,22 @@ func (bi *bugInput) fixedAt() time.Time {
return closeTime
}
-type statsBugState int
-
-const (
- stateOpen statsBugState = iota
- stateDecisionMade
- stateAutoInvalidated
-)
-
-func (bi *bugInput) stateAt(date time.Time) statsBugState {
- bug := bi.bug
- closeTime := bug.Closed
- closeStatus := stateDecisionMade
- if at := bi.fixedAt(); !at.IsZero() {
- closeTime = at
- } else if bug.Status == BugStatusInvalid {
+func (bi *bugInput) bugStatus() (syzbotstats.BugStatus, error) {
+ if bi.bug.Status == BugStatusFixed ||
+ bi.bug.Closed.IsZero() && len(bi.bug.Commits) > 0 {
+ return syzbotstats.BugFixed, nil
+ } else if bi.bug.Closed.IsZero() {
+ return syzbotstats.BugPending, nil
+ } else if bi.bug.Status == BugStatusDup {
+ return syzbotstats.BugDup, nil
+ } else if bi.bug.Status == BugStatusInvalid {
if bi.bugReporting.Auto {
- closeStatus = stateAutoInvalidated
+ return syzbotstats.BugAutoInvalidated, nil
+ } else {
+ return syzbotstats.BugInvalidated, nil
}
}
- if closeTime.IsZero() || date.Before(closeTime) {
- return stateOpen
- }
- return closeStatus
-}
-
-// Some common bug input filters.
-
-func bugsNoEarlier(since time.Time) statsFilter {
- return func(input *bugInput) bool {
- return input.reportedAt().After(since)
- }
-}
-
-func bugsNoLater(now time.Time, days int) statsFilter {
- return func(input *bugInput) bool {
- return now.Sub(input.foundAt()) > time.Hour*24*time.Duration(days)
- }
-}
-
-func bugsInReportingStage(name string) statsFilter {
- return func(input *bugInput) bool {
- return input.bugReporting.Name == name
- }
-}
-
-func bugsHaveRepro(input *bugInput) bool {
- return input.bug.ReproLevel > 0
+ return "", fmt.Errorf("cannot determine status")
}
// allBugInputs queries the raw data about all bugs from a namespace.
@@ -154,10 +85,8 @@ func allBugInputs(c context.Context, ns string) ([]*bugInput, error) {
buildToInput := map[*db.Key]*bugInput{}
if len(crashKeys) > 0 {
crashes := make([]*Crash, len(crashKeys))
- if err := getAllMulti(c, crashKeys, func(i, j int) interface{} {
- return crashes[i:j]
- }); err != nil {
- return nil, fmt.Errorf("failed to fetch crashes: %w", err)
+ if badKey, err := getAllMulti(c, crashKeys, crashes); err != nil {
+ return nil, fmt.Errorf("failed to fetch crashes for %v: %w", badKey, err)
}
for i, crash := range crashes {
if crash == nil {
@@ -174,10 +103,8 @@ func allBugInputs(c context.Context, ns string) ([]*bugInput, error) {
// Fetch builds.
if len(buildKeys) > 0 {
builds := make([]*Build, len(buildKeys))
- if err := getAllMulti(c, buildKeys, func(i, j int) interface{} {
- return builds[i:j]
- }); err != nil {
- return nil, fmt.Errorf("failed to fetch builds: %w", err)
+ if badKey, err := getAllMulti(c, buildKeys, builds); err != nil {
+ return nil, fmt.Errorf("failed to fetch builds for %v: %w", badKey, err)
}
for i, build := range builds {
if build != nil {
@@ -188,110 +115,96 @@ func allBugInputs(c context.Context, ns string) ([]*bugInput, error) {
return inputs, nil
}
-func getAllMulti(c context.Context, key []*db.Key, getDst func(from, to int) interface{}) error {
- // Circumventing the datastore multi query limitation.
+// Circumventing the datastore's multi query limitation.
+func getAllMulti[T any](c context.Context, keys []*db.Key, objects []*T) (*db.Key, error) {
const step = 1000
- for from := 0; from < len(key); from += step {
+ for from := 0; from < len(keys); from += step {
to := from + step
- if to > len(key) {
- to = len(key)
+ if to > len(keys) {
+ to = len(keys)
}
- if err := db.GetMulti(c, key[from:to], getDst(from, to)); err != nil {
- return err
+ err := db.GetMulti(c, keys[from:to], objects[from:to])
+ if err == nil {
+ continue
}
+ var merr appengine.MultiError
+ if errors.As(err, &merr) {
+ for i, objErr := range merr {
+ if objErr != nil {
+ return keys[from+i], objErr
+ }
+ }
+ }
+ return nil, err
}
- return nil
-}
-
-type statsCounter struct {
- total int
- match int
-}
-
-func (sc *statsCounter) Record(match bool) {
- sc.total++
- if match {
- sc.match++
- }
-}
-
-func (sc statsCounter) String() string {
- percent := 0.0
- if sc.total != 0 {
- percent = float64(sc.match) / float64(sc.total) * 100.0
- }
- return fmt.Sprintf("%.2f%% (%d/%d)", percent, sc.match, sc.total)
-}
-
-// reactionFactor represents the generic stats collector that measures the effect
-// of a single variable on the how it affected the chances of the bug status
-// becoming statusDecisionMade in `days` days after reporting.
-type reactionFactor struct {
- factorTrue statsCounter
- factorFalse statsCounter
- days int
- factorName string
- factor statsFilter
-}
-
-func newReactionFactor(days int, name string, factor statsFilter) *reactionFactor {
- return &reactionFactor{
- days: days,
- factorName: name,
- factor: factor,
- }
-}
-
-func (rf *reactionFactor) Record(input *bugInput) {
- reported := input.reportedAt()
- state := input.stateAt(reported.Add(time.Hour * time.Duration(24*rf.days)))
- match := state == stateDecisionMade
- if rf.factor(input) {
- rf.factorTrue.Record(match)
- } else {
- rf.factorFalse.Record(match)
- }
+ return nil, nil
}
-func (rf *reactionFactor) Collect() interface{} {
- return [][]string{
- {"", rf.factorName, "No " + rf.factorName},
- {
- fmt.Sprintf("Resolved in %d days", rf.days),
- rf.factorTrue.String(),
- rf.factorFalse.String(),
- },
+// getBugSummaries extracts the list of BugStatSummary objects among bugs
+// that reached the specific reporting stage.
+func getBugSummaries(c context.Context, ns, stage string) ([]*syzbotstats.BugStatSummary, error) {
+ inputs, err := allBugInputs(c, ns)
+ if err != nil {
+ return nil, err
}
-}
-
-// Some common factors affecting the attention to the bug.
-
-func newStraceEffect(days int) *reactionFactor {
- return newReactionFactor(days, "Strace", func(bi *bugInput) bool {
- if bi.reportedCrash == nil {
- return false
+ var ret []*syzbotstats.BugStatSummary
+ for _, input := range inputs {
+ bug, crash := input.bug, input.reportedCrash
+ if crash == nil {
+ continue
+ }
+ targetStage := bugReportingByName(bug, stage)
+ if targetStage == nil || targetStage.Closed.IsZero() {
+ continue
+ }
+ obj := &syzbotstats.BugStatSummary{
+ Title: bug.Title,
+ ReleasedTime: targetStage.Closed,
+ ResolvedTime: bug.Closed,
+ Strace: dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0,
+ }
+ for _, stage := range bug.Reporting {
+ if stage.ID != "" {
+ obj.IDs = append(obj.IDs, stage.ID)
+ }
+ }
+ if crash.ReproSyz > 0 {
+ obj.ReproTime = crash.Time
+ }
+ if bug.BisectCause == BisectYes {
+ causeBisect, err := queryBestBisection(c, bug, JobBisectCause)
+ if err != nil {
+ return nil, err
+ }
+ if causeBisect != nil {
+ obj.CauseBisectTime = causeBisect.job.Finished
+ }
+ }
+ fixTime := input.fixedAt()
+ if !fixTime.IsZero() && (obj.ResolvedTime.IsZero() || fixTime.Before(obj.ResolvedTime)) {
+ // Take the date of the fixing commit, if it's earlier.
+ obj.ResolvedTime = fixTime
+ }
+ obj.Status, err = input.bugStatus()
+ if err != nil {
+ return nil, fmt.Errorf("%s: %w", bug.Title, err)
}
- return dashapi.CrashFlags(bi.reportedCrash.Flags)&dashapi.CrashUnderStrace > 0
- })
-}
-func newReproEffect(days int) *reactionFactor {
- return newReactionFactor(days, "Repro", func(bi *bugInput) bool {
- return bi.bug.ReproLevel > 0
- })
-}
+ const minAvgHitCrashes = 5
+ const minAvgHitPeriod = time.Hour * 24
+ if bug.NumCrashes >= minAvgHitCrashes ||
+ bug.LastTime.Sub(bug.FirstTime) < minAvgHitPeriod {
+ // If there are only a few crashes or they all happened within a single day,
+ // it's hard to make any accurate frequency estimates.
+ timeSpan := bug.LastTime.Sub(bug.FirstTime)
+ obj.HitsPerDay = float64(bug.NumCrashes) / (timeSpan.Hours() / 24)
+ }
-func newAssetEffect(days int) *reactionFactor {
- return newReactionFactor(days, "Build Assets", func(bi *bugInput) bool {
- if bi.build == nil {
- return false
+ for _, label := range bug.LabelValues(SubsystemLabel) {
+ obj.Subsystems = append(obj.Subsystems, label.Value)
}
- return len(bi.build.Assets) > 0
- })
-}
-func newBisectCauseEffect(days int) *reactionFactor {
- return newReactionFactor(days, "Successful Cause Bisection", func(bi *bugInput) bool {
- return bi.bug.BisectCause == BisectYes
- })
+ ret = append(ret, obj)
+ }
+ return ret, nil
}
diff --git a/dashboard/app/stats_test.go b/dashboard/app/stats_test.go
deleted file mode 100644
index 5e2ca9fe8..000000000
--- a/dashboard/app/stats_test.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2022 syzkaller project authors. All rights reserved.
-// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
-
-package main
-
-import (
- "testing"
- "time"
-
- "github.com/google/syzkaller/dashboard/dashapi"
- "github.com/stretchr/testify/assert"
-)
-
-func TestStraceEffect(t *testing.T) {
- c := NewCtx(t)
- defer c.Close()
-
- client := c.publicClient
- build := testBuild(1)
- client.UploadBuild(build)
-
- // A bug with strace.
- crashStrace := testCrashWithRepro(build, 1)
- crashStrace.Flags = dashapi.CrashUnderStrace
- crashStrace.Report = []byte("with strace")
- client.ReportCrash(crashStrace)
- msg1 := c.pollEmailBug()
-
- // Invalidate it.
- c.advanceTime(time.Hour * 24)
- c.incomingEmail(msg1.Sender, "#syz invalid")
-
- // Two bugs without strace.
-
- c.advanceTime(time.Hour * 24 * 365)
- crash := testCrash(build, 2)
- client.ReportCrash(crash)
- msg2 := client.pollEmailBug()
-
- crash = testCrash(build, 3)
- client.ReportCrash(crash)
- msg3 := c.pollEmailBug()
-
- // Invalidate one of them quickly.
- c.advanceTime(time.Hour)
- c.incomingEmail(msg2.Sender, "#syz invalid")
-
- // And the other one later.
- c.advanceTime(time.Hour * 24 * 101)
- c.incomingEmail(msg3.Sender, "#syz invalid")
-
- // Query the stats.
- inputs, err := allBugInputs(c.ctx, "access-public-email")
- c.expectOK(err)
-
- stats := newStraceEffect(100)
- for _, input := range inputs {
- stats.Record(input)
- }
-
- ret := stats.Collect()
- assert.Equal(t, ret, [][]string{
- {"", "Strace", "No Strace"},
- {"Resolved in 100 days", "100.00% (1/1)", "50.00% (1/2)"},
- }, "invalid strace stats results")
-}
diff --git a/pkg/stats/syzbotstats/bug.go b/pkg/stats/syzbotstats/bug.go
new file mode 100644
index 000000000..11e97fac1
--- /dev/null
+++ b/pkg/stats/syzbotstats/bug.go
@@ -0,0 +1,29 @@
+// Copyright 2023 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package syzbotstats
+
+import "time"
+
+type BugStatSummary struct {
+ Title string
+ IDs []string // IDs used by syzbot for this bug.
+ ReleasedTime time.Time // When the bug was published.
+ ReproTime time.Time // When we found the reproducer.
+ CauseBisectTime time.Time // When we found cause bisection.
+ ResolvedTime time.Time // When the bug was resolved.
+ Status BugStatus
+ Subsystems []string
+ Strace bool // Whether we managed to reproduce under strace.
+ HitsPerDay float64 // Average number of bug hits per day.
+}
+
+type BugStatus string
+
+const (
+ BugFixed BugStatus = "fixed"
+ BugInvalidated BugStatus = "invalidated"
+ BugAutoInvalidated BugStatus = "auto-invalidated"
+ BugDup BugStatus = "dup"
+ BugPending BugStatus = "pending"
+)