OLD | NEW |
| (Empty) |
1 package data | |
2 | |
3 import ( | |
4 "bytes" | |
5 "encoding/gob" | |
6 "fmt" | |
7 "sort" | |
8 "sync" | |
9 | |
10 "github.com/skia-dev/glog" | |
11 "go.skia.org/infra/fuzzer/go/common" | |
12 ) | |
13 | |
14 type FuzzReportTree []FileFuzzReport | |
15 | |
16 type FileFuzzReport struct { | |
17 FileName string `json:"fileName"` | |
18 Count int `json:"count"` | |
19 Functions []FunctionFuzzReport `json:"byFunction"` | |
20 } | |
21 | |
22 type FunctionFuzzReport struct { | |
23 FunctionName string `json:"functionName"` | |
24 Count int `json:"count"` | |
25 LineNumbers []LineFuzzReport `json:"byLineNumber"` | |
26 } | |
27 | |
28 type LineFuzzReport struct { | |
29 LineNumber int `json:"lineNumber"` | |
30 Count int `json:"count"` | |
31 Details SortedFuzzReports `json:"reports"` | |
32 } | |
33 | |
34 type FuzzReport struct { | |
35 DebugStackTrace StackTrace `json:"debugStackTrace"` | |
36 ReleaseStackTrace StackTrace `json:"releaseStackTrace"` | |
37 DebugFlags []string `json:"debugFlags"` | |
38 ReleaseFlags []string `json:"releaseFlags"` | |
39 | |
40 FuzzName string `json:"fuzzName"` | |
41 FuzzCategory string `json:"category"` | |
42 } | |
43 | |
44 type SortedFuzzReports []FuzzReport | |
45 | |
46 // ParseReport creates a report given the raw materials passed in. | |
47 func ParseReport(g GCSPackage) FuzzReport { | |
48 result := ParseGCSPackage(g) | |
49 return FuzzReport{ | |
50 DebugStackTrace: result.Debug.StackTrace, | |
51 ReleaseStackTrace: result.Release.StackTrace, | |
52 DebugFlags: result.Debug.Flags.ToHumanReadableFlags(), | |
53 ReleaseFlags: result.Release.Flags.ToHumanReadableFlags(), | |
54 FuzzName: g.Name, | |
55 FuzzCategory: g.FuzzCategory, | |
56 } | |
57 } | |
58 | |
59 // treeReportBuilder is an in-memory structure that allows easy creation of a tr
ee of reports | |
60 // for use on the frontend. It has a fuzzReportCache for every fuzz type (e.g. s
kpicture, skcodec, etc) | |
61 type treeReportBuilder struct { | |
62 caches map[string]*fuzzReportCache | |
63 mutex sync.Mutex | |
64 } | |
65 | |
66 // newBuilder creates an initialized treeReportBuilder | |
67 func newBuilder() *treeReportBuilder { | |
68 return &treeReportBuilder{ | |
69 caches: map[string]*fuzzReportCache{}, | |
70 } | |
71 } | |
72 | |
73 // A fuzzReportCache holds three FuzzReportTrees - one for the raw data, a sorte
d version with | |
74 // all of the reports and an empty tree that holds no reports. These are used t
o procure data | |
75 // for the frontend. | |
76 type fuzzReportCache struct { | |
77 // All the data goes in here, in no particular order | |
78 rawData FuzzReportTree | |
79 // Generated, sorted cache | |
80 FullReport FuzzReportTree | |
81 | |
82 // If data is in rawData, but not in FullReport, the trees should be | |
83 // rebuilt | |
84 isDirty bool | |
85 } | |
86 | |
87 // currentData is the object that holds the cache of fuzz results. It is used b
y the frontend. | |
88 var currentData = newBuilder() | |
89 | |
90 // stagingData is the object that processes can write to to queue up new data | |
91 // without disturbing the data shown to users. | |
92 var stagingData = newBuilder() | |
93 | |
94 // FindFuzzDetails returns the detailed fuzz reports for a file name, function n
ame, and line number. | |
95 // If functionName is "" or lineNumber is -1, all reports are shown. | |
96 func FindFuzzDetails(category, fileName, functionName string, lineNumber int) (F
uzzReportTree, error) { | |
97 cache, found := currentData.caches[category] | |
98 if found { | |
99 if fileName == "" { | |
100 return cache.FullReport, nil | |
101 } | |
102 for _, file := range cache.FullReport { | |
103 if file.FileName == fileName { | |
104 if functionName == "" { | |
105 return FuzzReportTree{file}, nil | |
106 } | |
107 file.filterByFunctionName(functionName) | |
108 if lineNumber == common.UNKNOWN_LINE { | |
109 return FuzzReportTree{file}, nil | |
110 } | |
111 file.Functions[0].filterByLineNumber(lineNumber) | |
112 return FuzzReportTree{file}, nil | |
113 } | |
114 } | |
115 } | |
116 | |
117 return nil, fmt.Errorf("File %q not found", fileName) | |
118 } | |
119 | |
120 // filterByFunctionName removes all FuzzReportFunction except that which matches
functionName | |
121 func (file *FileFuzzReport) filterByFunctionName(functionName string) { | |
122 for _, function := range file.Functions { | |
123 if functionName == function.FunctionName { | |
124 file.Functions = []FunctionFuzzReport{function} | |
125 break | |
126 } | |
127 } | |
128 } | |
129 | |
130 // filterByLineNumber removes all FuzzReportLineNumber except that which matches
lineNumber | |
131 func (function *FunctionFuzzReport) filterByLineNumber(lineNumber int) { | |
132 for _, line := range function.LineNumbers { | |
133 if lineNumber == line.LineNumber { | |
134 function.LineNumbers = []LineFuzzReport{line} | |
135 } | |
136 } | |
137 } | |
138 | |
139 // FindFuzzDetailForFuzz returns a tree containing the single | |
140 // report with the given name, or an error, it it doesn't exist. | |
141 func FindFuzzDetailForFuzz(category, name string) (FuzzReportTree, error) { | |
142 if cache, found := currentData.caches[category]; found { | |
143 for _, file := range cache.FullReport { | |
144 if file.filterByFuzzName(name) { | |
145 return FuzzReportTree{file}, nil | |
146 } | |
147 } | |
148 } | |
149 return nil, fmt.Errorf("Fuzz with name %q not found", name) | |
150 } | |
151 | |
152 // filterByFuzzName filters out all functions that do not contain a fuzz with th
e given | |
153 // name and returns true. If such a fuzz does not exist, it returns false. | |
154 func (file *FileFuzzReport) filterByFuzzName(name string) bool { | |
155 for _, function := range file.Functions { | |
156 if function.filterByFuzzName(name) { | |
157 file.Functions = []FunctionFuzzReport{function} | |
158 return true | |
159 } | |
160 } | |
161 return false | |
162 } | |
163 | |
164 // filterByFuzzName filters out all lines that do not contain a fuzz with the gi
ven | |
165 // name and returns true. If such a fuzz does not exist, it returns false. | |
166 func (function *FunctionFuzzReport) filterByFuzzName(name string) bool { | |
167 for _, line := range function.LineNumbers { | |
168 if line.filterByFuzzName(name) { | |
169 function.LineNumbers = []LineFuzzReport{line} | |
170 return true | |
171 } | |
172 } | |
173 return false | |
174 } | |
175 | |
176 // filterByFuzzName filters out all fuzzes that do not have the given | |
177 // name and returns true. If such a fuzz does not exist, it returns false. | |
178 func (line *LineFuzzReport) filterByFuzzName(name string) bool { | |
179 if b, hasIt := line.Details.containsName(name); hasIt { | |
180 line.Details = SortedFuzzReports{b} | |
181 return true | |
182 } | |
183 return false | |
184 } | |
185 | |
186 func NewFuzzFound(category string, b FuzzReport) { | |
187 // set the category if it has not already been set | |
188 b.FuzzCategory = category | |
189 stagingData.addFuzzReport(category, b) | |
190 } | |
191 | |
192 // ClearStaging clears the staging representation. | |
193 func ClearStaging() { | |
194 stagingData.mutex.Lock() | |
195 defer stagingData.mutex.Unlock() | |
196 stagingData.caches = map[string]*fuzzReportCache{} | |
197 } | |
198 | |
199 // SetStaging replaces the staging representation with the given FuzzReport. | |
200 func SetStaging(category string, r FuzzReportTree) { | |
201 stagingData.mutex.Lock() | |
202 defer stagingData.mutex.Unlock() | |
203 cache, found := stagingData.caches[category] | |
204 if !found { | |
205 cache = &fuzzReportCache{} | |
206 stagingData.caches[category] = cache | |
207 } | |
208 cache.rawData = r | |
209 cache.rebuildSortedReports() | |
210 } | |
211 | |
212 // StagingToCurrent moves a copy of the staging data to the currentData. | |
213 func StagingToCurrent() { | |
214 currentData.mutex.Lock() | |
215 defer currentData.mutex.Unlock() | |
216 stagingData.mutex.Lock() | |
217 defer stagingData.mutex.Unlock() | |
218 | |
219 currentData.caches = map[string]*fuzzReportCache{} | |
220 for k, v := range stagingData.caches { | |
221 cache := fuzzReportCache{} | |
222 cache.rawData = cloneReport(v.rawData) | |
223 cache.rebuildSortedReports() | |
224 currentData.caches[k] = &cache | |
225 } | |
226 } | |
227 | |
228 // StagingToCurrent moves a copy of the current data to the staging data. | |
229 func StagingFromCurrent() { | |
230 currentData.mutex.Lock() | |
231 defer currentData.mutex.Unlock() | |
232 stagingData.mutex.Lock() | |
233 defer stagingData.mutex.Unlock() | |
234 | |
235 stagingData.caches = map[string]*fuzzReportCache{} | |
236 for k, v := range currentData.caches { | |
237 cache := fuzzReportCache{} | |
238 cache.rawData = cloneReport(v.rawData) | |
239 cache.rebuildSortedReports() | |
240 stagingData.caches[k] = &cache | |
241 } | |
242 } | |
243 | |
244 // StagingCopy returns a fresh copy of the underlying staging data. | |
245 func StagingCopy(category string) FuzzReportTree { | |
246 stagingData.mutex.Lock() | |
247 defer stagingData.mutex.Unlock() | |
248 cache, found := stagingData.caches[category] | |
249 if !found { | |
250 return FuzzReportTree{} | |
251 } | |
252 return cloneReport(cache.rawData) | |
253 } | |
254 | |
255 // addFuzzReport adds a FuzzReport to a treeReportBuilder's data member | |
256 func (r *treeReportBuilder) addFuzzReport(category string, b FuzzReport) { | |
257 reportFileName, reportFunctionName, reportLineNumber := extractStacktrac
eInfo(b.DebugStackTrace, b.ReleaseStackTrace) | |
258 | |
259 cache, found := r.caches[category] | |
260 if !found { | |
261 cache = &fuzzReportCache{} | |
262 r.caches[category] = cache | |
263 } | |
264 r.mutex.Lock() | |
265 defer r.mutex.Unlock() | |
266 foundFile, foundFunction, foundLine := cache.makeOrFindRecords(reportFil
eName, reportFunctionName, reportLineNumber) | |
267 | |
268 foundFile.Count++ | |
269 foundFunction.Count++ | |
270 foundLine.Count++ | |
271 foundLine.Details = foundLine.Details.append(b) | |
272 cache.isDirty = true | |
273 | |
274 } | |
275 | |
276 // extractStacktraceInfo returns the file name, function name and line number th
at | |
277 // a report with the given debug and release stacktrace should be sorted by. | |
278 // this tries to read the release stacktrace first, falling back to the debug st
acktrace, | |
279 // failling back to Unknown. | |
280 func extractStacktraceInfo(debug, release StackTrace) (reportFileName, reportFun
ctionName string, reportLineNumber int) { | |
281 reportFileName, reportFunctionName, reportLineNumber = common.UNKNOWN_FI
LE, common.UNKNOWN_FUNCTION, common.UNKNOWN_LINE | |
282 | |
283 stacktrace := release | |
284 if stacktrace.IsEmpty() { | |
285 stacktrace = debug | |
286 } | |
287 if !stacktrace.IsEmpty() { | |
288 frame := stacktrace.Frames[0] | |
289 reportFileName = frame.PackageName + frame.FileName | |
290 reportFunctionName, reportLineNumber = frame.FunctionName, frame
.LineNumber | |
291 } | |
292 return | |
293 } | |
294 | |
295 // makeOrFindRecords finds the FuzzReportFile, FuzzReportFunction and FuzzReport
LineNumber | |
296 // associated with the inputs, creating the structures if needed. | |
297 func (c *fuzzReportCache) makeOrFindRecords(reportFileName, reportFunctionName s
tring, reportLineNumber int) (*FileFuzzReport, *FunctionFuzzReport, *LineFuzzRep
ort) { | |
298 var foundFile *FileFuzzReport | |
299 for i, file := range c.rawData { | |
300 if file.FileName == reportFileName { | |
301 foundFile = &c.rawData[i] | |
302 break | |
303 } | |
304 } | |
305 if foundFile == nil { | |
306 c.rawData = append(c.rawData, FileFuzzReport{reportFileName, 0,
nil}) | |
307 foundFile = &c.rawData[len(c.rawData)-1] | |
308 } | |
309 | |
310 var foundFunction *FunctionFuzzReport | |
311 for i, function := range foundFile.Functions { | |
312 if function.FunctionName == reportFunctionName { | |
313 foundFunction = &foundFile.Functions[i] | |
314 break | |
315 } | |
316 } | |
317 if foundFunction == nil { | |
318 foundFile.Functions = append(foundFile.Functions, FunctionFuzzRe
port{reportFunctionName, 0, nil}) | |
319 foundFunction = &foundFile.Functions[len(foundFile.Functions)-1] | |
320 } | |
321 | |
322 var foundLine *LineFuzzReport | |
323 for i, line := range foundFunction.LineNumbers { | |
324 if line.LineNumber == reportLineNumber { | |
325 foundLine = &foundFunction.LineNumbers[i] | |
326 } | |
327 } | |
328 if foundLine == nil { | |
329 foundFunction.LineNumbers = append(foundFunction.LineNumbers, Li
neFuzzReport{reportLineNumber, 0, nil}) | |
330 foundLine = &foundFunction.LineNumbers[len(foundFunction.LineNum
bers)-1] | |
331 } | |
332 return foundFile, foundFunction, foundLine | |
333 } | |
334 | |
335 // getTreeSortedByTotal gets the detailed FuzzReport for a fuzz category | |
336 // sorted by total number of fuzzes. | |
337 func (r *treeReportBuilder) getTreeSortedByTotal(category string) FuzzReportTree
{ | |
338 cache, found := r.caches[category] | |
339 if !found { | |
340 glog.Warningf("Could not find report tree for category %s", cate
gory) | |
341 return FuzzReportTree{} | |
342 } | |
343 if cache.isDirty { | |
344 r.mutex.Lock() | |
345 defer r.mutex.Unlock() | |
346 cache.rebuildSortedReports() | |
347 } | |
348 return cache.FullReport | |
349 } | |
350 | |
351 // rebuildSortedReports creates the sorted reports for a given cache. | |
352 func (c *fuzzReportCache) rebuildSortedReports() { | |
353 c.FullReport = c.getClonedSortedReport(true) | |
354 c.isDirty = false | |
355 } | |
356 | |
357 // getClonedSortedReport makes a newly allocated FuzzReport after running the pa
ssed in function | |
358 // on all FuzzReportLineNumber objects in the report. | |
359 func (c *fuzzReportCache) getClonedSortedReport(keepDetails bool) FuzzReportTree
{ | |
360 report := cloneReport(c.rawData) | |
361 sort.Sort(filesTotalSort(report)) | |
362 for i := range report { | |
363 file := &report[i] | |
364 sort.Sort(functionsTotalSort(file.Functions)) | |
365 for j := range file.Functions { | |
366 function := &file.Functions[j] | |
367 sort.Sort(linesTotalSort(function.LineNumbers)) | |
368 for k := range function.LineNumbers { | |
369 line := &function.LineNumbers[k] | |
370 if !keepDetails { | |
371 line.Details = nil | |
372 } | |
373 } | |
374 } | |
375 } | |
376 return report | |
377 } | |
378 | |
379 // cloneReport makes a copy of the input using the gob library. | |
380 func cloneReport(data []FileFuzzReport) FuzzReportTree { | |
381 var temp bytes.Buffer | |
382 enc := gob.NewEncoder(&temp) | |
383 dec := gob.NewDecoder(&temp) | |
384 | |
385 if err := enc.Encode(data); err != nil { | |
386 // This should never happen, but log it if it does | |
387 glog.Errorf("Error while cloning report: %v", err) | |
388 } | |
389 var clone FuzzReportTree | |
390 if err := dec.Decode(&clone); err != nil { | |
391 // This should never happen, but log it if it does | |
392 glog.Errorf("Error while cloning report: %v", err) | |
393 } | |
394 return clone | |
395 } | |
396 | |
397 // Total sort methods - sorts files, functions and lines by Count | |
398 type filesTotalSort []FileFuzzReport | |
399 | |
400 func (r filesTotalSort) Len() int { return len(r) } | |
401 func (r filesTotalSort) Swap(i, j int) { r[i], r[j] = r[j], r[i] } | |
402 | |
403 func (r filesTotalSort) Less(i, j int) bool { | |
404 if r[i].Count != r[j].Count { | |
405 return r[i].Count > r[j].Count | |
406 } | |
407 // If they have the same total, sort by name | |
408 return r[i].FileName < r[j].FileName | |
409 } | |
410 | |
411 type functionsTotalSort []FunctionFuzzReport | |
412 | |
413 func (r functionsTotalSort) Len() int { return len(r) } | |
414 func (r functionsTotalSort) Swap(i, j int) { r[i], r[j] = r[j], r[i] } | |
415 | |
416 func (r functionsTotalSort) Less(i, j int) bool { | |
417 if r[i].Count != r[j].Count { | |
418 return r[i].Count > r[j].Count | |
419 } | |
420 // If they have the same total, sort by name | |
421 return r[i].FunctionName < r[j].FunctionName | |
422 } | |
423 | |
424 type linesTotalSort []LineFuzzReport | |
425 | |
426 func (r linesTotalSort) Len() int { return len(r) } | |
427 func (r linesTotalSort) Swap(i, j int) { r[i], r[j] = r[j], r[i] } | |
428 | |
429 func (r linesTotalSort) Less(i, j int) bool { | |
430 if r[i].Count != r[j].Count { | |
431 return r[i].Count > r[j].Count | |
432 } | |
433 // If they have the same total, sort by line number | |
434 return r[i].LineNumber < r[j].LineNumber | |
435 } | |
436 | |
437 func (p SortedFuzzReports) Len() int { return len(p) } | |
438 func (p SortedFuzzReports) Less(i, j int) bool { return p[i].FuzzName < p[j].Fuz
zName } | |
439 func (p SortedFuzzReports) Swap(i, j int) { p[i], p[j] = p[j], p[i] } | |
440 | |
441 // append adds b to the already sorted caller, and returns the sorted result. | |
442 // Precondition: Caller must be nil or sorted | |
443 func (p SortedFuzzReports) append(b FuzzReport) SortedFuzzReports { | |
444 s := append(p, b) | |
445 | |
446 // Google Storage gives us the fuzzes in alphabetical order. Thus, we c
an short circuit | |
447 // if the fuzz goes on the end (which is usually does). | |
448 // However, we can't always do this because when we load a second batch
of fuzzes, | |
449 // those are in alphabetical order, but starting over from 0. | |
450 // We want to avoid [a,c,x,z,b,d] where b,d were added from the second b
atch. | |
451 if len(s) <= 1 || s.Less(len(s)-2, len(s)-1) { | |
452 return s | |
453 } | |
454 sort.Sort(s) | |
455 return s | |
456 } | |
457 | |
458 // containsName returns the FuzzReport and true if a fuzz with the given name is
in the list. | |
459 func (p SortedFuzzReports) containsName(fuzzName string) (FuzzReport, bool) { | |
460 i := sort.Search(len(p), func(i int) bool { return p[i].FuzzName >= fuzz
Name }) | |
461 if i < len(p) && p[i].FuzzName == fuzzName { | |
462 return p[i], true | |
463 } | |
464 return FuzzReport{}, false | |
465 } | |
OLD | NEW |