aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pkg/cover/heatmap.go174
-rw-r--r--pkg/cover/templates/heatmap.html116
-rw-r--r--tools/syz-cover/syz-cover.go49
3 files changed, 329 insertions, 10 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
diff --git a/tools/syz-cover/syz-cover.go b/tools/syz-cover/syz-cover.go
index def54d549..bf1c02ae4 100644
--- a/tools/syz-cover/syz-cover.go
+++ b/tools/syz-cover/syz-cover.go
@@ -29,7 +29,9 @@ import (
"os/exec"
"strconv"
"strings"
+ "time"
+ "cloud.google.com/go/civil"
"github.com/google/syzkaller/pkg/cover"
"github.com/google/syzkaller/pkg/mgrconfig"
"github.com/google/syzkaller/pkg/osutil"
@@ -37,18 +39,45 @@ import (
"github.com/google/syzkaller/pkg/vminfo"
)
+var (
+ flagConfig = flag.String("config", "", "configuration file")
+ flagModules = flag.String("modules", "",
+ "modules JSON info obtained from /modules (optional)")
+ flagExportCSV = flag.String("csv", "", "export coverage data in csv format (optional)")
+ flagExportLineJSON = flag.String("json", "", "export coverage data with source line info in json format (optional)")
+ flagExportJSONL = flag.String("jsonl", "", "export jsonl coverage data (optional)")
+ flagExportHTML = flag.String("html", "", "save coverage HTML report to file (optional)")
+ flagNsHeatmap = flag.String("heatmap", "", "generate namespace heatmap")
+ flagDateFrom = flag.String("from",
+ civil.DateOf(time.Now().Add(-14*24*time.Hour)).String(), "heatmap date from(optional)")
+ flagDateTo = flag.String("to",
+ civil.DateOf(time.Now()).String(), "heatmap date to(optional)")
+)
+
+func toolBuildNsHeatmap() {
+ buf := new(bytes.Buffer)
+ var dateFrom, dateTo civil.Date
+ var err error
+ if dateFrom, err = civil.ParseDate(*flagDateFrom); err != nil {
+ tool.Failf("failed to parse date from: %v", err.Error())
+ }
+ if dateTo, err = civil.ParseDate(*flagDateTo); err != nil {
+ tool.Failf("failed to parse date to: %v", err.Error())
+ }
+ if err = cover.DoHeatMap(buf, *flagNsHeatmap, dateFrom, dateTo); err != nil {
+ tool.Fail(err)
+ }
+ if err = osutil.WriteFile(*flagNsHeatmap+".html", buf.Bytes()); err != nil {
+ tool.Fail(err)
+ }
+}
+
func main() {
- var (
- flagConfig = flag.String("config", "", "configuration file")
- flagModules = flag.String("modules", "",
- "modules JSON info obtained from /modules (optional)")
- flagExportCSV = flag.String("csv", "", "export coverage data in csv format (optional)")
- flagExportLineJSON = flag.String("json", "", "export coverage data with source line info in json format (optional)")
- flagExportJSONL = flag.String("jsonl", "", "export jsonl coverage data (optional)")
- flagExportHTML = flag.String("html", "", "save coverage HTML report to file (optional)")
- )
defer tool.Init()()
-
+ if *flagNsHeatmap != "" {
+ toolBuildNsHeatmap()
+ return
+ }
cfg, err := mgrconfig.LoadFile(*flagConfig)
if err != nil {
tool.Fail(err)