github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/dashboard/app/stats.go (about) 1 // Copyright 2022 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 "errors" 9 "fmt" 10 "time" 11 12 "github.com/google/syzkaller/dashboard/dashapi" 13 "github.com/google/syzkaller/pkg/stat/syzbotstats" 14 "google.golang.org/appengine/v2" 15 db "google.golang.org/appengine/v2/datastore" 16 ) 17 18 // bugInput structure contains the information for collecting all bug-related statistics. 19 type bugInput struct { 20 bug *Bug 21 bugReporting *BugReporting 22 reportedCrash *Crash 23 build *Build 24 } 25 26 func (bi *bugInput) fixedAt() time.Time { 27 closeTime := time.Time{} 28 if bi.bug.Status == BugStatusFixed { 29 closeTime = bi.bug.Closed 30 } 31 for _, commit := range bi.bug.CommitInfo { 32 if closeTime.IsZero() || closeTime.After(commit.Date) { 33 closeTime = commit.Date 34 } 35 } 36 return closeTime 37 } 38 39 func (bi *bugInput) bugStatus() (syzbotstats.BugStatus, error) { 40 if bi.bug.Status == BugStatusFixed || 41 bi.bug.Closed.IsZero() && len(bi.bug.Commits) > 0 { 42 return syzbotstats.BugFixed, nil 43 } else if bi.bug.Closed.IsZero() { 44 return syzbotstats.BugPending, nil 45 } else if bi.bug.Status == BugStatusDup { 46 return syzbotstats.BugDup, nil 47 } else if bi.bug.Status == BugStatusInvalid { 48 if bi.bugReporting.Auto { 49 return syzbotstats.BugAutoInvalidated, nil 50 } else { 51 return syzbotstats.BugInvalidated, nil 52 } 53 } 54 return "", fmt.Errorf("cannot determine status") 55 } 56 57 // allBugInputs queries the raw data about all bugs from a namespace. 58 func allBugInputs(c context.Context, ns string) ([]*bugInput, error) { 59 filter := func(query *db.Query) *db.Query { 60 return query.Filter("Namespace=", ns) 61 } 62 inputs := []*bugInput{} 63 bugs, bugKeys, err := loadAllBugs(c, filter) 64 if err != nil { 65 return nil, err 66 } 67 crashLoader := &dependencyLoader[Crash]{} 68 for i, bug := range bugs { 69 bugReporting := lastReportedReporting(bug) 70 input := &bugInput{ 71 bug: bug, 72 bugReporting: bugReporting, 73 } 74 if bugReporting.CrashID != 0 { 75 crashKey := db.NewKey(c, "Crash", "", bugReporting.CrashID, bugKeys[i]) 76 crashLoader.add(crashKey, func(crash *Crash) { 77 input.reportedCrash = crash 78 }) 79 } 80 inputs = append(inputs, input) 81 } 82 if err := crashLoader.load(c); err != nil { 83 return nil, fmt.Errorf("failed to fetch crashes: %w", err) 84 } 85 buildLoader := &dependencyLoader[Build]{} 86 for _, input := range inputs { 87 if input.reportedCrash == nil { 88 continue 89 } 90 buildLoader.add(buildKey(c, ns, input.reportedCrash.BuildID), func(build *Build) { 91 input.build = build 92 }) 93 } 94 if err := buildLoader.load(c); err != nil { 95 return nil, fmt.Errorf("failed to fetch builds: %w", err) 96 } 97 return inputs, nil 98 } 99 100 // Circumventing the datastore's multi query limitation. 101 func getAllMulti[T any](c context.Context, keys []*db.Key, objects []*T) (*db.Key, error) { 102 const step = 1000 103 for from := 0; from < len(keys); from += step { 104 to := min(from+step, len(keys)) 105 err := db.GetMulti(c, keys[from:to], objects[from:to]) 106 if err == nil { 107 continue 108 } 109 var merr appengine.MultiError 110 if errors.As(err, &merr) { 111 for i, objErr := range merr { 112 if objErr != nil { 113 return keys[from+i], objErr 114 } 115 } 116 } 117 return nil, err 118 } 119 return nil, nil 120 } 121 122 // getBugSummaries extracts the list of BugStatSummary objects among bugs 123 // that reached the specific reporting stage. 124 func getBugSummaries(c context.Context, ns, stage string) ([]*syzbotstats.BugStatSummary, error) { 125 inputs, err := allBugInputs(c, ns) 126 if err != nil { 127 return nil, err 128 } 129 var ret []*syzbotstats.BugStatSummary 130 for _, input := range inputs { 131 bug, crash := input.bug, input.reportedCrash 132 if crash == nil { 133 continue 134 } 135 targetStage := bugReportingByName(bug, stage) 136 if targetStage == nil || targetStage.Reported.IsZero() { 137 continue 138 } 139 obj := &syzbotstats.BugStatSummary{ 140 Title: bug.Title, 141 FirstTime: bug.FirstTime, 142 ReleasedTime: targetStage.Reported, 143 ResolvedTime: bug.Closed, 144 HappenedOn: bug.HappenedOn, 145 Strace: dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0, 146 } 147 for _, stage := range bug.Reporting { 148 if stage.ID != "" { 149 obj.IDs = append(obj.IDs, stage.ID) 150 } 151 } 152 for _, commit := range bug.CommitInfo { 153 obj.FixHashes = append(obj.FixHashes, commit.Hash) 154 } 155 if crash.ReproSyz > 0 { 156 obj.ReproTime = crash.Time 157 } 158 if bug.BisectCause == BisectYes { 159 causeBisect, err := queryBestBisection(c, bug, JobBisectCause) 160 if err != nil { 161 return nil, err 162 } 163 if causeBisect != nil { 164 obj.CauseBisectTime = causeBisect.job.Finished 165 } 166 } 167 fixTime := input.fixedAt() 168 if !fixTime.IsZero() && (obj.ResolvedTime.IsZero() || fixTime.Before(obj.ResolvedTime)) { 169 // Take the date of the fixing commit, if it's earlier. 170 obj.ResolvedTime = fixTime 171 } 172 obj.Status, err = input.bugStatus() 173 if err != nil { 174 return nil, fmt.Errorf("%s: %w", bug.Title, err) 175 } 176 177 const minAvgHitCrashes = 5 178 const minAvgHitPeriod = time.Hour * 24 179 if bug.NumCrashes >= minAvgHitCrashes || 180 bug.LastTime.Sub(bug.FirstTime) < minAvgHitPeriod { 181 // If there are only a few crashes or they all happened within a single day, 182 // it's hard to make any accurate frequency estimates. 183 timeSpan := bug.LastTime.Sub(bug.FirstTime) 184 obj.HitsPerDay = float64(bug.NumCrashes) / (timeSpan.Hours() / 24) 185 } 186 187 for _, label := range bug.LabelValues(SubsystemLabel) { 188 obj.Subsystems = append(obj.Subsystems, label.Value) 189 } 190 191 ret = append(ret, obj) 192 } 193 return ret, nil 194 }