github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/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/stats/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 68 crashKeys := []*db.Key{} 69 crashToInput := map[*db.Key]*bugInput{} 70 for i, bug := range bugs { 71 bugReporting := lastReportedReporting(bug) 72 input := &bugInput{ 73 bug: bug, 74 bugReporting: bugReporting, 75 } 76 if bugReporting.CrashID != 0 { 77 crashKey := db.NewKey(c, "Crash", "", bugReporting.CrashID, bugKeys[i]) 78 crashKeys = append(crashKeys, crashKey) 79 crashToInput[crashKey] = input 80 } 81 inputs = append(inputs, input) 82 } 83 // Fetch crashes. 84 buildKeys := []*db.Key{} 85 buildToInput := map[*db.Key]*bugInput{} 86 if len(crashKeys) > 0 { 87 crashes := make([]*Crash, len(crashKeys)) 88 if badKey, err := getAllMulti(c, crashKeys, crashes); err != nil { 89 return nil, fmt.Errorf("failed to fetch crashes for %v: %w", badKey, err) 90 } 91 for i, crash := range crashes { 92 if crash == nil { 93 continue 94 } 95 input := crashToInput[crashKeys[i]] 96 input.reportedCrash = crash 97 98 buildKey := buildKey(c, ns, crash.BuildID) 99 buildKeys = append(buildKeys, buildKey) 100 buildToInput[buildKey] = input 101 } 102 } 103 // Fetch builds. 104 if len(buildKeys) > 0 { 105 builds := make([]*Build, len(buildKeys)) 106 if badKey, err := getAllMulti(c, buildKeys, builds); err != nil { 107 return nil, fmt.Errorf("failed to fetch builds for %v: %w", badKey, err) 108 } 109 for i, build := range builds { 110 if build != nil { 111 buildToInput[buildKeys[i]].build = build 112 } 113 } 114 } 115 return inputs, nil 116 } 117 118 // Circumventing the datastore's multi query limitation. 119 func getAllMulti[T any](c context.Context, keys []*db.Key, objects []*T) (*db.Key, error) { 120 const step = 1000 121 for from := 0; from < len(keys); from += step { 122 to := from + step 123 if to > len(keys) { 124 to = len(keys) 125 } 126 err := db.GetMulti(c, keys[from:to], objects[from:to]) 127 if err == nil { 128 continue 129 } 130 var merr appengine.MultiError 131 if errors.As(err, &merr) { 132 for i, objErr := range merr { 133 if objErr != nil { 134 return keys[from+i], objErr 135 } 136 } 137 } 138 return nil, err 139 } 140 return nil, nil 141 } 142 143 // getBugSummaries extracts the list of BugStatSummary objects among bugs 144 // that reached the specific reporting stage. 145 func getBugSummaries(c context.Context, ns, stage string) ([]*syzbotstats.BugStatSummary, error) { 146 inputs, err := allBugInputs(c, ns) 147 if err != nil { 148 return nil, err 149 } 150 var ret []*syzbotstats.BugStatSummary 151 for _, input := range inputs { 152 bug, crash := input.bug, input.reportedCrash 153 if crash == nil { 154 continue 155 } 156 targetStage := bugReportingByName(bug, stage) 157 if targetStage == nil || targetStage.Reported.IsZero() { 158 continue 159 } 160 obj := &syzbotstats.BugStatSummary{ 161 Title: bug.Title, 162 FirstTime: bug.FirstTime, 163 ReleasedTime: targetStage.Reported, 164 ResolvedTime: bug.Closed, 165 HappenedOn: bug.HappenedOn, 166 Strace: dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0, 167 } 168 for _, stage := range bug.Reporting { 169 if stage.ID != "" { 170 obj.IDs = append(obj.IDs, stage.ID) 171 } 172 } 173 for _, commit := range bug.CommitInfo { 174 obj.FixHashes = append(obj.FixHashes, commit.Hash) 175 } 176 if crash.ReproSyz > 0 { 177 obj.ReproTime = crash.Time 178 } 179 if bug.BisectCause == BisectYes { 180 causeBisect, err := queryBestBisection(c, bug, JobBisectCause) 181 if err != nil { 182 return nil, err 183 } 184 if causeBisect != nil { 185 obj.CauseBisectTime = causeBisect.job.Finished 186 } 187 } 188 fixTime := input.fixedAt() 189 if !fixTime.IsZero() && (obj.ResolvedTime.IsZero() || fixTime.Before(obj.ResolvedTime)) { 190 // Take the date of the fixing commit, if it's earlier. 191 obj.ResolvedTime = fixTime 192 } 193 obj.Status, err = input.bugStatus() 194 if err != nil { 195 return nil, fmt.Errorf("%s: %w", bug.Title, err) 196 } 197 198 const minAvgHitCrashes = 5 199 const minAvgHitPeriod = time.Hour * 24 200 if bug.NumCrashes >= minAvgHitCrashes || 201 bug.LastTime.Sub(bug.FirstTime) < minAvgHitPeriod { 202 // If there are only a few crashes or they all happened within a single day, 203 // it's hard to make any accurate frequency estimates. 204 timeSpan := bug.LastTime.Sub(bug.FirstTime) 205 obj.HitsPerDay = float64(bug.NumCrashes) / (timeSpan.Hours() / 24) 206 } 207 208 for _, label := range bug.LabelValues(SubsystemLabel) { 209 obj.Subsystems = append(obj.Subsystems, label.Value) 210 } 211 212 ret = append(ret, obj) 213 } 214 return ret, nil 215 }