diff --git a/dashboard/app/api.go b/dashboard/app/api.go index 49c524c1464b..3bffab48dfff 100644 --- a/dashboard/app/api.go +++ b/dashboard/app/api.go @@ -916,6 +916,9 @@ func reportCrash(c context.Context, build *Build, req *dashapi.Crash) (*Bug, err bug.SetAutoSubsystems(c, newSubsystems, now, subsystemService.Revision) } bug.increaseCrashStats(now) + if err = bug.addTitleStat(req.TailTitles); err != nil { + return fmt.Errorf("failed to add title stat: %w", err) + } bug.HappenedOn = mergeString(bug.HappenedOn, build.Manager) // Migration of older entities (for new bugs Title is always in MergedTitles). bug.MergedTitles = mergeString(bug.MergedTitles, bug.Title) @@ -975,10 +978,11 @@ func (crash *Crash) UpdateReportingPriority(c context.Context, build *Build, bug func saveCrash(c context.Context, ns string, req *dashapi.Crash, bug *Bug, bugKey *db.Key, build *Build, assets []Asset) error { crash := &Crash{ - Title: req.Title, - Manager: build.Manager, - BuildID: req.BuildID, - Time: timeNow(c), + Title: req.Title, + TailTitles: req.TailTitles, + Manager: build.Manager, + BuildID: req.BuildID, + Time: timeNow(c), Maintainers: email.MergeEmailLists(req.Maintainers, GetEmails(req.Recipients, dashapi.To), GetEmails(req.Recipients, dashapi.Cc)), @@ -996,6 +1000,13 @@ func saveCrash(c context.Context, ns string, req *dashapi.Crash, bug *Bug, bugKe if crash.Report, err = putText(c, ns, textCrashReport, req.Report); err != nil { return err } + for _, tailReport := range req.TailReports { + tailReportID, err := putText(c, ns, textCrashReport, tailReport) + if err != nil { + return err + } + crash.TailReports = append(crash.TailReports, tailReportID) + } if crash.ReproSyz, err = putText(c, ns, textReproSyz, req.ReproSyz); err != nil { return err } @@ -1073,6 +1084,11 @@ func purgeOldCrashes(c context.Context, bug *Bug, bugKey *db.Key) { if crash.Report != 0 { toDelete = append(toDelete, db.NewKey(c, textCrashReport, "", crash.Report, nil)) } + if len(crash.TailReports) != 0 { + for _, tailReport := range crash.TailReports { + toDelete = append(toDelete, db.NewKey(c, textCrashReport, "", tailReport, nil)) + } + } if crash.ReproSyz != 0 { toDelete = append(toDelete, db.NewKey(c, textReproSyz, "", crash.ReproSyz, nil)) } diff --git a/dashboard/app/app_test.go b/dashboard/app/app_test.go index 693ca61ee9ad..9370fc8de0d3 100644 --- a/dashboard/app/app_test.go +++ b/dashboard/app/app_test.go @@ -16,9 +16,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/syzkaller/dashboard/dashapi" "github.com/google/syzkaller/pkg/auth" + "github.com/google/syzkaller/pkg/report" "github.com/google/syzkaller/pkg/subsystem" _ "github.com/google/syzkaller/pkg/subsystem/lists" "github.com/google/syzkaller/sys/targets" + "github.com/stretchr/testify/assert" "google.golang.org/appengine/v2/user" ) @@ -1139,3 +1141,43 @@ kernel BUG at %s
%s<", crash.Title, crash.TailTitles[0]) + if !strings.Contains(string(res), wantTitles) { + t.Logf("%s", res) + t.Errorf("can't find titles string %q", wantTitles) + } + if !strings.Contains(string(res), ">tail report 1<") { + t.Logf("%s", res) + t.Error("can't find tail report string") + } +} diff --git a/dashboard/app/entities_datastore.go b/dashboard/app/entities_datastore.go index e424172f4cce..32ca92b26d01 100644 --- a/dashboard/app/entities_datastore.go +++ b/dashboard/app/entities_datastore.go @@ -13,6 +13,7 @@ import ( "github.com/google/syzkaller/dashboard/dashapi" "github.com/google/syzkaller/pkg/hash" + "github.com/google/syzkaller/pkg/report" "github.com/google/syzkaller/pkg/subsystem" db "google.golang.org/appengine/v2/datastore" ) @@ -91,6 +92,7 @@ type Bug struct { Title string MergedTitles []string // crash titles that we already merged into this bug AltTitles []string // alternative crash titles that we may merge into this bug + TitleStat string `datastore:",noindex"` // serialized report.TitleStat Status int StatusReason dashapi.BugStatusReason // e.g. if the bug status is "invalid", here's the reason why DupOf string @@ -348,6 +350,7 @@ type Crash struct { // May be different from bug.Title due to AltTitles. // May be empty for old bugs, in such case bug.Title is the right title. Title string + TailTitles []string Manager string BuildID string Time time.Time @@ -357,6 +360,7 @@ type Crash struct { Log int64 // reference to CrashLog text entity Flags int64 // properties of the Crash Report int64 // reference to CrashReport text entity + TailReports []int64 // references to CrashReport text entity ReportElements CrashReportElements // parsed parts of the crash report ReproOpts []byte `datastore:",noindex"` ReproSyz int64 // reference to ReproSyz text entity @@ -985,6 +989,20 @@ func (bug *Bug) increaseCrashStats(now time.Time) { } } +func (bug *Bug) addTitleStat(titles []string) error { + ts, err := report.TitleStatFromBytes([]byte(bug.TitleStat)) + if err != nil { + return err + } + ts.Add(append([]string{bug.Title}, titles...)) + bytes, err := ts.ToBytes() + if err != nil { + return err + } + bug.TitleStat = string(bytes) + return nil +} + func (bug *Bug) dailyStatsTail(from time.Time) []BugDailyStats { startDate := timeDate(from) startPos := len(bug.DailyStats) diff --git a/dashboard/app/main.go b/dashboard/app/main.go index 0d2e588b1902..7f28eb8e4935 100644 --- a/dashboard/app/main.go +++ b/dashboard/app/main.go @@ -368,6 +368,7 @@ type uiBug struct { Namespace string Title string ImpactScore int + RankTooltip string NumCrashes int64 NumCrashesBad bool BisectCause BisectStatus @@ -398,19 +399,21 @@ type uiBugLabel struct { } type uiCrash struct { - Title string - Manager string - Time time.Time - Maintainers string - LogLink string - LogHasStrace bool - ReportLink string - ReproSyzLink string - ReproCLink string - ReproIsRevoked bool - ReproLogLink string - MachineInfoLink string - Assets []*uiAsset + Title string + TailTitles []string + Manager string + Time time.Time + Maintainers string + LogLink string + LogHasStrace bool + ReportLink string + TailReportsLinks []string + ReproSyzLink string + ReproCLink string + ReproIsRevoked bool + ReproLogLink string + MachineInfoLink string + Assets []*uiAsset *uiBuild } @@ -1938,10 +1941,16 @@ func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers [] log.Errorf(c, "failed to generate credit email: %v", err) } } + + titleStat, err := report.TitleStatFromBytes([]byte(bug.TitleStat)) + if err != nil { + log.Errorf(c, "report.TitleStatFromBytes: %v", err) + } uiBug := &uiBug{ Namespace: bug.Namespace, Title: bug.displayTitle(), ImpactScore: report.TitlesToImpact(bug.Title, bug.AltTitles...), + RankTooltip: report.HigherRankTooltip(bug.Title, titleStat.Explain()), BisectCause: bug.BisectCause, BisectFix: bug.BisectFix, NumCrashes: bug.NumCrashes, @@ -2076,20 +2085,26 @@ func makeUIAssets(c context.Context, build *Build, crash *Crash, forReport bool) } func makeUICrash(c context.Context, crash *Crash, build *Build) *uiCrash { + var tailReportsLinks []string + for _, tailReportID := range crash.TailReports { + tailReportsLinks = append(tailReportsLinks, textLink(textCrashReport, tailReportID)) + } ui := &uiCrash{ - Title: crash.Title, - Manager: crash.Manager, - Time: crash.Time, - Maintainers: strings.Join(crash.Maintainers, ", "), - LogLink: textLink(textCrashLog, crash.Log), - LogHasStrace: dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0, - ReportLink: textLink(textCrashReport, crash.Report), - ReproSyzLink: textLink(textReproSyz, crash.ReproSyz), - ReproCLink: textLink(textReproC, crash.ReproC), - ReproLogLink: textLink(textReproLog, crash.ReproLog), - ReproIsRevoked: crash.ReproIsRevoked, - MachineInfoLink: textLink(textMachineInfo, crash.MachineInfo), - Assets: makeUIAssets(c, build, crash, true), + Title: crash.Title, + TailTitles: crash.TailTitles, + Manager: crash.Manager, + Time: crash.Time, + Maintainers: strings.Join(crash.Maintainers, ", "), + LogLink: textLink(textCrashLog, crash.Log), + LogHasStrace: dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0, + ReportLink: textLink(textCrashReport, crash.Report), + TailReportsLinks: tailReportsLinks, + ReproSyzLink: textLink(textReproSyz, crash.ReproSyz), + ReproCLink: textLink(textReproC, crash.ReproC), + ReproLogLink: textLink(textReproLog, crash.ReproLog), + ReproIsRevoked: crash.ReproIsRevoked, + MachineInfoLink: textLink(textMachineInfo, crash.MachineInfo), + Assets: makeUIAssets(c, build, crash, true), } if build != nil { ui.uiBuild = makeUIBuild(c, build, true) diff --git a/dashboard/app/templates/templates.html b/dashboard/app/templates/templates.html index af54dab6f4d3..5491904c7a50 100644 --- a/dashboard/app/templates/templates.html +++ b/dashboard/app/templates/templates.html @@ -206,7 +206,14 @@

syzbot

{{link .Link .Name}} {{- end}} - {{$b.ImpactScore}} + + {{if $b.RankTooltip}} + {{$b.ImpactScore}} +
{{$b.RankTooltip}}
+ {{else}} + {{$b.ImpactScore}} + {{end}} + {{formatReproLevel $b.ReproLevel}} {{print $b.BisectCause}} {{print $b.BisectFix}} @@ -456,7 +463,14 @@

syzbot

{{link $b.SyzkallerCommitLink (formatShortHash $b.SyzkallerCommit)}} {{if $b.KernelConfigLink}}.config{{end}} {{if $b.LogLink}}{{if $b.LogHasStrace}}strace{{else}}console{{end}} log{{end}} - {{if $b.ReportLink}}report{{end}} + + {{- if $b.ReportLink -}} + report + {{- end -}} + {{- range $i, $trl := $b.TailReportsLinks -}} +
tail report {{add $i 1}} + {{- end -}} + {{if $b.ReproSyzLink}}syz{{end}}{{if $b.ReproLogLink}} / log{{end}} {{if $b.ReproCLink}}C{{end}} {{if $b.MachineInfoLink}}info{{end}} @@ -464,7 +478,12 @@

syzbot

[{{$asset.Title}}{{if $asset.FsckLogURL}} ({{if $asset.FsIsClean}}clean{{else}}corrupt{{end}} fs){{end}}] {{end}} {{$b.Manager}} - {{$b.Title}} + + {{- $b.Title}} + {{- range $tt := $b.TailTitles -}} +
{{$tt}} + {{- end -}} + {{end}} diff --git a/dashboard/dashapi/dashapi.go b/dashboard/dashapi/dashapi.go index 1f9dec94dcfa..5594a4ac7c7f 100644 --- a/dashboard/dashapi/dashapi.go +++ b/dashboard/dashapi/dashapi.go @@ -322,6 +322,7 @@ type Crash struct { BuildID string // refers to Build.ID Title string AltTitles []string // alternative titles, used for better deduplication + TailTitles []string // titles of the tail reports, see TailReports field Corrupted bool // report is corrupted (corrupted title, no stacks, etc) Suppressed bool Maintainers []string // deprecated in favor of Recipients @@ -329,6 +330,9 @@ type Crash struct { Log []byte Flags CrashFlags Report []byte + // Crashing machine may generate report chain like WARNING -> WARNING -> KASAN -> GPF. + // These additional reports are used to better understand the bug nature and impact. + TailReports [][]byte MachineInfo []byte Assets []NewAsset GuiltyFiles []string diff --git a/pkg/html/html.go b/pkg/html/html.go index e077153d1856..9e75d830a34a 100644 --- a/pkg/html/html.go +++ b/pkg/html/html.go @@ -52,24 +52,27 @@ func CreateTextGlob(glob string) *texttemplate.Template { } var Funcs = template.FuncMap{ - "link": link, - "optlink": optlink, - "formatTime": FormatTime, - "formatDate": FormatDate, - "formatKernelTime": formatKernelTime, - "formatJSTime": formatJSTime, + // keep-sorted start + "add": func(a, b int) int { return a + b }, + "commitLink": commitLink, + "dereference": dereferencePointer, "formatClock": formatClock, + "formatCommitTableTitle": formatCommitTableTitle, + "formatDate": FormatDate, "formatDuration": formatDuration, + "formatJSTime": formatJSTime, + "formatKernelTime": formatKernelTime, "formatLateness": formatLateness, + "formatList": formatStringList, "formatReproLevel": formatReproLevel, - "formatStat": formatStat, "formatShortHash": formatShortHash, + "formatStat": formatStat, "formatTagHash": formatTagHash, - "formatCommitTableTitle": formatCommitTableTitle, - "formatList": formatStringList, + "formatTime": FormatTime, + "link": link, + "optlink": optlink, "selectBisect": selectBisect, - "dereference": dereferencePointer, - "commitLink": commitLink, + // keep-sorted end } func selectBisect(rep *dashapi.BugReport) *dashapi.BisectResult { diff --git a/pkg/manager/crash.go b/pkg/manager/crash.go index a62ec8280125..b3ecd20f2a2d 100644 --- a/pkg/manager/crash.go +++ b/pkg/manager/crash.go @@ -91,8 +91,10 @@ func (cs *CrashStore) SaveCrash(crash *Crash) (bool, error) { writeOrRemove("tag", []byte(cs.Tag)) writeOrRemove("report", report.MergeReportBytes(reps)) writeOrRemove("machineInfo", crash.MachineInfo) - if err := report.AddTitleStat(filepath.Join(dir, "title-stat"), reps); err != nil { - return false, fmt.Errorf("report.AddTitleStat: %w", err) + titleStatPath := filepath.Join(dir, "title-stat") + titles := append([]string{crash.Title}, crash.TailTitles()...) + if err := report.AddTitlesToStatFile(titleStatPath, titles); err != nil { + return false, fmt.Errorf("report.AddTitlesToStatFile: %w", err) } return first, nil @@ -243,11 +245,13 @@ func (cs *CrashStore) BugInfo(id string, full bool) (*BugInfo, error) { // Bug rank may go up over time if we observe higher ranked bugs as a consequence of the first failure. ret.Rank = report.TitlesToImpact(ret.Title) - if titleStat, err := report.ReadStatFile(filepath.Join(dir, "title-stat")); err == nil { - ret.TailTitles = report.ExplainTitleStat(titleStat) - for _, ti := range ret.TailTitles { - ret.Rank = max(ret.Rank, ti.Rank) - } + titleStat, err := report.ReadStatFile(filepath.Join(dir, "title-stat")) + if err != nil { + return nil, err + } + ret.TailTitles = titleStat.Explain() + for _, ti := range ret.TailTitles { + ret.Rank = max(ret.Rank, ti.Rank) } ret.FirstTime = osutil.CreationTime(stat) diff --git a/pkg/manager/http.go b/pkg/manager/http.go index 61d208f410cc..07042a5cd6ae 100644 --- a/pkg/manager/http.go +++ b/pkg/manager/http.go @@ -357,7 +357,7 @@ func makeUICrashType(info *BugInfo, startTime time.Time, repros map[string]bool) info.ReproAttempts >= MaxReproAttempts) return UICrashType{ BugInfo: *info, - RankTooltip: higherRankTooltip(info.Title, info.TailTitles), + RankTooltip: report.HigherRankTooltip(info.Title, info.TailTitles), New: info.FirstTime.After(startTime), Active: info.LastTime.After(startTime), Triaged: triaged, @@ -365,26 +365,6 @@ func makeUICrashType(info *BugInfo, startTime time.Time, repros map[string]bool) } } -// higherRankTooltip generates the prioritized list of the titles with higher Rank -// than the firstTitle has. -func higherRankTooltip(firstTitle string, titlesInfo []*report.TitleFreqRank) string { - baseRank := report.TitlesToImpact(firstTitle) - res := "" - for _, ti := range titlesInfo { - if ti.Rank <= baseRank { - continue - } - res += fmt.Sprintf("[rank %2v, freq %5.1f%%] %s\n", - ti.Rank, - 100*float32(ti.Count)/float32(ti.Total), - ti.Title) - } - if res != "" { - return fmt.Sprintf("[rank %2v, originally] %s\n%s", baseRank, firstTitle, res) - } - return res -} - var crashIDRe = regexp.MustCompile(`^\w+$`) func (serv *HTTPServer) httpCrash(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/manager/repro.go b/pkg/manager/repro.go index 184945c5ce9b..a3bc90569b3d 100644 --- a/pkg/manager/repro.go +++ b/pkg/manager/repro.go @@ -51,6 +51,22 @@ func (c *Crash) FullTitle() string { panic("the crash is expected to have a report") } +func (c *Crash) TailTitles() []string { + res := make([]string, len(c.TailReports)) + for i, r := range c.TailReports { + res[i] = r.Title + } + return res +} + +func (c *Crash) TailReportsBytes() [][]byte { + res := make([][]byte, len(c.TailReports)) + for i, r := range c.TailReports { + res[i] = r.Report + } + return res +} + type ReproManagerView interface { RunRepro(ctx context.Context, crash *Crash) *ReproResult NeedRepro(crash *Crash) bool diff --git a/pkg/report/impact_score.go b/pkg/report/impact_score.go index 8139644d2a6a..63adb652ad41 100644 --- a/pkg/report/impact_score.go +++ b/pkg/report/impact_score.go @@ -4,8 +4,6 @@ package report import ( - "sort" - "github.com/google/syzkaller/pkg/report/crash" ) @@ -64,45 +62,3 @@ func TitlesToImpact(title string, otherTitles ...string) int { } return maxImpact } - -type TitleFreqRank struct { - Title string - Count int - Total int - Rank int -} - -func ExplainTitleStat(ts *titleStat) []*TitleFreqRank { - titleCount := map[string]int{} - var totalCount int - ts.visit(func(count int, titles ...string) { - uniq := map[string]bool{} - for _, title := range titles { - uniq[title] = true - } - for title := range uniq { - titleCount[title] += count - } - totalCount += count - }) - var res []*TitleFreqRank - for title, count := range titleCount { - res = append(res, &TitleFreqRank{ - Title: title, - Count: count, - Total: totalCount, - Rank: TitlesToImpact(title), - }) - } - sort.Slice(res, func(l, r int) bool { - if res[l].Rank != res[r].Rank { - return res[l].Rank > res[r].Rank - } - lTitle, rTitle := res[l].Title, res[r].Title - if titleCount[lTitle] != titleCount[rTitle] { - return titleCount[lTitle] > titleCount[rTitle] - } - return lTitle < rTitle - }) - return res -} diff --git a/pkg/report/report.go b/pkg/report/report.go index b4f93e3ac0bd..ab7c6b7e7b5f 100644 --- a/pkg/report/report.go +++ b/pkg/report/report.go @@ -955,6 +955,10 @@ func MergeReportBytes(reps []*Report) []byte { return res } -func SplitReportBytes(data []byte) [][]byte { - return bytes.Split(data, []byte(reportSeparator)) +func firstReportBytes(data []byte) []byte { + reps := bytes.Split(data, []byte(reportSeparator)) + if len(reps) == 0 { + return nil + } + return reps[0] } diff --git a/pkg/report/report_test.go b/pkg/report/report_test.go index 8c863fe8daff..45533d5f2a3b 100644 --- a/pkg/report/report_test.go +++ b/pkg/report/report_test.go @@ -511,37 +511,37 @@ BCDEF`), Truncate([]byte(`0123456789ABCDEF`), 0, 5)) DEF`), Truncate([]byte(`0123456789ABCDEF`), 4, 3)) } -func TestSplitReportBytes(t *testing.T) { +func TestFirstReportBytes(t *testing.T) { tests := []struct { - name string - input []byte - wantFirst string + name string + input []byte + want string }{ { - name: "empty", - input: nil, - wantFirst: "", + name: "empty", + input: nil, + want: "", }, { - name: "single", - input: []byte("report1"), - wantFirst: "report1", + name: "single", + input: []byte("report1"), + want: "report1", }, { - name: "split in the middle", - input: []byte("report1" + reportSeparator + "report2"), - wantFirst: "report1", + name: "split in the middle", + input: []byte("report1" + reportSeparator + "report2"), + want: "report1", }, { - name: "split in the middle, save new line", - input: []byte("report1\n" + reportSeparator + "report2"), - wantFirst: "report1\n", + name: "split in the middle, save new line", + input: []byte("report1\n" + reportSeparator + "report2"), + want: "report1\n", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - splitted := SplitReportBytes(test.input) - assert.Equal(t, test.wantFirst, string(splitted[0])) + first := firstReportBytes(test.input) + assert.Equal(t, test.want, string(first)) }) } } diff --git a/pkg/report/title_stat.go b/pkg/report/title_stat.go index 5b3cd3fb791c..7c43cf032d14 100644 --- a/pkg/report/title_stat.go +++ b/pkg/report/title_stat.go @@ -9,61 +9,54 @@ import ( "fmt" "maps" "os" + "sort" ) -func AddTitleStat(file string, reps []*Report) error { - var titles []string - for _, rep := range reps { - titles = append(titles, rep.Title) - } +func AddTitlesToStatFile(file string, titles []string) error { stat, err := ReadStatFile(file) if err != nil { - return fmt.Errorf("report.ReadStatFile: %w", err) + return fmt.Errorf("readStatFile: %w", err) } - stat.add(titles) - if err := writeStatFile(file, stat); err != nil { - return fmt.Errorf("writeStatFile: %w", err) + stat.Add(titles) + bytes, err := stat.ToBytes() + if err != nil { + return err } - return nil + return os.WriteFile(file, bytes, 0644) } -func ReadStatFile(file string) (*titleStat, error) { - stat := &titleStat{} +func ReadStatFile(file string) (*TitleStat, error) { + res := &TitleStat{} if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) { - return stat, nil + return res, nil } data, err := os.ReadFile(file) if err != nil { return nil, err } - if len(data) == 0 { - return stat, nil - } - if err := json.Unmarshal(data, stat); err != nil { - return nil, err - } - return stat, nil + return TitleStatFromBytes(data) } -func writeStatFile(file string, stat *titleStat) error { - data, err := json.MarshalIndent(stat, "", "\t") - if err != nil { - return err +func TitleStatFromBytes(data []byte) (*TitleStat, error) { + var ts TitleStat + if len(data) == 0 { + return &ts, nil } - if err := os.WriteFile(file, data, 0644); err != nil { - return err + if err := json.Unmarshal(data, &ts); err != nil { + return nil, err } - return nil + return &ts, nil } -type titleStatNodes map[string]*titleStat +type titleStatNodes map[string]*TitleStat -type titleStat struct { +type TitleStat struct { Count int Nodes titleStatNodes } -func (ts *titleStat) add(reps []string) { +func (ts *TitleStat) Add(reps []string) { + ts.Count++ if len(reps) == 0 { return } @@ -71,13 +64,12 @@ func (ts *titleStat) add(reps []string) { ts.Nodes = make(titleStatNodes) } if ts.Nodes[reps[0]] == nil { - ts.Nodes[reps[0]] = &titleStat{} + ts.Nodes[reps[0]] = &TitleStat{} } - ts.Nodes[reps[0]].Count++ - ts.Nodes[reps[0]].add(reps[1:]) + ts.Nodes[reps[0]].Add(reps[1:]) } -func (ts *titleStat) visit(cb func(int, ...string), titles ...string) { +func (ts *TitleStat) visit(cb func(int, ...string), titles ...string) { if len(ts.Nodes) == 0 { cb(ts.Count, titles...) return @@ -86,3 +78,67 @@ func (ts *titleStat) visit(cb func(int, ...string), titles ...string) { ts.Nodes[title].visit(cb, append(titles, title)...) } } + +func (ts *TitleStat) ToBytes() ([]byte, error) { + return json.MarshalIndent(ts, "", "\t") +} + +type TitleFreqRank struct { + Title string + Count int + Total int + Rank int +} + +func (ts *TitleStat) Explain() []*TitleFreqRank { + titleCount := map[string]int{} + + ts.visit(func(count int, titles ...string) { + uniq := map[string]bool{} + for _, title := range titles { + uniq[title] = true + } + for title := range uniq { + titleCount[title] += count + } + }) + var res []*TitleFreqRank + for title, count := range titleCount { + res = append(res, &TitleFreqRank{ + Title: title, + Count: count, + Total: ts.Count, + Rank: TitlesToImpact(title), + }) + } + sort.Slice(res, func(l, r int) bool { + if res[l].Rank != res[r].Rank { + return res[l].Rank > res[r].Rank + } + lTitle, rTitle := res[l].Title, res[r].Title + if titleCount[lTitle] != titleCount[rTitle] { + return titleCount[lTitle] > titleCount[rTitle] + } + return lTitle < rTitle + }) + return res +} + +// HigherRankTooltip generates a prioritized list of titles with a rank higher than firstTitle. +func HigherRankTooltip(firstTitle string, titlesInfo []*TitleFreqRank) string { + baseRank := TitlesToImpact(firstTitle) + res := "" + for _, ti := range titlesInfo { + if ti.Rank <= baseRank { + continue + } + res += fmt.Sprintf("[rank %2v, freq %5.1f%%] %s\n", + ti.Rank, + 100*float32(ti.Count)/float32(ti.Total), + ti.Title) + } + if res != "" { + return fmt.Sprintf("[rank %2v, originally] %s\n%s", baseRank, firstTitle, res) + } + return res +} diff --git a/pkg/report/title_stat_test.go b/pkg/report/title_stat_test.go index 216af456ca99..c1de3b72c598 100644 --- a/pkg/report/title_stat_test.go +++ b/pkg/report/title_stat_test.go @@ -4,36 +4,36 @@ package report import ( - "os" "testing" "github.com/stretchr/testify/assert" ) -func TestAddTitleStat(t *testing.T) { +func TestAddTitlesToStatFile(t *testing.T) { tests := []struct { - name string - base string - reps [][]*Report - want *titleStat + name string + titleChains [][]string + want *TitleStat }{ { name: "read empty", - want: &titleStat{}, + want: &TitleStat{}, }, { - name: "add single", - reps: [][]*Report{{{Title: "warning 1"}}}, - want: &titleStat{ + name: "add single", + titleChains: [][]string{{"warning 1"}}, + want: &TitleStat{ + Count: 1, Nodes: titleStatNodes{ "warning 1": {Count: 1}, }, }, }, { - name: "add chain", - reps: [][]*Report{{{Title: "warning 1"}, {Title: "warning 2"}}}, - want: &titleStat{ + name: "add chain", + titleChains: [][]string{{"warning 1", "warning 2"}}, + want: &TitleStat{ + Count: 1, Nodes: titleStatNodes{ "warning 1": {Count: 1, Nodes: titleStatNodes{ @@ -44,9 +44,10 @@ func TestAddTitleStat(t *testing.T) { }, }, { - name: "add multi chains", - reps: [][]*Report{{{Title: "warning 1"}, {Title: "warning 2"}}, {{Title: "warning 1"}, {Title: "warning 3"}}}, - want: &titleStat{ + name: "add multi chains", + titleChains: [][]string{{"warning 1", "warning 2"}, {"warning 1", "warning 3"}}, + want: &TitleStat{ + Count: 2, Nodes: titleStatNodes{ "warning 1": {Count: 2, Nodes: titleStatNodes{ @@ -62,15 +63,66 @@ func TestAddTitleStat(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() tmpFile := t.TempDir() + "/test.input" - err := os.WriteFile(tmpFile, []byte(test.base), 0644) - assert.NoError(t, err) - for _, reps := range test.reps { - err = AddTitleStat(tmpFile, reps) + for _, titles := range test.titleChains { + err := AddTitlesToStatFile(tmpFile, titles) assert.NoError(t, err) } - got, err := ReadStatFile(tmpFile) + statData, err := ReadStatFile(tmpFile) assert.NoError(t, err) - assert.Equal(t, test.want, got) + assert.Equal(t, test.want, statData) + }) + } +} + +func TestTitleStat_Explain(t *testing.T) { + tests := []struct { + name string + input [][]string + want []*TitleFreqRank + }{ + { + name: "empty", + want: nil, + }, + { + name: "single input", + input: [][]string{{"info"}}, + want: []*TitleFreqRank{ + { + Title: "info", + Count: 1, + Total: 1, + Rank: -1, + }, + }, + }, + { + name: "single nested input", + input: [][]string{{"info"}, {"info", "warning"}}, + want: []*TitleFreqRank{ + { + Title: "info", + Count: 1, + Total: 2, + Rank: -1, + }, + { + Title: "warning", + Count: 1, + Total: 2, + Rank: -1, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + ts := &TitleStat{} + for _, input := range test.input { + ts.Add(input) + } + assert.Equal(t, test.want, ts.Explain()) }) } } diff --git a/syz-manager/manager.go b/syz-manager/manager.go index 3f94bd23a714..bf5aca3956dc 100644 --- a/syz-manager/manager.go +++ b/syz-manager/manager.go @@ -714,8 +714,8 @@ func (mgr *Manager) saveCrash(crash *manager.Crash) bool { flags += " [suppressed]" } log.Logf(0, "VM %v: crash: %v%v", crash.InstanceIndex, crash.Report.Title, flags) - for i, report := range crash.TailReports { - log.Logf(0, "VM %v: crash(tail%d): %v%v", crash.InstanceIndex, i, report.Title, flags) + for i, t := range crash.TailTitles() { + log.Logf(0, "VM %v: crash(tail%d): %v%v", crash.InstanceIndex, i, t, flags) } if mgr.mode.FailOnCrashes { @@ -748,12 +748,14 @@ func (mgr *Manager) saveCrash(crash *manager.Crash) bool { dc := &dashapi.Crash{ BuildID: mgr.cfg.Tag, Title: crash.Title, + TailTitles: crash.TailTitles(), AltTitles: crash.AltTitles, Corrupted: crash.Corrupted, Suppressed: crash.Suppressed, Recipients: crash.Recipients.ToDash(), Log: crash.Output, - Report: report.SplitReportBytes(crash.Report.Report)[0], + Report: crash.Report.Report, + TailReports: crash.TailReportsBytes(), MachineInfo: crash.MachineInfo, } setGuiltyFiles(dc, crash.Report) @@ -900,12 +902,14 @@ func (mgr *Manager) saveRepro(res *manager.ReproResult) { dc := &dashapi.Crash{ BuildID: mgr.cfg.Tag, Title: reproReport.Title, + TailTitles: res.Crash.TailTitles(), AltTitles: reproReport.AltTitles, Suppressed: reproReport.Suppressed, Recipients: reproReport.Recipients.ToDash(), Log: output, Flags: crashFlags, - Report: report.SplitReportBytes(reproReport.Report)[0], + Report: reproReport.Report, + TailReports: res.Crash.TailReportsBytes(), ReproOpts: repro.Opts.Serialize(), ReproSyz: progText, ReproC: cprogText,