diff options
| -rw-r--r-- | pkg/cover/heatmap.go | 174 | ||||
| -rw-r--r-- | pkg/cover/templates/heatmap.html | 116 | ||||
| -rw-r--r-- | tools/syz-cover/syz-cover.go | 49 |
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) |
