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))