diff options
| -rw-r--r-- | dashboard/app/entities_spanner.go | 31 | ||||
| -rw-r--r-- | dashboard/app/graphs.go | 23 | ||||
| -rw-r--r-- | pkg/coveragedb/time_period.go | 52 | ||||
| -rw-r--r-- | pkg/coveragedb/time_period_test.go | 28 |
4 files changed, 109 insertions, 25 deletions
diff --git a/dashboard/app/entities_spanner.go b/dashboard/app/entities_spanner.go index abe7556d3..ff154986e 100644 --- a/dashboard/app/entities_spanner.go +++ b/dashboard/app/entities_spanner.go @@ -19,10 +19,11 @@ import ( type CoverageHistory struct { instrumented map[string]int64 covered map[string]int64 + periods map[coveragedb.TimePeriod]struct{} } // MergedCoverage uses dates, not time. -func MergedCoverage(ctx context.Context, ns string, fromDate, toDate civil.Date) (*CoverageHistory, error) { +func MergedCoverage(ctx context.Context, ns, periodType string) (*CoverageHistory, error) { projectID := os.Getenv("GOOGLE_CLOUD_PROJECT") client, err := coveragedb.NewClient(ctx, projectID) if err != nil { @@ -30,20 +31,29 @@ func MergedCoverage(ctx context.Context, ns string, fromDate, toDate civil.Date) } defer client.Close() + minDays, maxDays, err := coveragedb.MinMaxDays(periodType) + if err != nil { + return nil, fmt.Errorf("coveragedb.MinMaxDays: %w", err) + } + pOps, err := coveragedb.PeriodOps(periodType) + if err != nil { + return nil, fmt.Errorf("coveragedb.PeriodOps: %w", err) + } stmt := spanner.Statement{ SQL: ` select dateto as targetdate, + duration as days, cast(sum(instrumented) as INTEGER) as instrumented, cast(sum(covered) as INTEGER) as covered from merge_history join files on merge_history.session = files.session -where namespace=$1 and dateto>=$2 and dateto<=$3 -group by dateto`, +where namespace=$1 and duration>=$2 and duration<=$3 +group by dateto, duration`, Params: map[string]interface{}{ "p1": ns, - "p2": fromDate, - "p3": toDate, + "p2": minDays, + "p3": maxDays, }, } @@ -52,6 +62,7 @@ group by dateto`, res := &CoverageHistory{ instrumented: map[string]int64{}, covered: map[string]int64{}, + periods: map[coveragedb.TimePeriod]struct{}{}, } for { row, err := iter.Next() @@ -63,14 +74,24 @@ group by dateto`, } var r struct { Targetdate civil.Date + Days int64 Instrumented int64 Covered int64 } if err = row.ToStruct(&r); err != nil { return nil, fmt.Errorf("failed to row.ToStruct() spanner DB: %w", err) } + period := coveragedb.TimePeriod{DateTo: r.Targetdate, Days: int(r.Days)} + if !pOps.IsValidPeriod(period) { + continue + } res.instrumented[r.Targetdate.String()] = r.Instrumented res.covered[r.Targetdate.String()] = r.Covered + if _, found := res.periods[period]; found { + return nil, fmt.Errorf("db error: only one period expected for date %s, days %d", + period.DateTo.String(), period.Days) + } + res.periods[period] = struct{}{} } return res, nil } diff --git a/dashboard/app/graphs.go b/dashboard/app/graphs.go index 7e9a45c84..e4ded73d6 100644 --- a/dashboard/app/graphs.go +++ b/dashboard/app/graphs.go @@ -16,6 +16,7 @@ import ( "cloud.google.com/go/civil" "github.com/google/syzkaller/pkg/cover" + "github.com/google/syzkaller/pkg/coveragedb" db "google.golang.org/appengine/v2/datastore" ) @@ -232,18 +233,26 @@ func handleCoverageGraph(c context.Context, w http.ResponseWriter, r *http.Reque if err != nil { return err } - yesterday := civil.DateOf(time.Now().Add(-1 * 24 * time.Hour)) - monthAgo := yesterday.AddDays(-31) - hist, err := MergedCoverage(c, hdr.Namespace, monthAgo, yesterday) + periodType := r.FormValue("period") + if periodType == "" { + periodType = coveragedb.QuarterPeriod + } + if periodType != coveragedb.QuarterPeriod && periodType != coveragedb.MonthPeriod { + return fmt.Errorf("only quarter and month are allowed, but received %s instead", periodType) + } + hist, err := MergedCoverage(c, hdr.Namespace, periodType) if err != nil { return err } - dates := []string{} - for i := 31; i >= 0; i-- { - dates = append(dates, yesterday.AddDays(-i).String()) + pOps, err := coveragedb.PeriodOps(periodType) + if err != nil { + return err } + periodEndDates := coveragedb.GenNPeriodEndDatesTill(12, civil.DateOf(time.Now()), pOps) + cols := []uiGraphColumn{} - for _, date := range dates { + for _, periodEndDate := range periodEndDates { + date := periodEndDate.String() if _, ok := hist.covered[date]; !ok || hist.instrumented[date] == 0 { cols = append(cols, uiGraphColumn{Hint: date, Vals: []uiGraphValue{{IsNull: true}}}) } else { diff --git a/pkg/coveragedb/time_period.go b/pkg/coveragedb/time_period.go index 463ee6c41..b3a2aad73 100644 --- a/pkg/coveragedb/time_period.go +++ b/pkg/coveragedb/time_period.go @@ -4,6 +4,8 @@ package coveragedb import ( + "errors" + "slices" "sort" "cloud.google.com/go/civil" @@ -14,19 +16,59 @@ type TimePeriod struct { Days int } +const ( + MonthPeriod = "month" + QuarterPeriod = "quarter" +) + +var errUnknownTimePeriodType = errors.New("unknown time period type") + +func MinMaxDays(periodType string) (int, int, error) { + switch periodType { + case MonthPeriod: + return 28, 31, nil + case QuarterPeriod: + return 31 + 28 + 31, 31 + 30 + 31, nil + default: + return 0, 0, errUnknownTimePeriodType + } +} + +func PeriodOps(periodType string) (periodOps, error) { + switch periodType { + case MonthPeriod: + return &MonthPeriodOps{}, nil + case QuarterPeriod: + return &QuarterPeriodOps{}, nil + default: + return nil, errUnknownTimePeriodType + } +} + type periodOps interface { - isValidPeriod(p TimePeriod) bool + IsValidPeriod(p TimePeriod) bool lastPeriodDate(d civil.Date) civil.Date pointedPeriodDays(d civil.Date) int } +func GenNPeriodEndDatesTill(n int, d civil.Date, po periodOps) []civil.Date { + var res []civil.Date + for i := 0; i < n; i++ { + d = po.lastPeriodDate(d) + res = append(res, d) + d = d.AddDays(-po.pointedPeriodDays(d)) + } + slices.Reverse(res) + return res +} + type DayPeriodOps struct{} func (dpo *DayPeriodOps) lastPeriodDate(d civil.Date) civil.Date { return d } -func (dpo *DayPeriodOps) isValidPeriod(p TimePeriod) bool { +func (dpo *DayPeriodOps) IsValidPeriod(p TimePeriod) bool { return p.Days == 1 } @@ -43,7 +85,7 @@ func (m *MonthPeriodOps) lastPeriodDate(d civil.Date) civil.Date { return d.AddDays(-1) } -func (m *MonthPeriodOps) isValidPeriod(p TimePeriod) bool { +func (m *MonthPeriodOps) IsValidPeriod(p TimePeriod) bool { lmd := m.lastPeriodDate(p.DateTo) return lmd == p.DateTo && p.Days == lmd.Day } @@ -54,7 +96,7 @@ func (m *MonthPeriodOps) pointedPeriodDays(d civil.Date) int { type QuarterPeriodOps struct{} -func (q *QuarterPeriodOps) isValidPeriod(p TimePeriod) bool { +func (q *QuarterPeriodOps) IsValidPeriod(p TimePeriod) bool { lmd := q.lastPeriodDate(p.DateTo) return lmd == p.DateTo && p.Days == q.pointedPeriodDays(lmd) } @@ -83,7 +125,7 @@ func PeriodsToMerge(srcDates, mergedPeriods []TimePeriod, srcRows, mergedRows [] periodRows[periodID] += srcRows[i] } for i, period := range mergedPeriods { - if !ops.isValidPeriod(period) { + if !ops.IsValidPeriod(period) { continue } mergerPeriodID := period.DateTo diff --git a/pkg/coveragedb/time_period_test.go b/pkg/coveragedb/time_period_test.go index 85fb4b407..86201e4db 100644 --- a/pkg/coveragedb/time_period_test.go +++ b/pkg/coveragedb/time_period_test.go @@ -19,10 +19,14 @@ func TestDayPeriodOps(t *testing.T) { assert.Equal(t, "2024-02-20", ops.lastPeriodDate(d).String()) - assert.True(t, ops.isValidPeriod(goodPeriod)) - assert.False(t, ops.isValidPeriod(badPeriod)) + assert.True(t, ops.IsValidPeriod(goodPeriod)) + assert.False(t, ops.IsValidPeriod(badPeriod)) assert.Equal(t, 1, ops.pointedPeriodDays(d)) + + assert.Equal(t, + []civil.Date{{Year: 2024, Month: time.February, Day: 19}, {Year: 2024, Month: time.February, Day: 20}}, + GenNPeriodEndDatesTill(2, d, ops)) } func TestMonthPeriodOps(t *testing.T) { @@ -37,11 +41,15 @@ func TestMonthPeriodOps(t *testing.T) { assert.Equal(t, "2024-02-29", ops.lastPeriodDate(midMonthDate).String()) - assert.True(t, ops.isValidPeriod(goodPeriod)) - assert.False(t, ops.isValidPeriod(badPeriod1)) - assert.False(t, ops.isValidPeriod(badPeriod2)) + assert.True(t, ops.IsValidPeriod(goodPeriod)) + assert.False(t, ops.IsValidPeriod(badPeriod1)) + assert.False(t, ops.IsValidPeriod(badPeriod2)) assert.Equal(t, 29, ops.pointedPeriodDays(midMonthDate)) + + assert.Equal(t, + []civil.Date{{Year: 2024, Month: time.January, Day: 31}, {Year: 2024, Month: time.February, Day: 29}}, + GenNPeriodEndDatesTill(2, goodPeriod.DateTo, ops)) } func TestQuarterPeriodOps(t *testing.T) { @@ -57,11 +65,15 @@ func TestQuarterPeriodOps(t *testing.T) { assert.Equal(t, "2024-03-31", ops.lastPeriodDate(midQuarterDate).String()) - assert.True(t, ops.isValidPeriod(goodPeriod)) - assert.False(t, ops.isValidPeriod(badPeriod1)) - assert.False(t, ops.isValidPeriod(badPeriod2)) + assert.True(t, ops.IsValidPeriod(goodPeriod)) + assert.False(t, ops.IsValidPeriod(badPeriod1)) + assert.False(t, ops.IsValidPeriod(badPeriod2)) assert.Equal(t, 31+29+31, ops.pointedPeriodDays(midQuarterDate)) + + assert.Equal(t, + []civil.Date{{Year: 2023, Month: time.December, Day: 31}, {Year: 2024, Month: time.March, Day: 31}}, + GenNPeriodEndDatesTill(2, goodPeriod.DateTo, ops)) } func TestPeriodsToMerge(t *testing.T) { |
