aboutsummaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorTaras Madan <tarasmadan@google.com>2024-07-10 18:07:37 +0200
committerTaras Madan <tarasmadan@google.com>2024-07-15 20:31:23 +0000
commit626375c87b8c76fa305742f4dceefccde55245b2 (patch)
treef75b0c3210c7f9d3390bebbdfffd66c8058652d5 /pkg
parent4178f02a1cc67951c9573229e920860d86049d57 (diff)
all: add the coverage map visualization to syz-cover
Diffstat (limited to 'pkg')
-rw-r--r--pkg/cover/heatmap.go174
-rw-r--r--pkg/cover/templates/heatmap.html116
2 files changed, 290 insertions, 0 deletions
diff --git a/pkg/cover/heatmap.go b/pkg/cover/heatmap.go
new file mode 100644
index 000000000..e5d2f4476
--- /dev/null
+++ b/pkg/cover/heatmap.go
@@ -0,0 +1,174 @@
+// Copyright 2024 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package cover
+
+import (
+ "context"
+ _ "embed"
+ "fmt"
+ "html/template"
+ "io"
+ "sort"
+ "strings"
+
+ "cloud.google.com/go/civil"
+ "cloud.google.com/go/spanner"
+ "github.com/google/syzkaller/pkg/spanner/coveragedb"
+ "golang.org/x/exp/maps"
+ "google.golang.org/api/iterator"
+)
+
+type templateHeatmapRow struct {
+ Items []*templateHeatmapRow
+ Name string
+ Coverage []int64
+ IsDir bool
+ Depth int
+
+ builder map[string]*templateHeatmapRow
+ instrumented map[civil.Date]int64
+ covered map[civil.Date]int64
+}
+
+type templateHeatmap struct {
+ Root *templateHeatmapRow
+ Dates []string
+}
+
+func (thm *templateHeatmapRow) addParts(depth int, pathLeft []string, instrumented, covered int64, dateto civil.Date) {
+ thm.instrumented[dateto] += instrumented
+ thm.covered[dateto] += covered
+ if len(pathLeft) == 0 {
+ return
+ }
+ nextElement := pathLeft[0]
+ isDir := len(pathLeft) > 1
+ if _, ok := thm.builder[nextElement]; !ok {
+ thm.builder[nextElement] = &templateHeatmapRow{
+ Name: nextElement,
+ Depth: depth,
+ IsDir: isDir,
+ builder: make(map[string]*templateHeatmapRow),
+ instrumented: make(map[civil.Date]int64),
+ covered: make(map[civil.Date]int64),
+ }
+ }
+ thm.builder[nextElement].addParts(depth+1, pathLeft[1:], instrumented, covered, dateto)
+}
+
+func (thm *templateHeatmapRow) prepareDataFor(dates []civil.Date) {
+ thm.Items = maps.Values(thm.builder)
+ sort.Slice(thm.Items, func(i, j int) bool {
+ if thm.Items[i].IsDir != thm.Items[j].IsDir {
+ return thm.Items[i].IsDir
+ }
+ return thm.Items[i].Name < thm.Items[j].Name
+ })
+ for _, d := range dates {
+ var dateCoverage int64
+ if thm.instrumented[d] != 0 {
+ dateCoverage = 100 * thm.covered[d] / thm.instrumented[d]
+ }
+ thm.Coverage = append(thm.Coverage, dateCoverage)
+ }
+ for _, item := range thm.builder {
+ item.prepareDataFor(dates)
+ }
+}
+
+type fileCoverageAndDate struct {
+ Filepath string
+ Instrumented int64
+ Covered int64
+ Dateto civil.Date
+}
+
+func filesCoverageToTemplateData(fCov []*fileCoverageAndDate) *templateHeatmap {
+ res := templateHeatmap{
+ Root: &templateHeatmapRow{
+ builder: map[string]*templateHeatmapRow{},
+ instrumented: map[civil.Date]int64{},
+ covered: map[civil.Date]int64{},
+ },
+ }
+ dates := map[civil.Date]struct{}{}
+ for _, fc := range fCov {
+ res.Root.addParts(
+ 0,
+ strings.Split(fc.Filepath, "/"),
+ fc.Instrumented,
+ fc.Covered,
+ fc.Dateto)
+ dates[fc.Dateto] = struct{}{}
+ }
+ sortedDates := maps.Keys(dates)
+ sort.Slice(sortedDates, func(i, j int) bool {
+ return sortedDates[i].Before(sortedDates[j])
+ })
+ for _, d := range sortedDates {
+ res.Dates = append(res.Dates, d.String())
+ }
+
+ res.Root.prepareDataFor(sortedDates)
+ return &res
+}
+
+func filesCoverageAndDates(ctx context.Context, ns string, fromDate, toDate civil.Date,
+) ([]*fileCoverageAndDate, error) {
+ client, err := coveragedb.NewClient(ctx, "syzkaller")
+ if err != nil {
+ return nil, fmt.Errorf("spanner.NewClient() failed: %s", err.Error())
+ }
+ defer client.Close()
+
+ stmt := spanner.Statement{
+ SQL: `
+select
+ dateto,
+ instrumented,
+ covered,
+ filepath
+from merge_history join files
+ on merge_history.session = files.session
+where namespace=$1 and dateto>=$2 and dateto<=$3
+`,
+ Params: map[string]interface{}{
+ "p1": ns,
+ "p2": fromDate,
+ "p3": toDate,
+ },
+ }
+
+ iter := client.Single().Query(ctx, stmt)
+ defer iter.Stop()
+ res := []*fileCoverageAndDate{}
+ for {
+ row, err := iter.Next()
+ if err == iterator.Done {
+ break
+ }
+ if err != nil {
+ return nil, fmt.Errorf("failed to iter.Next() spanner DB: %w", err)
+ }
+ var r fileCoverageAndDate
+ if err = row.ToStruct(&r); err != nil {
+ return nil, fmt.Errorf("failed to row.ToStruct() spanner DB: %w", err)
+ }
+ res = append(res, &r)
+ }
+ return res, nil
+}
+
+func DoHeatMap(w io.Writer, ns string, dateFrom, dateTo civil.Date) error {
+ covAndDates, err := filesCoverageAndDates(context.Background(), ns, dateFrom, dateTo)
+ if err != nil {
+ panic(err)
+ }
+ templateData := filesCoverageToTemplateData(covAndDates)
+ return heatmapTemplate.Execute(w, templateData)
+}
+
+//go:embed templates/heatmap.html
+var templatesHeatmap string
+var heatmapTemplate = template.Must(template.New("").Parse(templatesHeatmap))
diff --git a/pkg/cover/templates/heatmap.html b/pkg/cover/templates/heatmap.html
new file mode 100644
index 000000000..9f5d1da2c
--- /dev/null
+++ b/pkg/cover/templates/heatmap.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <style>
+ ul {
+ list-style-type: none;
+ padding-left: 0px;
+ }
+ .first_column {
+ display: inline-block;
+ width: 250px;
+ }
+ .date_column {
+ display: inline-block;
+ width: 50px;
+ }
+ .tree_depth_0 {width: 0px;}
+ .tree_depth_1 {width: 20px;}
+ .tree_depth_2 {width: 40px;}
+ .tree_depth_3 {width: 60px;}
+ .tree_depth_4 {width: 80px;}
+ .tree_depth_5 {width: 100px;}
+ .tree_depth_6 {width: 120px;}
+ .tree_depth_7 {width: 140px;}
+
+ .bold {font-weight: bold;}
+ .hover:hover {
+ background: #ffff99;
+ }
+ .caret {
+ cursor: pointer;
+ user-select: none;
+ }
+ .caret::before {
+ color: black;
+ content: "\25B6";
+ display: inline-block;
+ margin-right: 3px;
+ }
+ .caret-down::before {
+ transform: rotate(90deg);
+ }
+ .nested {
+ display: none;
+ }
+ .active {
+ display: block;
+ }
+ </style>
+</head>
+<body>
+<div>
+ <ul id="collapsible-list">
+ <li>
+ <div class="first_column bold">
+ date
+ </div>
+ {{ range $date := .Dates }}
+ <div class="date_column bold">
+ {{ $date }}
+ </div>
+ {{ end }}
+ </li>
+ <li>
+ <div class="first_column bold">
+ total
+ </div>
+ {{ range $cov := .Root.Coverage }}
+ <div class="date_column">
+ {{ $cov }}%
+ </div>
+ {{ end }}
+ </li>
+ <br>
+ {{template "dir" .Root}}
+ </ul>
+</div>
+</body>
+<script>
+ var togglers = document.getElementsByClassName("caret");
+ for (var i = 0; i < togglers.length; i++) {
+ togglers[i].addEventListener("click", function() {
+ this.classList.toggle("caret-down");
+ this.parentElement.parentElement.parentElement.querySelector(".nested").classList.toggle("active");
+ });
+ }
+ </script>
+</html>
+
+{{define "dir"}}
+ {{range $child := .Items}}
+ <li>
+ <div>
+ <div class="first_column" style="display: inline-block">
+ <div class="tree_depth_{{ $child.Depth }}" style="display: inline-block">
+ </div>
+ <div class="{{ if $child.IsDir }}caret hover{{ end }}"
+ style="display: inline-block">
+ {{$child.Name}}
+ </div>
+ </div>
+ {{ range $cov := $child.Coverage }}
+ <div class="date_column">
+ {{ $cov }}%
+ </div>
+ {{ end }}
+ </div>
+ {{ if $child.IsDir }}
+ <ul class="nested">
+ {{template "dir" $child}}
+ </ul>
+ {{ end }}
+ </li>
+ {{end}}
+{{end}} \ No newline at end of file