|  | package dfbuilder | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "net/url" | 
|  | "testing" | 
|  | "time" | 
|  |  | 
|  | "github.com/stretchr/testify/assert" | 
|  | "github.com/stretchr/testify/require" | 
|  | "go.skia.org/infra/go/paramtools" | 
|  | "go.skia.org/infra/go/query" | 
|  | "go.skia.org/infra/perf/go/config" | 
|  | "go.skia.org/infra/perf/go/dataframe" | 
|  | perfgit "go.skia.org/infra/perf/go/git" | 
|  | "go.skia.org/infra/perf/go/git/gittest" | 
|  | "go.skia.org/infra/perf/go/progress" | 
|  | "go.skia.org/infra/perf/go/sql/sqltest" | 
|  | "go.skia.org/infra/perf/go/tracestore" | 
|  | "go.skia.org/infra/perf/go/tracestore/sqltracestore" | 
|  | "go.skia.org/infra/perf/go/types" | 
|  | ) | 
|  |  | 
|  | var ( | 
|  | cfg = &config.InstanceConfig{ | 
|  | DataStoreConfig: config.DataStoreConfig{ | 
|  | TileSize: 256, | 
|  | }, | 
|  | } | 
|  | ) | 
|  |  | 
|  | func TestBuildTraceMapper(t *testing.T) { | 
|  |  | 
|  | db := sqltest.NewCockroachDBForTests(t, "dfbuilder") | 
|  |  | 
|  | store, err := sqltracestore.New(db, cfg.DataStoreConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | tileMap := buildTileMapOffsetToIndex([]types.CommitNumber{0, 1, 255, 256, 257}, store) | 
|  | expected := tileMapOffsetToIndex{0: map[int32]int32{0: 0, 1: 1, 255: 2}, 1: map[int32]int32{0: 3, 1: 4}} | 
|  | assert.Equal(t, expected, tileMap) | 
|  |  | 
|  | tileMap = buildTileMapOffsetToIndex([]types.CommitNumber{}, store) | 
|  | expected = tileMapOffsetToIndex{} | 
|  | assert.Equal(t, expected, tileMap) | 
|  | } | 
|  |  | 
|  | // The keys of values are structured keys, not encoded keys. | 
|  | func addValuesAtIndex(store tracestore.TraceStore, index types.CommitNumber, keyValues map[string]float32, filename string, ts time.Time) error { | 
|  | ps := paramtools.ParamSet{} | 
|  | params := []paramtools.Params{} | 
|  | values := []float32{} | 
|  | for k, v := range keyValues { | 
|  | p, err := query.ParseKey(k) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | ps.AddParams(p) | 
|  | params = append(params, p) | 
|  | values = append(values, v) | 
|  | } | 
|  | return store.WriteTraces(context.Background(), index, params, values, ps, filename, ts) | 
|  | } | 
|  |  | 
|  | func TestBuildNew(t *testing.T) { | 
|  | ctx := context.Background() | 
|  |  | 
|  | ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t) | 
|  | g, err := perfgit.New(ctx, true, db, instanceConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | instanceConfig.DataStoreConfig.TileSize = 6 | 
|  |  | 
|  | store, err := sqltracestore.New(db, instanceConfig.DataStoreConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | builder := NewDataFrameBuilderFromTraceStore(g, store, 2, doNotFilterParentTraces) | 
|  |  | 
|  | // Add some points to the first and second tile. | 
|  | err = addValuesAtIndex(store, 0, map[string]float32{ | 
|  | ",arch=x86,config=8888,": 1.2, | 
|  | ",arch=x86,config=565,":  2.1, | 
|  | ",arch=arm,config=8888,": 100.5, | 
|  | }, "gs://foo.json", time.Now()) | 
|  | assert.NoError(t, err) | 
|  | err = addValuesAtIndex(store, 1, map[string]float32{ | 
|  | ",arch=x86,config=8888,": 1.3, | 
|  | ",arch=x86,config=565,":  2.2, | 
|  | ",arch=arm,config=8888,": 100.6, | 
|  | }, "gs://foo.json", time.Now()) | 
|  | assert.NoError(t, err) | 
|  | err = addValuesAtIndex(store, 7, map[string]float32{ | 
|  | ",arch=x86,config=8888,": 1.0, | 
|  | ",arch=x86,config=565,":  2.5, | 
|  | ",arch=arm,config=8888,": 101.1, | 
|  | }, "gs://foo.json", time.Now()) | 
|  | assert.NoError(t, err) | 
|  |  | 
|  | // NewFromQueryAndRange | 
|  | q, err := query.New(url.Values{"config": []string{"8888"}}) | 
|  | assert.NoError(t, err) | 
|  | now := gittest.StartTime.Add(7 * time.Minute) | 
|  |  | 
|  | df, err := builder.NewFromQueryAndRange(ctx, now.Add(-7*time.Minute), now.Add(time.Second), q, false, progress.New()) | 
|  | require.NoError(t, err) | 
|  | assert.Len(t, df.TraceSet, 2) | 
|  | assert.Len(t, df.Header, 3) | 
|  | assert.Len(t, df.TraceSet[",arch=x86,config=8888,"], 3) | 
|  | assert.Len(t, df.TraceSet[",arch=arm,config=8888,"], 3) | 
|  |  | 
|  | // A dense response from NewNFromQuery(). | 
|  | df, err = builder.NewNFromQuery(ctx, now, q, 4, progress.New()) | 
|  | assert.NoError(t, err) | 
|  | assert.Len(t, df.TraceSet, 2) | 
|  | assert.Len(t, df.Header, 3) | 
|  | assert.Equal(t, df.Header[0].Offset, types.CommitNumber(0)) | 
|  | assert.Equal(t, df.Header[1].Offset, types.CommitNumber(1)) | 
|  | assert.Equal(t, df.Header[2].Offset, types.CommitNumber(7)) | 
|  | assert.Equal(t, df.TraceSet[",arch=x86,config=8888,"][0], float32(1.2)) | 
|  | assert.Equal(t, df.TraceSet[",arch=x86,config=8888,"][1], float32(1.3)) | 
|  | assert.Equal(t, df.TraceSet[",arch=x86,config=8888,"][2], float32(1.0)) | 
|  |  | 
|  | df, err = builder.NewNFromQuery(ctx, now, q, 2, progress.New()) | 
|  | assert.NoError(t, err) | 
|  | assert.Len(t, df.TraceSet, 2) | 
|  | assert.Len(t, df.Header, 2) | 
|  | assert.Equal(t, df.Header[1].Offset, types.CommitNumber(7)) | 
|  | assert.Equal(t, df.TraceSet[",arch=x86,config=8888,"][1], float32(1.0)) | 
|  |  | 
|  | // NewFromQueryAndRange where query doesn't encode. | 
|  | q, err = query.New(url.Values{"config": []string{"nvpr"}}) | 
|  | assert.NoError(t, err) | 
|  |  | 
|  | df, err = builder.NewFromQueryAndRange(ctx, now.Add(-7*time.Minute), now.Add(time.Second), q, false, progress.New()) | 
|  | assert.NoError(t, err) | 
|  | assert.Len(t, df.TraceSet, 0) | 
|  | assert.Len(t, df.Header, 0) | 
|  |  | 
|  | // NewFromKeysAndRange. | 
|  | df, err = builder.NewFromKeysAndRange(ctx, []string{",arch=x86,config=8888,", ",arch=x86,config=565,"}, now.Add(-7*time.Minute), now.Add(time.Second), false, progress.New()) | 
|  | assert.NoError(t, err) | 
|  | assert.Len(t, df.TraceSet, 2) | 
|  | assert.Len(t, df.Header, 3) | 
|  | assert.Len(t, df.ParamSet, 2) | 
|  | assert.Len(t, df.TraceSet[",arch=x86,config=8888,"], 3) | 
|  | assert.Len(t, df.TraceSet[",arch=x86,config=565,"], 3) | 
|  |  | 
|  | // NewNFromKeys. | 
|  | df, err = builder.NewNFromKeys(ctx, now, []string{",arch=x86,config=8888,", ",arch=x86,config=565,"}, 2, progress.New()) | 
|  | assert.NoError(t, err) | 
|  | assert.Len(t, df.TraceSet, 2) | 
|  | assert.Len(t, df.Header, 2) | 
|  | assert.Len(t, df.ParamSet, 2) | 
|  | assert.Len(t, df.TraceSet[",arch=x86,config=8888,"], 2) | 
|  | assert.Len(t, df.TraceSet[",arch=x86,config=565,"], 2) | 
|  |  | 
|  | df, err = builder.NewNFromKeys(ctx, now, []string{",arch=x86,config=8888,", ",arch=x86,config=565,"}, 3, progress.New()) | 
|  | assert.NoError(t, err) | 
|  | assert.Len(t, df.TraceSet, 2) | 
|  | assert.Len(t, df.Header, 3) | 
|  | assert.Len(t, df.TraceSet[",arch=x86,config=8888,"], 3) | 
|  | assert.Len(t, df.TraceSet[",arch=x86,config=565,"], 3) | 
|  |  | 
|  | df, err = builder.NewNFromKeys(ctx, now, []string{",arch=x86,config=8888,"}, 3, progress.New()) | 
|  | assert.NoError(t, err) | 
|  | assert.Len(t, df.TraceSet, 1) | 
|  | assert.Len(t, df.Header, 3) | 
|  | assert.Len(t, df.TraceSet[",arch=x86,config=8888,"], 3) | 
|  |  | 
|  | df, err = builder.NewNFromKeys(ctx, now, []string{}, 3, progress.New()) | 
|  | assert.NoError(t, err) | 
|  | assert.Len(t, df.TraceSet, 0) | 
|  | assert.Len(t, df.Header, 0) | 
|  |  | 
|  | // Empty set of keys should not fail. | 
|  | df, err = builder.NewFromKeysAndRange(ctx, []string{}, now.Add(-7*time.Minute), now.Add(time.Second), false, progress.New()) | 
|  | assert.NoError(t, err) | 
|  | assert.Len(t, df.TraceSet, 0) | 
|  | assert.Len(t, df.Header, 0) | 
|  |  | 
|  | // Add a value that only appears in one of the tiles. | 
|  | err = addValuesAtIndex(store, 7, map[string]float32{ | 
|  | ",config=8888,model=Pixel,": 3.0, | 
|  | }, "gs://foo.json", time.Now()) | 
|  | assert.NoError(t, err) | 
|  | store.ClearOrderedParamSetCache() | 
|  |  | 
|  | // This query will only encode for one tile and should still succeed. | 
|  | q, err = query.New(url.Values{"model": []string{"Pixel"}}) | 
|  | assert.NoError(t, err) | 
|  | df, err = builder.NewFromQueryAndRange(ctx, now.Add(-7*time.Minute), now.Add(time.Second), q, false, progress.New()) | 
|  | assert.NoError(t, err) | 
|  | assert.Len(t, df.TraceSet, 1) | 
|  | assert.Len(t, df.Header, 1) | 
|  | } | 
|  |  | 
|  | func TestFromIndexRange_Success(t *testing.T) { | 
|  | ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t) | 
|  | g, err := perfgit.New(ctx, true, db, instanceConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | columnHeaders, commitNumbers, _, err := fromIndexRange(ctx, g, types.CommitNumber(0), types.CommitNumber(2)) | 
|  | require.NoError(t, err) | 
|  | assert.Equal(t, []*dataframe.ColumnHeader{ | 
|  | { | 
|  | Offset:    0, | 
|  | Timestamp: gittest.StartTime.Unix(), | 
|  | }, | 
|  | { | 
|  | Offset:    1, | 
|  | Timestamp: gittest.StartTime.Add(time.Minute).Unix(), | 
|  | }, | 
|  | { | 
|  | Offset:    2, | 
|  | Timestamp: gittest.StartTime.Add(2 * time.Minute).Unix(), | 
|  | }, | 
|  | }, columnHeaders) | 
|  | assert.Equal(t, []types.CommitNumber{0, 1, 2}, commitNumbers) | 
|  | } | 
|  |  | 
|  | func TestFromIndexRange_EmptySliceOnBadCommitNumber(t *testing.T) { | 
|  | ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t) | 
|  | g, err := perfgit.New(ctx, true, db, instanceConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | columnHeaders, commitNumbers, _, err := fromIndexRange(ctx, g, types.BadCommitNumber, types.BadCommitNumber) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | assert.Empty(t, columnHeaders) | 
|  | assert.Empty(t, commitNumbers) | 
|  | } | 
|  |  | 
|  | func TestPreflightQuery_EmptyQuery_ReturnsError(t *testing.T) { | 
|  | ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t) | 
|  | g, err := perfgit.New(ctx, true, db, instanceConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | instanceConfig.DataStoreConfig.TileSize = 6 | 
|  |  | 
|  | store, err := sqltracestore.New(db, instanceConfig.DataStoreConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | builder := NewDataFrameBuilderFromTraceStore(g, store, 2, doNotFilterParentTraces) | 
|  |  | 
|  | // Add some points to the first tile. | 
|  | err = addValuesAtIndex(store, 0, map[string]float32{ | 
|  | ",arch=x86,config=8888,": 1.2, | 
|  | ",arch=x86,config=565,":  2.1, | 
|  | ",arch=arm,config=8888,": 100.5, | 
|  | }, "gs://foo.json", time.Now()) | 
|  | assert.NoError(t, err) | 
|  |  | 
|  | q, err := query.NewFromString("") | 
|  | require.NoError(t, err) | 
|  | _, _, err = builder.PreflightQuery(ctx, q, paramtools.NewReadOnlyParamSet()) | 
|  | require.Error(t, err) | 
|  | assert.Contains(t, err.Error(), "Can not pre-flight an empty query") | 
|  | } | 
|  |  | 
|  | func TestPreflightQuery_NonEmptyQuery_Success(t *testing.T) { | 
|  | ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t) | 
|  | g, err := perfgit.New(ctx, true, db, instanceConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | instanceConfig.DataStoreConfig.TileSize = 6 | 
|  |  | 
|  | store, err := sqltracestore.New(db, instanceConfig.DataStoreConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | builder := NewDataFrameBuilderFromTraceStore(g, store, 2, doNotFilterParentTraces) | 
|  |  | 
|  | // Add some points to the first tile. | 
|  | err = addValuesAtIndex(store, 0, map[string]float32{ | 
|  | ",arch=x86,config=8888,": 1.2, | 
|  | ",arch=x86,config=565,":  2.1, | 
|  | ",arch=arm,config=8888,": 100.5, | 
|  | }, "gs://foo.json", time.Now()) | 
|  | assert.NoError(t, err) | 
|  |  | 
|  | // Create a query that will match two of the points. | 
|  | q, err := query.NewFromString("config=8888") | 
|  | require.NoError(t, err) | 
|  |  | 
|  | // The referenceParamSet contains values that should not appear in the | 
|  | // returned ParamSet, and some that get retained. | 
|  | referenceParamSet := paramtools.ReadOnlyParamSet{ | 
|  | "arch":   {"x86", "arm", "should-disappear"}, | 
|  | "config": {"565", "8888", "should-be-retained"}, | 
|  | // 'should-be-retained' is retained because 'config' is in the query and | 
|  | // so all 'config' values should be returned in the ParamSet. | 
|  | } | 
|  | count, ps, err := builder.PreflightQuery(ctx, q, referenceParamSet) | 
|  | require.NoError(t, err) | 
|  | assert.Equal(t, int64(2), count) | 
|  |  | 
|  | expectedParamSet := paramtools.ParamSet{ | 
|  | "arch":   {"arm", "x86"}, | 
|  | "config": {"565", "8888", "should-be-retained"}, | 
|  | } | 
|  | assert.Equal(t, expectedParamSet, ps) | 
|  | } | 
|  |  | 
|  | func TestPreflightQuery_TilesContainDifferentNumberOfMatches_ReturnedParamSetReflectsBothTiles(t *testing.T) { | 
|  | ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t) | 
|  | g, err := perfgit.New(ctx, true, db, instanceConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | instanceConfig.DataStoreConfig.TileSize = 6 | 
|  |  | 
|  | store, err := sqltracestore.New(db, instanceConfig.DataStoreConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | builder := NewDataFrameBuilderFromTraceStore(g, store, 2, doNotFilterParentTraces) | 
|  |  | 
|  | // Add some points to the first tile. | 
|  | err = addValuesAtIndex(store, 0, map[string]float32{ | 
|  | ",arch=x86,config=8888,": 1.2, | 
|  | ",arch=x86,config=565,":  2.1, | 
|  | ",arch=arm,config=8888,": 100.5, | 
|  | }, "gs://foo.json", time.Now()) | 
|  | assert.NoError(t, err) | 
|  |  | 
|  | // Add some points to the second tile. | 
|  | err = addValuesAtIndex(store, types.CommitNumber(instanceConfig.DataStoreConfig.TileSize), map[string]float32{ | 
|  | ",arch=riscv,config=8888,": 1.2, | 
|  | }, "gs://foo.json", time.Now()) | 
|  | assert.NoError(t, err) | 
|  |  | 
|  | // Create a query that will match two of the points in tile 0 and one of the points in tile 1. | 
|  | q, err := query.NewFromString("config=8888") | 
|  | require.NoError(t, err) | 
|  |  | 
|  | // The reference ParamSet contains values that should not appear in the | 
|  | // returned ParamSet, and some that get retained. | 
|  | referenceParamSet := paramtools.ReadOnlyParamSet{ | 
|  | "arch":   {"x86", "arm", "should-disappear"}, | 
|  | "config": {"565", "8888", "should-be-retained"}, | 
|  | // 'should-be-retained' is retained because 'config' is in the query and | 
|  | // so all 'config' values should be returned in the ParamSet. | 
|  | } | 
|  | count, ps, err := builder.PreflightQuery(ctx, q, referenceParamSet) | 
|  | require.NoError(t, err) | 
|  | assert.Equal(t, int64(2), count) | 
|  |  | 
|  | expectedParamSet := paramtools.ParamSet{ | 
|  | "arch":   {"arm", "riscv", "x86"}, | 
|  | "config": {"565", "8888", "should-be-retained"}, | 
|  | } | 
|  | assert.Equal(t, expectedParamSet, ps) | 
|  | } | 
|  |  | 
|  | func TestNumMatches_EmptyQuery_ReturnsError(t *testing.T) { | 
|  | ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t) | 
|  | g, err := perfgit.New(ctx, true, db, instanceConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | instanceConfig.DataStoreConfig.TileSize = 6 | 
|  |  | 
|  | store, err := sqltracestore.New(db, instanceConfig.DataStoreConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | builder := NewDataFrameBuilderFromTraceStore(g, store, 2, doNotFilterParentTraces) | 
|  | q, err := query.NewFromString("") | 
|  | require.NoError(t, err) | 
|  | _, err = builder.NumMatches(ctx, q) | 
|  | require.Error(t, err) | 
|  | } | 
|  |  | 
|  | func TestNumMatches_NonEmptyQuery_Success(t *testing.T) { | 
|  | ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t) | 
|  | g, err := perfgit.New(ctx, true, db, instanceConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | instanceConfig.DataStoreConfig.TileSize = 6 | 
|  |  | 
|  | store, err := sqltracestore.New(db, instanceConfig.DataStoreConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | builder := NewDataFrameBuilderFromTraceStore(g, store, 2, doNotFilterParentTraces) | 
|  |  | 
|  | // Add some points to the first tile. | 
|  | err = addValuesAtIndex(store, 0, map[string]float32{ | 
|  | ",arch=x86,config=8888,": 1.2, | 
|  | ",arch=x86,config=565,":  2.1, | 
|  | ",arch=arm,config=8888,": 100.5, | 
|  | }, "gs://foo.json", time.Now()) | 
|  | assert.NoError(t, err) | 
|  |  | 
|  | // Create a query that will match two of the points. | 
|  | q, err := query.NewFromString("config=8888") | 
|  | require.NoError(t, err) | 
|  |  | 
|  | count, err := builder.NumMatches(ctx, q) | 
|  | require.NoError(t, err) | 
|  | assert.Equal(t, int64(2), count) | 
|  | } | 
|  |  | 
|  | func TestNumMatches_TilesContainDifferentNumberOfMatches_TheLargerOfTheTwoCountsIsReturned(t *testing.T) { | 
|  | ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t) | 
|  | g, err := perfgit.New(ctx, true, db, instanceConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | instanceConfig.DataStoreConfig.TileSize = 6 | 
|  |  | 
|  | store, err := sqltracestore.New(db, instanceConfig.DataStoreConfig) | 
|  | require.NoError(t, err) | 
|  |  | 
|  | builder := NewDataFrameBuilderFromTraceStore(g, store, 2, doNotFilterParentTraces) | 
|  |  | 
|  | // Add some points to the latest tile. | 
|  | err = addValuesAtIndex(store, types.CommitNumber(instanceConfig.DataStoreConfig.TileSize+1), map[string]float32{ | 
|  | ",arch=x86,config=8888,": 1.2, | 
|  | ",arch=x86,config=565,":  2.1, | 
|  | ",arch=arm,config=8888,": 100.5, | 
|  | }, "gs://foo.json", time.Now()) | 
|  | assert.NoError(t, err) | 
|  |  | 
|  | // Add some points to the previous tile. | 
|  | err = addValuesAtIndex(store, 1, map[string]float32{ | 
|  | ",arch=x86,config=8888,":   1.2, | 
|  | ",arch=riscv,config=8888,": 2.1, | 
|  | ",arch=arm,config=8888,":   100.5, | 
|  | }, "gs://foo.json", time.Now()) | 
|  | assert.NoError(t, err) | 
|  |  | 
|  | // Create a query that will match two of the points in tile 1, but three | 
|  | // points in tile 0. | 
|  | q, err := query.NewFromString("config=8888") | 
|  | require.NoError(t, err) | 
|  |  | 
|  | count, err := builder.NumMatches(ctx, q) | 
|  | require.NoError(t, err) | 
|  | assert.Equal(t, int64(3), count) | 
|  | } |