Skip to content

Commit 7c7792d

Browse files
committed
dashboard/app: show manager unique coverage
1. Make heatmap testable, move out the spanner client instantiation. 2. Generate spannerdb.ReadOnlyTransaction mocks. 3. Generate spannerdb.RowIterator mocks. 4. Generate spannerdb.Row mocks. 5. Prepare spannerdb fixture. 6. Fixed html control name + value. 7. Added multiple tests.
1 parent d938113 commit 7c7792d

File tree

9 files changed

+559
-36
lines changed

9 files changed

+559
-36
lines changed

dashboard/app/coverage.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ import (
1414
"cloud.google.com/go/civil"
1515
"github.com/google/syzkaller/pkg/cover"
1616
"github.com/google/syzkaller/pkg/coveragedb"
17+
"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
1718
"github.com/google/syzkaller/pkg/covermerger"
1819
"github.com/google/syzkaller/pkg/validator"
1920
)
2021

21-
type funcStyleBodyJS func(ctx context.Context, projectID string, scope *cover.SelectScope, sss, managers []string,
22+
type funcStyleBodyJS func(
23+
ctx context.Context, client spannerclient.SpannerClient,
24+
scope *cover.SelectScope, onlyUnique bool, sss, managers []string,
2225
) (template.CSS, template.HTML, template.HTML, error)
2326

2427
func handleCoverageHeatmap(c context.Context, w http.ResponseWriter, r *http.Request) error {
@@ -71,16 +74,24 @@ func handleHeatmap(c context.Context, w http.ResponseWriter, r *http.Request, f
7174
slices.Sort(managers)
7275
slices.Sort(subsystems)
7376

77+
onlyUnique := r.FormValue("unique-only") == "1"
78+
79+
spannerClient, err := spannerclient.NewClient(c, "syzkaller")
80+
if err != nil {
81+
return fmt.Errorf("spanner.NewClient: %s", err.Error())
82+
}
83+
defer spannerClient.Close()
84+
7485
var style template.CSS
7586
var body, js template.HTML
76-
if style, body, js, err = f(c, "syzkaller",
87+
if style, body, js, err = f(c, spannerClient,
7788
&cover.SelectScope{
7889
Ns: hdr.Namespace,
7990
Subsystem: ss,
8091
Manager: manager,
8192
Periods: periods,
8293
},
83-
subsystems, managers); err != nil {
94+
onlyUnique, subsystems, managers); err != nil {
8495
return fmt.Errorf("failed to generate heatmap: %w", err)
8596
}
8697
return serveTemplate(w, "custom_content.html", struct {

dashboard/app/static/coverage.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function initUpdateForm(){
2020
}
2121
$('#target-subsystem').val(curUrlParams.get('subsystem'));
2222
$('#target-manager').val(curUrlParams.get('manager'));
23-
$("#only-unique").prop("checked", curUrlParams.get('subsystem') == "1");
23+
$("#unique-only").prop("checked", curUrlParams.get('unique-only') == "1");
2424
}
2525

2626
// This handler is called when user clicks on the coverage percentage.

pkg/cover/heatmap.go

Lines changed: 144 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
1818
_ "github.com/google/syzkaller/pkg/subsystem/lists"
1919
"golang.org/x/exp/maps"
20+
"golang.org/x/sync/errgroup"
2021
"google.golang.org/api/iterator"
2122
)
2223

@@ -115,6 +116,12 @@ type fileCoverageWithDetails struct {
115116
Subsystems []string
116117
}
117118

119+
type fileCoverageWithLineInfo struct {
120+
fileCoverageWithDetails
121+
LinesInstrumented []int64
122+
HitCounts []int64
123+
}
124+
118125
type pageColumnTarget struct {
119126
TimePeriod coveragedb.TimePeriod
120127
Commit string
@@ -157,18 +164,17 @@ func filesCoverageToTemplateData(fCov []*fileCoverageWithDetails) *templateHeatm
157164
return &res
158165
}
159166

160-
func filesCoverageWithDetailsStmt(ns, subsystem, manager string, timePeriod coveragedb.TimePeriod) spanner.Statement {
167+
func filesCoverageWithDetailsStmt(ns, subsystem, manager string, timePeriod coveragedb.TimePeriod, withLines bool,
168+
) spanner.Statement {
161169
if manager == "" {
162170
manager = "*"
163171
}
172+
selectColumns := "commit, instrumented, covered, files.filepath, subsystems"
173+
if withLines {
174+
selectColumns += ", linesinstrumented, hitcounts"
175+
}
164176
stmt := spanner.Statement{
165-
SQL: `
166-
select
167-
commit,
168-
instrumented,
169-
covered,
170-
files.filepath,
171-
subsystems
177+
SQL: "select " + selectColumns + `
172178
from merge_history
173179
join files
174180
on merge_history.session = files.session
@@ -187,37 +193,143 @@ where
187193
stmt.SQL += " and $5=ANY(subsystems)"
188194
stmt.Params["p5"] = subsystem
189195
}
196+
stmt.SQL += "\norder by files.filepath"
190197
return stmt
191198
}
192199

193-
func filesCoverageWithDetails(ctx context.Context, projectID string, scope *SelectScope,
194-
) ([]*fileCoverageWithDetails, error) {
195-
client, err := spannerclient.NewClient(ctx, projectID)
200+
func readCoverage(iterManager spannerclient.RowIterator) ([]*fileCoverageWithDetails, error) {
201+
res := []*fileCoverageWithDetails{}
202+
ch := make(chan *fileCoverageWithDetails)
203+
var err error
204+
go func() {
205+
defer close(ch)
206+
err = readIterToChan(context.Background(), iterManager, ch)
207+
}()
208+
for fc := range ch {
209+
res = append(res, fc)
210+
}
196211
if err != nil {
197-
return nil, fmt.Errorf("spanner.NewClient() failed: %s", err.Error())
212+
return nil, fmt.Errorf("readIterToChan: %w", err)
198213
}
199-
defer client.Close()
214+
return res, nil
215+
}
200216

217+
// Unique coverage from specific manager is more expensive to get.
218+
// We get unique coverage comparing manager and total coverage on the AppEngine side.
219+
func readCoverageUniq(full, mgr spannerclient.RowIterator,
220+
) ([]*fileCoverageWithDetails, error) {
221+
eg, ctx := errgroup.WithContext(context.Background())
222+
fullCh := make(chan *fileCoverageWithLineInfo)
223+
eg.Go(func() error {
224+
defer close(fullCh)
225+
return readIterToChan(ctx, full, fullCh)
226+
})
227+
partCh := make(chan *fileCoverageWithLineInfo)
228+
eg.Go(func() error {
229+
defer close(partCh)
230+
return readIterToChan(ctx, mgr, partCh)
231+
})
201232
res := []*fileCoverageWithDetails{}
202-
for _, timePeriod := range scope.Periods {
203-
stmt := filesCoverageWithDetailsStmt(scope.Ns, scope.Subsystem, scope.Manager, timePeriod)
204-
iter := client.Single().Query(ctx, stmt)
205-
defer iter.Stop()
206-
for {
207-
row, err := iter.Next()
208-
if err == iterator.Done {
209-
break
233+
eg.Go(func() error {
234+
partCov := <-partCh
235+
for fullCov := range fullCh {
236+
if partCov == nil || partCov.Filepath > fullCov.Filepath {
237+
// No pair for the file in full aggregation is available.
238+
cov := fullCov.fileCoverageWithDetails
239+
cov.Covered = 0
240+
res = append(res, &cov)
241+
continue
242+
}
243+
if partCov.Filepath == fullCov.Filepath {
244+
if len(partCov.LinesInstrumented) > len(fullCov.LinesInstrumented) ||
245+
len(partCov.HitCounts) > len(fullCov.HitCounts) ||
246+
partCov.Commit != fullCov.Commit {
247+
return fmt.Errorf("db record for file %s don't match", fullCov.Filepath)
248+
}
249+
res = append(res, uniqCoverage(fullCov, partCov))
250+
partCov = <-partCh
251+
continue
210252
}
253+
// Partial coverage is a subset of full coverage.
254+
// File can't exist only in partial set.
255+
return fmt.Errorf("currupted db, file %s can't exist", partCov.Filepath)
256+
}
257+
return nil
258+
})
259+
if err := eg.Wait(); err != nil {
260+
return nil, fmt.Errorf("eg.Wait: %w", err)
261+
}
262+
return res, nil
263+
}
264+
265+
func uniqCoverage(full, partial *fileCoverageWithLineInfo) *fileCoverageWithDetails {
266+
res := full.fileCoverageWithDetails // Use Instrumented count from full aggregation.
267+
res.Covered = 0 // We're recalculating only the covered lines.
268+
fullCov := map[int64]int64{}
269+
for i, ln := range full.LinesInstrumented {
270+
fullCov[ln] = full.HitCounts[i]
271+
}
272+
for i, ln := range partial.LinesInstrumented {
273+
if hitCount, exist := fullCov[ln]; exist && hitCount > 0 && hitCount == partial.HitCounts[i] {
274+
res.Covered++
275+
}
276+
}
277+
return &res
278+
}
279+
280+
func readIterToChan[K fileCoverageWithLineInfo | fileCoverageWithDetails](
281+
ctx context.Context, iter spannerclient.RowIterator, ch chan<- *K) error {
282+
for {
283+
row, err := iter.Next()
284+
if err == iterator.Done {
285+
break
286+
}
287+
if err != nil {
288+
return fmt.Errorf("iter.Next: %w", err)
289+
}
290+
var r K
291+
if err = row.ToStruct(&r); err != nil {
292+
return fmt.Errorf("row.ToStruct: %w", err)
293+
}
294+
select {
295+
case ch <- &r:
296+
case <-ctx.Done():
297+
return nil
298+
}
299+
}
300+
return nil
301+
}
302+
303+
func filesCoverageWithDetails(
304+
ctx context.Context, client spannerclient.SpannerClient, scope *SelectScope, onlyUnique bool,
305+
) ([]*fileCoverageWithDetails, error) {
306+
var res []*fileCoverageWithDetails
307+
for _, timePeriod := range scope.Periods {
308+
needLinesDetails := onlyUnique
309+
iterManager := client.Single().Query(ctx,
310+
filesCoverageWithDetailsStmt(scope.Ns, scope.Subsystem, scope.Manager, timePeriod, needLinesDetails))
311+
defer iterManager.Stop()
312+
313+
var err error
314+
var periodRes []*fileCoverageWithDetails
315+
if onlyUnique {
316+
iterAll := client.Single().Query(ctx,
317+
filesCoverageWithDetailsStmt(scope.Ns, scope.Subsystem, "", timePeriod, needLinesDetails))
318+
defer iterAll.Stop()
319+
periodRes, err = readCoverageUniq(iterAll, iterManager)
211320
if err != nil {
212-
return nil, fmt.Errorf("failed to iter.Next() spanner DB: %w", err)
321+
return nil, fmt.Errorf("uniqueFilesCoverageWithDetails: %w", err)
213322
}
214-
var r fileCoverageWithDetails
215-
if err = row.ToStruct(&r); err != nil {
216-
return nil, fmt.Errorf("failed to row.ToStruct() spanner DB: %w", err)
323+
} else {
324+
periodRes, err = readCoverage(iterManager)
325+
if err != nil {
326+
return nil, fmt.Errorf("readCoverage: %w", err)
217327
}
328+
}
329+
for _, r := range periodRes {
218330
r.TimePeriod = timePeriod
219-
res = append(res, &r)
220331
}
332+
res = append(res, periodRes...)
221333
}
222334
return res, nil
223335
}
@@ -252,9 +364,10 @@ type SelectScope struct {
252364
Periods []coveragedb.TimePeriod
253365
}
254366

255-
func DoHeatMapStyleBodyJS(ctx context.Context, projectID string, scope *SelectScope, sss, managers []string,
367+
func DoHeatMapStyleBodyJS(
368+
ctx context.Context, client spannerclient.SpannerClient, scope *SelectScope, onlyUnique bool, sss, managers []string,
256369
) (template.CSS, template.HTML, template.HTML, error) {
257-
covAndDates, err := filesCoverageWithDetails(ctx, projectID, scope)
370+
covAndDates, err := filesCoverageWithDetails(ctx, client, scope, onlyUnique)
258371
if err != nil {
259372
return "", "", "", fmt.Errorf("failed to filesCoverageWithDetails: %w", err)
260373
}
@@ -264,9 +377,10 @@ func DoHeatMapStyleBodyJS(ctx context.Context, projectID string, scope *SelectSc
264377
return stylesBodyJSTemplate(templData)
265378
}
266379

267-
func DoSubsystemsHeatMapStyleBodyJS(ctx context.Context, projectID string, scope *SelectScope, sss, managers []string,
380+
func DoSubsystemsHeatMapStyleBodyJS(
381+
ctx context.Context, client spannerclient.SpannerClient, scope *SelectScope, onlyUnique bool, sss, managers []string,
268382
) (template.CSS, template.HTML, template.HTML, error) {
269-
covWithDetails, err := filesCoverageWithDetails(ctx, projectID, scope)
383+
covWithDetails, err := filesCoverageWithDetails(ctx, client, scope, onlyUnique)
270384
if err != nil {
271385
panic(err)
272386
}

0 commit comments

Comments
 (0)