github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/cover/heatmap.go (about) 1 // Copyright 2024 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 cover 5 6 import ( 7 "bytes" 8 "context" 9 _ "embed" 10 "fmt" 11 "html/template" 12 "slices" 13 "sort" 14 "strings" 15 16 "github.com/google/syzkaller/pkg/coveragedb" 17 "github.com/google/syzkaller/pkg/coveragedb/spannerclient" 18 _ "github.com/google/syzkaller/pkg/subsystem/lists" 19 "golang.org/x/exp/maps" 20 ) 21 22 type templateHeatmapRow struct { 23 Items []*templateHeatmapRow 24 Name string 25 Coverage []int64 // in percent 26 Covered []int64 // in lines count 27 IsDir bool 28 Depth int 29 Summary int64 // right column, may be negative to show drops 30 Tooltips []string 31 FileCoverageLink []string 32 33 builder map[string]*templateHeatmapRow 34 instrumented map[coveragedb.TimePeriod]int64 35 covered map[coveragedb.TimePeriod]int64 36 filePath string 37 } 38 39 type templateHeatmap struct { 40 Root *templateHeatmapRow 41 Periods []string 42 Subsystems []string 43 Managers []string 44 } 45 46 func (th *templateHeatmap) Filter(pred func(*templateHeatmapRow) bool) { 47 th.Root.filter(pred) 48 } 49 50 func (th *templateHeatmap) Transform(f func(*templateHeatmapRow)) { 51 th.Root.transform(f) 52 } 53 54 func (th *templateHeatmap) Sort(pred func(*templateHeatmapRow, *templateHeatmapRow) int) { 55 th.Root.sort(pred) 56 } 57 58 func (thm *templateHeatmapRow) transform(f func(*templateHeatmapRow)) { 59 for _, item := range thm.Items { 60 item.transform(f) 61 } 62 f(thm) 63 } 64 65 func (thm *templateHeatmapRow) filter(pred func(*templateHeatmapRow) bool) { 66 var filteredItems []*templateHeatmapRow 67 for _, item := range thm.Items { 68 item.filter(pred) 69 if pred(item) { 70 filteredItems = append(filteredItems, item) 71 } 72 } 73 thm.Items = filteredItems 74 } 75 76 func (thm *templateHeatmapRow) sort(pred func(*templateHeatmapRow, *templateHeatmapRow) int) { 77 for _, item := range thm.Items { 78 item.sort(pred) 79 } 80 slices.SortFunc(thm.Items, pred) 81 } 82 83 func (thm *templateHeatmapRow) addParts(depth int, pathLeft []string, filePath string, instrumented, covered int64, 84 timePeriod coveragedb.TimePeriod) { 85 thm.instrumented[timePeriod] += instrumented 86 thm.covered[timePeriod] += covered 87 if len(pathLeft) == 0 { 88 return 89 } 90 nextElement := pathLeft[0] 91 isDir := len(pathLeft) > 1 92 fp := "" 93 if !isDir { 94 fp = filePath 95 } 96 if _, ok := thm.builder[nextElement]; !ok { 97 thm.builder[nextElement] = &templateHeatmapRow{ 98 Name: nextElement, 99 Depth: depth, 100 IsDir: isDir, 101 filePath: fp, 102 builder: make(map[string]*templateHeatmapRow), 103 instrumented: make(map[coveragedb.TimePeriod]int64), 104 covered: make(map[coveragedb.TimePeriod]int64), 105 } 106 } 107 thm.builder[nextElement].addParts(depth+1, pathLeft[1:], filePath, instrumented, covered, timePeriod) 108 } 109 110 func (thm *templateHeatmapRow) prepareDataFor(pageColumns []pageColumnTarget) { 111 for _, item := range thm.builder { 112 thm.Items = append(thm.Items, item) 113 } 114 sort.Slice(thm.Items, func(i, j int) bool { 115 if thm.Items[i].IsDir != thm.Items[j].IsDir { 116 return thm.Items[i].IsDir 117 } 118 return thm.Items[i].Name < thm.Items[j].Name 119 }) 120 for _, pageColumn := range pageColumns { 121 var dateCoverage int64 122 tp := pageColumn.TimePeriod 123 if thm.instrumented[tp] != 0 { 124 dateCoverage = Percent(thm.covered[tp], thm.instrumented[tp]) 125 } 126 thm.Coverage = append(thm.Coverage, dateCoverage) 127 thm.Covered = append(thm.Covered, thm.covered[tp]) 128 thm.Tooltips = append(thm.Tooltips, fmt.Sprintf("Instrumented:\t%d blocks\nCovered:\t%d blocks", 129 thm.instrumented[tp], thm.covered[tp])) 130 if !thm.IsDir { 131 thm.FileCoverageLink = append(thm.FileCoverageLink, 132 fmt.Sprintf("/coverage/file?dateto=%s&period=%s&commit=%s&filepath=%s", 133 tp.DateTo.String(), 134 tp.Type, 135 pageColumn.Commit, 136 thm.filePath)) 137 } 138 } 139 if len(pageColumns) > 0 { 140 lastDate := pageColumns[len(pageColumns)-1].TimePeriod 141 thm.Summary = thm.instrumented[lastDate] 142 } 143 for _, item := range thm.builder { 144 item.prepareDataFor(pageColumns) 145 } 146 } 147 148 func (thm *templateHeatmapRow) Visit(v func(string, int64, bool), path ...string) { 149 curPath := append(path, thm.Name) 150 v(strings.Join(curPath, "/"), thm.Summary, thm.IsDir) 151 for _, item := range thm.Items { 152 item.Visit(v, curPath...) 153 } 154 } 155 156 type pageColumnTarget struct { 157 TimePeriod coveragedb.TimePeriod 158 Commit string 159 } 160 161 func FilesCoverageToTemplateData(fCov []*coveragedb.FileCoverageWithDetails) *templateHeatmap { 162 res := templateHeatmap{ 163 Root: &templateHeatmapRow{ 164 IsDir: true, 165 builder: map[string]*templateHeatmapRow{}, 166 instrumented: map[coveragedb.TimePeriod]int64{}, 167 covered: map[coveragedb.TimePeriod]int64{}, 168 }, 169 } 170 columns := map[pageColumnTarget]struct{}{} 171 for _, fc := range fCov { 172 var pathLeft []string 173 if fc.Subsystem != "" { 174 pathLeft = append(pathLeft, fc.Subsystem) 175 } 176 res.Root.addParts( 177 0, 178 append(pathLeft, strings.Split(fc.Filepath, "/")...), 179 fc.Filepath, 180 fc.Instrumented, 181 fc.Covered, 182 fc.TimePeriod) 183 columns[pageColumnTarget{TimePeriod: fc.TimePeriod, Commit: fc.Commit}] = struct{}{} 184 } 185 targetDateAndCommits := maps.Keys(columns) 186 sort.Slice(targetDateAndCommits, func(i, j int) bool { 187 return targetDateAndCommits[i].TimePeriod.DateTo.Before(targetDateAndCommits[j].TimePeriod.DateTo) 188 }) 189 for _, tdc := range targetDateAndCommits { 190 tp := tdc.TimePeriod 191 res.Periods = append(res.Periods, fmt.Sprintf("%s(%d)", tp.DateTo.String(), tp.Days)) 192 } 193 194 res.Root.prepareDataFor(targetDateAndCommits) 195 return &res 196 } 197 198 type StyleBodyJS struct { 199 Style template.CSS 200 Body template.HTML 201 JS template.HTML 202 } 203 204 func stylesBodyJSTemplate(templData *templateHeatmap, 205 ) (template.CSS, template.HTML, template.HTML, error) { 206 var styles, body, js bytes.Buffer 207 if err := heatmapTemplate.ExecuteTemplate(&styles, "style", templData); err != nil { 208 return "", "", "", fmt.Errorf("failed to get styles: %w", err) 209 } 210 if err := heatmapTemplate.ExecuteTemplate(&body, "body", templData); err != nil { 211 return "", "", "", fmt.Errorf("failed to get body: %w", err) 212 } 213 if err := heatmapTemplate.ExecuteTemplate(&js, "js", templData); err != nil { 214 return "", "", "", fmt.Errorf("failed to get js: %w", err) 215 } 216 return template.CSS(styles.String()), 217 template.HTML(body.String()), 218 template.HTML(js.Bytes()), nil 219 } 220 221 type Format struct { 222 FilterMinCoveredLinesDrop int 223 OrderByCoveredLinesDrop bool 224 DropCoveredLines0 bool 225 } 226 227 func DoHeatMapStyleBodyJS( 228 ctx context.Context, client spannerclient.SpannerClient, scope *coveragedb.SelectScope, onlyUnique bool, 229 sss, managers []string, dataFilters Format) (template.CSS, template.HTML, template.HTML, error) { 230 covAndDates, err := coveragedb.FilesCoverageWithDetails(ctx, client, scope, onlyUnique) 231 if err != nil { 232 return "", "", "", fmt.Errorf("failed to FilesCoverageWithDetails: %w", err) 233 } 234 templData := FilesCoverageToTemplateData(covAndDates) 235 templData.Subsystems = sss 236 templData.Managers = managers 237 FormatResult(templData, dataFilters) 238 239 return stylesBodyJSTemplate(templData) 240 } 241 242 func DoSubsystemsHeatMapStyleBodyJS( 243 ctx context.Context, client spannerclient.SpannerClient, scope *coveragedb.SelectScope, onlyUnique bool, 244 sss, managers []string, format Format) (template.CSS, template.HTML, template.HTML, error) { 245 covWithDetails, err := coveragedb.FilesCoverageWithDetails(ctx, client, scope, onlyUnique) 246 if err != nil { 247 panic(err) 248 } 249 var ssCovAndDates []*coveragedb.FileCoverageWithDetails 250 for _, cwd := range covWithDetails { 251 for _, ssName := range cwd.Subsystems { 252 newRecord := coveragedb.FileCoverageWithDetails{ 253 Filepath: cwd.Filepath, 254 Subsystem: ssName, 255 Instrumented: cwd.Instrumented, 256 Covered: cwd.Covered, 257 TimePeriod: cwd.TimePeriod, 258 Commit: cwd.Commit, 259 } 260 ssCovAndDates = append(ssCovAndDates, &newRecord) 261 } 262 } 263 templData := FilesCoverageToTemplateData(ssCovAndDates) 264 templData.Managers = managers 265 FormatResult(templData, format) 266 return stylesBodyJSTemplate(templData) 267 } 268 269 func FormatResult(thm *templateHeatmap, format Format) { 270 // Remove file coverage lines with drop less than a threshold. 271 if format.FilterMinCoveredLinesDrop > 0 { 272 thm.Filter(func(row *templateHeatmapRow) bool { 273 return row.IsDir || 274 slices.Max(row.Covered)-row.Covered[len(row.Covered)-1] >= int64(format.FilterMinCoveredLinesDrop) 275 }) 276 } 277 // Remove file coverage lines with zero coverage during the analysis period. 278 if format.DropCoveredLines0 { 279 thm.Filter(func(row *templateHeatmapRow) bool { 280 return slices.Max(row.Covered) > 0 281 }) 282 } 283 // Drop empty dir elements. 284 thm.Filter(func(row *templateHeatmapRow) bool { 285 return !row.IsDir || len(row.Items) > 0 286 }) 287 // The files are sorted lexicographically by default. 288 if format.OrderByCoveredLinesDrop { 289 thm.Sort(func(row1 *templateHeatmapRow, row2 *templateHeatmapRow) int { 290 row1CoveredDrop := slices.Max(row1.Covered) - row1.Covered[len(row1.Covered)-1] 291 row2CoveredDrop := slices.Max(row2.Covered) - row2.Covered[len(row2.Covered)-1] 292 return int(row2CoveredDrop - row1CoveredDrop) 293 }) 294 // We want to show the coverage drop numbers instead of total instrumented blocks. 295 thm.Transform(func(row *templateHeatmapRow) { 296 if !row.IsDir { 297 row.Summary = -1 * (slices.Max(row.Covered) - row.Covered[len(row.Covered)-1]) 298 return 299 } 300 row.Summary = 0 301 for _, item := range row.Items { 302 if item.Summary < 0 { // only the items with coverage drop 303 row.Summary += item.Summary 304 } 305 } 306 }) 307 } 308 } 309 310 func approximateInstrumented(points int64) string { 311 dim := " " 312 if abs(points) > 10000 { 313 dim = "K" 314 points /= 1000 315 } 316 return fmt.Sprintf("%d%s", points, dim) 317 } 318 319 func abs(a int64) int64 { 320 if a < 0 { 321 return -a 322 } 323 return a 324 } 325 326 //go:embed templates/heatmap.html 327 var templatesHeatmap string 328 var templateHeatmapFuncs = template.FuncMap{ 329 "approxInstr": approximateInstrumented, 330 } 331 var heatmapTemplate = template.Must(template.New("").Funcs(templateHeatmapFuncs).Parse(templatesHeatmap))