[gold] Preslice data in indexer by IgnoreState/Corpus/Test

This decreases the latency of /json/byblame by at least 3x
and can be used in general searching too.

Timings of /json/byblame on skia-gold-public instance for
the given corpus (average of 3)

Before:
gm: 7.8s
image: 5.1s
svg: 2.3s

After:
gm: 1.2s
image: 1.8s
svg: .74s

Bug: skia:9080
Change-Id: I8d200ebe4dd687ed2f51220dc8254038abfd3d99
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/253882
Reviewed-by: Ben Wagner aka dogben <benjaminwagner@google.com>
diff --git a/go/tiling/tiling.go b/go/tiling/tiling.go
index c5b54a1..5b57764 100644
--- a/go/tiling/tiling.go
+++ b/go/tiling/tiling.go
@@ -44,11 +44,6 @@
 	// Just like a Go [:] slice this is inclusive of begin and exclusive of end.
 	// The length on the Trace will then become end-begin.
 	Trim(begin, end int) error
-
-	// SetAt sets the value of the measurement at index.
-	//
-	// Each specialization will convert []byte to the correct type.
-	SetAt(index int, value []byte) error
 }
 
 // TraceID helps document when strings should represent TraceIds
diff --git a/golden/go/indexer/indexer.go b/golden/go/indexer/indexer.go
index 34b74f2..e8dd8c6 100644
--- a/golden/go/indexer/indexer.go
+++ b/golden/go/indexer/indexer.go
@@ -49,6 +49,7 @@
 	dCounters         [2]digest_counter.DigestCounter
 	summaries         [2]countsAndBlames
 	paramsetSummaries [2]paramsets.ParamSummary
+	preSliced         map[preSliceGroup][]*types.TracePair
 
 	cpxTile types.ComplexTile
 	blamer  blame.Blamer
@@ -58,6 +59,12 @@
 	testNames types.TestNameSet
 }
 
+type preSliceGroup struct {
+	IgnoreState types.IgnoreState
+	Corpus      string
+	Test        types.TestName
+}
+
 // countsAndBlame makes the type declaration of SearchIndex a little nicer to read.
 type countsAndBlames []*summary.TriageStatus
 
@@ -78,23 +85,26 @@
 		dCounters:         [2]digest_counter.DigestCounter{},
 		summaries:         [2]countsAndBlames{},
 		paramsetSummaries: [2]paramsets.ParamSummary{},
+		preSliced:         map[preSliceGroup][]*types.TracePair{},
 		cpxTile:           cpxTile,
 	}
 }
 
 // SearchIndexForTesting returns filled in search index to be used when testing. Note that the
 // indices of the arrays are the int values of types.IgnoreState
-func SearchIndexForTesting(cpxTile types.ComplexTile, dc [2]digest_counter.DigestCounter, pm [2]paramsets.ParamSummary, exp expstorage.ExpectationsStore, b blame.Blamer) *SearchIndex {
-	return &SearchIndex{
+func SearchIndexForTesting(cpxTile types.ComplexTile, dc [2]digest_counter.DigestCounter, pm [2]paramsets.ParamSummary, exp expstorage.ExpectationsStore, b blame.Blamer) (*SearchIndex, error) {
+	s := &SearchIndex{
 		searchIndexConfig: searchIndexConfig{
 			expectationsStore: exp,
 		},
 		dCounters:         dc,
 		summaries:         [2]countsAndBlames{},
 		paramsetSummaries: pm,
+		preSliced:         map[preSliceGroup][]*types.TracePair{},
 		blamer:            b,
 		cpxTile:           cpxTile,
 	}
+	return s, preSliceData(s)
 }
 
 // Tile implements the IndexSearcher interface.
@@ -139,7 +149,7 @@
 		return nil, skerr.Wrap(err)
 	}
 	d := summary.Data{
-		Traces:       idx.cpxTile.GetTile(is).Traces,
+		Traces:       idx.slicedTraces(is, query),
 		Expectations: exp,
 		ByTrace:      idx.dCounters[is].ByTrace(),
 		Blamer:       idx.blamer,
@@ -167,6 +177,35 @@
 	return idx.blamer.GetBlame(test, digest, commits)
 }
 
+// slicedTraces returns a slice of TracePairs that match the query and the ignore state.
+// This is meant to be a superset of traces, as only the corpus and testname from the query are
+// used for this pre-filter step.
+func (idx *SearchIndex) slicedTraces(is types.IgnoreState, query map[string][]string) []*types.TracePair {
+	if len(query[types.CORPUS_FIELD]) == 0 {
+		return idx.preSliced[preSliceGroup{
+			IgnoreState: is,
+		}]
+	}
+	var rv []*types.TracePair
+	for _, corpus := range query[types.CORPUS_FIELD] {
+		if len(query[types.PRIMARY_KEY_FIELD]) == 0 {
+			rv = append(rv, idx.preSliced[preSliceGroup{
+				IgnoreState: is,
+				Corpus:      corpus,
+			}]...)
+		} else {
+			for _, tn := range query[types.PRIMARY_KEY_FIELD] {
+				rv = append(rv, idx.preSliced[preSliceGroup{
+					IgnoreState: is,
+					Corpus:      corpus,
+					Test:        types.TestName(tn),
+				}]...)
+			}
+		}
+	}
+	return rv
+}
+
 type IndexerConfig struct {
 	DiffStore         diff.DiffStore
 	EventBus          eventbus.EventBus
@@ -206,6 +245,8 @@
 	// in large repos like Skia.
 	countsNodeExclude := root.Child(calcDigestCountsExclude)
 
+	preSliceNode := root.Child(preSliceData)
+
 	// Node that triggers blame and writing baselines.
 	// This is used to trigger when expectations change.
 	// We don't need to re-calculate DigestCounts if the
@@ -227,7 +268,7 @@
 	writeHashes := countsNodeInclude.Child(writeKnownHashesList)
 
 	// Summaries depend on DigestCounter and Blamer.
-	summariesNode := pdag.NewNodeWithParents(calcSummaries, countsNodeInclude, countsNodeExclude, blamerNode)
+	summariesNode := pdag.NewNodeWithParents(calcSummaries, countsNodeInclude, countsNodeExclude, blamerNode, preSliceNode)
 
 	// The Warmer depends on summaries.
 	pdag.NewNodeWithParents(runWarmer, summariesNode)
@@ -377,6 +418,7 @@
 		cpxTile:           lastIdx.cpxTile,
 		dCounters:         lastIdx.dCounters,         // stay the same even if expectations change.
 		paramsetSummaries: lastIdx.paramsetSummaries, // stay the same even if expectations change.
+		preSliced:         lastIdx.preSliced,         // stay the same even if expectations change.
 
 		summaries: [2]countsAndBlames{
 			// the objects inside the summaries are immutable, but may be replaced if expectations
@@ -422,6 +464,48 @@
 	return nil
 }
 
+// preSliceData is the pipeline function to pre-slice our traces. Currently, we pre-slice by
+// corpus name and then by test name because this breaks our traces up into groups of <1000.
+func preSliceData(state interface{}) error {
+	idx := state.(*SearchIndex)
+	for _, is := range types.IgnoreStates {
+		t := idx.cpxTile.GetTile(is)
+		for id, tr := range t.Traces {
+			gt, ok := tr.(*types.GoldenTrace)
+			if !ok {
+				sklog.Warningf("Unexpected trace type: %#v", gt)
+				continue
+			}
+			tp := types.TracePair{
+				ID:    id,
+				Trace: gt,
+			}
+			// Pre-slice the data by IgnoreState, then by IgnoreState and Corpus, finally by all
+			// three of IgnoreState/Corpus/Test. We shouldn't allow queries by Corpus w/o specifying
+			// IgnoreState, nor should we allow queries by TestName w/o specifying a Corpus or
+			// IgnoreState.
+			ignoreOnly := preSliceGroup{
+				IgnoreState: is,
+			}
+			idx.preSliced[ignoreOnly] = append(idx.preSliced[ignoreOnly], &tp)
+
+			ignoreAndCorpus := preSliceGroup{
+				IgnoreState: is,
+				Corpus:      gt.Corpus(),
+			}
+			idx.preSliced[ignoreAndCorpus] = append(idx.preSliced[ignoreAndCorpus], &tp)
+
+			ignoreCorpusTest := preSliceGroup{
+				IgnoreState: is,
+				Corpus:      gt.Corpus(),
+				Test:        gt.TestName(),
+			}
+			idx.preSliced[ignoreCorpusTest] = append(idx.preSliced[ignoreCorpusTest], &tp)
+		}
+	}
+	return nil
+}
+
 // calcSummaries is the pipeline function to calculate the summaries.
 func calcSummaries(state interface{}) error {
 	idx := state.(*SearchIndex)
@@ -431,7 +515,7 @@
 	}
 	for _, is := range types.IgnoreStates {
 		d := summary.Data{
-			Traces:       idx.cpxTile.GetTile(is).Traces,
+			Traces:       idx.slicedTraces(is, nil),
 			Expectations: exp,
 			ByTrace:      idx.dCounters[is].ByTrace(),
 			Blamer:       idx.blamer,
diff --git a/golden/go/indexer/indexer_test.go b/golden/go/indexer/indexer_test.go
index 2e40688..ca5decf 100644
--- a/golden/go/indexer/indexer_test.go
+++ b/golden/go/indexer/indexer_test.go
@@ -190,9 +190,11 @@
 			digest_counter.New(partialTile),
 			digest_counter.New(fullTile),
 		},
+		preSliced: map[preSliceGroup][]*types.TracePair{},
 
 		cpxTile: ct,
 	}
+	require.NoError(t, preSliceData(ixr.lastIndex))
 
 	ixr.indexTests([]expstorage.Delta{
 		{
@@ -229,6 +231,99 @@
 	wg.Wait()
 }
 
+// TestPreSlicedTracesCreatedCorrectly makes sure that we pre-slice the data based on IgnoreState,
+// then Corpus, then TestName.
+func TestPreSlicedTracesCreatedCorrectly(t *testing.T) {
+	unittest.SmallTest(t)
+
+	ct, _, _ := makeComplexTileWithCrosshatchIgnores()
+
+	si := &SearchIndex{
+		preSliced: map[preSliceGroup][]*types.TracePair{},
+		cpxTile:   ct,
+	}
+	require.NoError(t, preSliceData(si))
+
+	// (2 IgnoreStates) + (2 IgnoreStates * 1 corpus) + (2 IgnoreStates * 1 corpus * 2 tests)
+	assert.Len(t, si.preSliced, 8)
+	allCombos := []preSliceGroup{
+		{
+			IgnoreState: types.IncludeIgnoredTraces,
+		},
+		{
+			IgnoreState: types.ExcludeIgnoredTraces,
+		},
+		{
+			IgnoreState: types.IncludeIgnoredTraces,
+			Corpus:      "gm",
+		},
+		{
+			IgnoreState: types.ExcludeIgnoredTraces,
+			Corpus:      "gm",
+		},
+		{
+			IgnoreState: types.IncludeIgnoredTraces,
+			Corpus:      "gm",
+			Test:        data.AlphaTest,
+		},
+		{
+			IgnoreState: types.IncludeIgnoredTraces,
+			Corpus:      "gm",
+			Test:        data.BetaTest,
+		},
+		{
+			IgnoreState: types.ExcludeIgnoredTraces,
+			Corpus:      "gm",
+			Test:        data.AlphaTest,
+		},
+		{
+			IgnoreState: types.ExcludeIgnoredTraces,
+			Corpus:      "gm",
+			Test:        data.BetaTest,
+		},
+	}
+	for _, psg := range allCombos {
+		assert.Contains(t, si.preSliced, psg)
+	}
+}
+
+// TestPreSlicedTracesQuery tests that querying slicedTraces returns the correct set of tracePairs
+// from the preSliced data. This especially includes if multiple tests are in the query.
+func TestPreSlicedTracesQuery(t *testing.T) {
+	unittest.SmallTest(t)
+
+	ct, _, _ := makeComplexTileWithCrosshatchIgnores()
+
+	si := &SearchIndex{
+		preSliced: map[preSliceGroup][]*types.TracePair{},
+		cpxTile:   ct,
+	}
+	require.NoError(t, preSliceData(si))
+
+	allTraces := si.slicedTraces(types.IncludeIgnoredTraces, nil)
+	assert.Len(t, allTraces, 6)
+
+	withIgnores := si.slicedTraces(types.ExcludeIgnoredTraces, nil)
+	assert.Len(t, withIgnores, 4)
+
+	justCorpus := si.slicedTraces(types.IncludeIgnoredTraces, map[string][]string{
+		types.CORPUS_FIELD: {"gm"},
+	})
+	assert.Len(t, justCorpus, 6)
+
+	bothTests := si.slicedTraces(types.IncludeIgnoredTraces, map[string][]string{
+		types.CORPUS_FIELD:      {"gm"},
+		types.PRIMARY_KEY_FIELD: {string(data.BetaTest), string(data.AlphaTest)},
+	})
+	assert.Len(t, bothTests, 6)
+
+	oneTest := si.slicedTraces(types.ExcludeIgnoredTraces, map[string][]string{
+		types.CORPUS_FIELD:      {"gm"},
+		types.PRIMARY_KEY_FIELD: {string(data.AlphaTest)},
+	})
+	assert.Len(t, oneTest, 2)
+}
+
 const (
 	// valid, but arbitrary md5 hash
 	unavailableDigest = types.Digest("fed541470e246b63b313930523220de8")
@@ -238,7 +333,8 @@
 // condition similar to https://github.com/stretchr/testify/issues/625 In essence, try
 // to avoid having a mock (A) assert it was called with another mock (B) where the
 // mock B is used elsewhere. There's a race because mock B is keeping track of what was
-// called on it while mock A records what it was called with.
+// called on it while mock A records what it was called with. Additionally, the general guidelines
+// are to prefer to use the real thing instead of a mock.
 func makeComplexTileWithCrosshatchIgnores() (types.ComplexTile, *tiling.Tile, *tiling.Tile) {
 	fullTile := data.MakeTestTile()
 	partialTile := data.MakeTestTile()
diff --git a/golden/go/search/search.go b/golden/go/search/search.go
index 480d7eb..e0c44dd 100644
--- a/golden/go/search/search.go
+++ b/golden/go/search/search.go
@@ -134,8 +134,7 @@
 	if getRefDiffs {
 		// Diff stage: Compare all digests found in the previous stages and find
 		// reference points (positive, negative etc.) for each digest.
-		err := s.getReferenceDiffs(ctx, ret, q.Metric, q.Match, q.RTraceValues, q.IgnoreState(), exp, idx)
-		if err != nil {
+		if err := s.getReferenceDiffs(ctx, ret, q.Metric, q.Match, q.RTraceValues, q.IgnoreState(), exp, idx); err != nil {
 			return nil, skerr.Wrapf(err, "fetching reference diffs for %#v", q)
 		}
 
diff --git a/golden/go/search/search_test.go b/golden/go/search/search_test.go
index 546b488..baf79dd 100644
--- a/golden/go/search/search_test.go
+++ b/golden/go/search/search_test.go
@@ -953,7 +953,12 @@
 	cpxTile := types.NewComplexTile(data.MakeTestTile())
 	dc := digest_counter.New(data.MakeTestTile())
 	ps := paramsets.NewParamSummary(data.MakeTestTile(), dc)
-	return indexer.SearchIndexForTesting(cpxTile, [2]digest_counter.DigestCounter{dc, dc}, [2]paramsets.ParamSummary{ps, ps}, nil, nil)
+	si, err := indexer.SearchIndexForTesting(cpxTile, [2]digest_counter.DigestCounter{dc, dc}, [2]paramsets.ParamSummary{ps, ps}, nil, nil)
+	if err != nil {
+		// Something is horribly broken with our test data/setup
+		panic(err.Error())
+	}
+	return si
 }
 
 // This is arbitrary data.
diff --git a/golden/go/summary/summary.go b/golden/go/summary/summary.go
index 0b45f64..ed2e876 100644
--- a/golden/go/summary/summary.go
+++ b/golden/go/summary/summary.go
@@ -69,7 +69,7 @@
 
 // Data is a helper struct containing the data that goes into computing a summary.
 type Data struct {
-	Traces       map[tiling.TraceID]tiling.Trace
+	Traces       []*types.TracePair
 	Expectations expectations.ReadOnly
 	// ByTrace maps all traces in Trace to the counts of digests that appeared
 	// in those traces.
@@ -92,19 +92,18 @@
 	var ret []*TriageStatus
 
 	// Filter down to just the traces we are interested in, based on query.
-	filtered := map[grouping][]*tracePair{}
+	filtered := map[grouping][]*types.TracePair{}
 	t := shared.NewMetricsTimer("calc_summaries_filter_traces")
-	for id, tr := range s.Traces {
-		gt := tr.(*types.GoldenTrace)
-		if len(testNames) > 0 && !testNames[gt.TestName()] {
+	for _, tp := range s.Traces {
+		if len(testNames) > 0 && !testNames[tp.Trace.TestName()] {
 			continue
 		}
-		if tiling.Matches(tr, query) {
-			k := grouping{test: gt.TestName(), corpus: gt.Corpus()}
+		if tiling.Matches(tp.Trace, query) {
+			k := grouping{test: tp.Trace.TestName(), corpus: tp.Trace.Corpus()}
 			if slice, ok := filtered[k]; ok {
-				filtered[k] = append(slice, &tracePair{tr: gt, id: id})
+				filtered[k] = append(slice, tp)
 			} else {
-				filtered[k] = []*tracePair{{tr: gt, id: id}}
+				filtered[k] = []*types.TracePair{tp}
 			}
 		}
 	}
@@ -117,24 +116,24 @@
 		for _, pair := range traces {
 			if head {
 				// Find the last non-missing value in the trace.
-				for i := len(pair.tr.Digests) - 1; i >= 0; i-- {
-					if pair.tr.IsMissing(i) {
+				for i := len(pair.Trace.Digests) - 1; i >= 0; i-- {
+					if pair.Trace.IsMissing(i) {
 						continue
 					} else {
-						digestMap[pair.tr.Digests[i]] = true
+						digestMap[pair.Trace.Digests[i]] = true
 						break
 					}
 				}
 			} else {
 				// Use the digests by trace if available, otherwise just inspect the trace.
-				if t, ok := s.ByTrace[pair.id]; ok {
+				if t, ok := s.ByTrace[pair.ID]; ok {
 					for d := range t {
 						digestMap[d] = true
 					}
 				} else {
-					for i := len(pair.tr.Digests) - 1; i >= 0; i-- {
-						if !pair.tr.IsMissing(i) {
-							digestMap[pair.tr.Digests[i]] = true
+					for i := len(pair.Trace.Digests) - 1; i >= 0; i-- {
+						if !pair.Trace.IsMissing(i) {
+							digestMap[pair.Trace.Digests[i]] = true
 						}
 					}
 				}
@@ -160,12 +159,6 @@
 	corpus string
 }
 
-// tracePair is used to hold traces, along with their ids.
-type tracePair struct {
-	id tiling.TraceID
-	tr *types.GoldenTrace
-}
-
 // makeSummary returns a TriageStatus for the given digests.
 func (s *Data) makeSummary(name types.TestName, corpus string, digests types.DigestSlice) *TriageStatus {
 	pos := 0
diff --git a/golden/go/summary/summary_test.go b/golden/go/summary/summary_test.go
index b894c41..7179ce7 100644
--- a/golden/go/summary/summary_test.go
+++ b/golden/go/summary/summary_test.go
@@ -210,8 +210,10 @@
 	blamer, err := blame.New(makeFullTile(), makeExpectations())
 	require.NoError(t, err)
 
+	tr := asSlice(t, tile.Traces)
+
 	d := Data{
-		Traces:       tile.Traces,
+		Traces:       tr,
 		Expectations: makeExpectations(),
 		ByTrace:      dc.ByTrace(),
 		Blamer:       blamer,
@@ -222,6 +224,19 @@
 	return d.Calculate(testNames, query, head)
 }
 
+func asSlice(t *testing.T, traces map[tiling.TraceID]tiling.Trace) []*types.TracePair {
+	xt := make([]*types.TracePair, 0, len(traces))
+	for id, tr := range traces {
+		gt, ok := tr.(*types.GoldenTrace)
+		require.True(t, ok)
+		xt = append(xt, &types.TracePair{
+			ID:    id,
+			Trace: gt,
+		})
+	}
+	return xt
+}
+
 // TestSummaryMap_FullBugRevert checks the entire return value, rather
 // than just the spot checks above.
 func TestSummaryMap_FullBugRevert(t *testing.T) {
@@ -320,8 +335,10 @@
 	blamer, err := blame.New(bug_revert.MakeTestTile(), bug_revert.MakeTestExpectations())
 	require.NoError(t, err)
 
+	tr := asSlice(t, bug_revert.MakeTestTile().Traces)
+
 	d := Data{
-		Traces:       bug_revert.MakeTestTile().Traces,
+		Traces:       tr,
 		Expectations: bug_revert.MakeTestExpectations(),
 		ByTrace:      dc.ByTrace(),
 		Blamer:       blamer,
@@ -380,8 +397,10 @@
 	blamer, err := blame.New(tile, &e)
 	require.NoError(t, err)
 
+	tr := asSlice(t, tile.Traces)
+
 	d := Data{
-		Traces:       tile.Traces,
+		Traces:       tr,
 		Expectations: &e,
 		ByTrace:      dc.ByTrace(),
 		Blamer:       blamer,
diff --git a/golden/go/types/complex_tile.go b/golden/go/types/complex_tile.go
index 9e5e79e..77c2d23 100644
--- a/golden/go/types/complex_tile.go
+++ b/golden/go/types/complex_tile.go
@@ -104,11 +104,3 @@
 
 // Make sure ComplexTileImpl fulfills the ComplexTile Interface
 var _ ComplexTile = (*ComplexTileImpl)(nil)
-
-// TODO(kjlubick): Most (all?) places in gold, we don't look anything up by trace id
-// Maps aren't the best choice in those cases, so maybe instead of
-// handing around a map of TraceID -> Trace we can hand around a []TracePair
-type TracePair struct {
-	ID    tiling.TraceID
-	Trace tiling.Trace
-}
diff --git a/golden/go/types/types.go b/golden/go/types/types.go
index c589d32..58563f0 100644
--- a/golden/go/types/types.go
+++ b/golden/go/types/types.go
@@ -1,19 +1,11 @@
 package types
 
 import (
-	"encoding/gob"
 	"fmt"
 
 	"go.skia.org/infra/go/tiling"
 )
 
-func init() {
-	// Register *GoldenTrace in gob so that it can be used as a
-	// concrete type for Trace when writing and reading Tiles in gobs.
-	// TODO(kjlubick) It does not appear we gob encode traces anymore.
-	gob.Register(&GoldenTrace{})
-}
-
 const (
 	// PRIMARY_KEY_FIELD is the field that uniquely identifies a key.
 	PRIMARY_KEY_FIELD = "name"
@@ -196,23 +188,6 @@
 	return nil
 }
 
-// SetAt implements the tiling.Trace interface.
-func (g *GoldenTrace) SetAt(index int, value []byte) error {
-	if index < 0 || index > len(g.Digests) {
-		return fmt.Errorf("Invalid index: %d", index)
-	}
-	g.Digests[index] = Digest(value)
-	return nil
-}
-
-// LastDigest returns the last digest in the trace (HEAD) or the empty string otherwise.
-func (g *GoldenTrace) LastDigest() Digest {
-	if idx := g.LastIndex(); idx >= 0 {
-		return g.Digests[idx]
-	}
-	return MISSING_DIGEST
-}
-
 // LastIndex returns the index of last non-empty value in this trace and -1 if
 // if the entire trace is empty.
 func (g *GoldenTrace) LastIndex() int {
@@ -228,3 +203,10 @@
 func (g *GoldenTrace) String() string {
 	return fmt.Sprintf("Keys: %#v, Digests: %q", g.Keys, g.Digests)
 }
+
+// TracePair represents a single Golden trace and its ID. A slice of TracePair is faster to
+// iterate over than a map of TraceID -> Trace
+type TracePair struct {
+	ID    tiling.TraceID
+	Trace *GoldenTrace
+}
diff --git a/golden/go/types/types_test.go b/golden/go/types/types_test.go
index f5c0ed8..501aa57 100644
--- a/golden/go/types/types_test.go
+++ b/golden/go/types/types_test.go
@@ -58,30 +58,6 @@
 	assert.Equal(t, 0, g.Len(), "final size wrong")
 }
 
-func TestSetAt(t *testing.T) {
-	unittest.SmallTest(t)
-	testCases := []struct {
-		want Digest
-	}{
-		{
-			want: "",
-		},
-		{
-			want: "abcd",
-		},
-		{
-			want: MISSING_DIGEST,
-		},
-	}
-	tr := NewEmptyGoldenTrace(len(testCases), nil)
-	for i, tc := range testCases {
-		require.NoError(t, tr.SetAt(i, []byte(tc.want)))
-	}
-	for i, tc := range testCases {
-		assert.Equal(t, tc.want, tr.Digests[i], "Bad at test case %d", i)
-	}
-}
-
 var _tn TestName
 
 // BenchmarkTraceTestName shows that a map-lookup in go for this example param map is about
diff --git a/golden/go/web/web.go b/golden/go/web/web.go
index 99db2b9..16d063d 100644
--- a/golden/go/web/web.go
+++ b/golden/go/web/web.go
@@ -296,7 +296,11 @@
 			Commits:       commitinfo[groupid],
 		})
 	}
-	sort.Sort(ByBlameEntrySlice(blameEntries))
+	sort.Slice(blameEntries, func(i, j int) bool {
+		return blameEntries[i].NDigests > blameEntries[j].NDigests ||
+			// For test determinism, use GroupID as a tie-breaker
+			(blameEntries[i].NDigests == blameEntries[j].NDigests && blameEntries[i].GroupID < blameEntries[j].GroupID)
+	})
 
 	return blameEntries, nil
 }
@@ -320,16 +324,6 @@
 	Commits       []*tiling.Commit `json:"commits"`
 }
 
-type ByBlameEntrySlice []ByBlameEntry
-
-func (b ByBlameEntrySlice) Len() int { return len(b) }
-func (b ByBlameEntrySlice) Less(i, j int) bool {
-	return b[i].NDigests > b[j].NDigests ||
-		// For test determinism, use GroupID as a tie-breaker
-		(b[i].NDigests == b[j].NDigests && b[i].GroupID < b[j].GroupID)
-}
-func (b ByBlameEntrySlice) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
-
 // ByBlame describes a single digest and its blames.
 type ByBlame struct {
 	Test          types.TestName          `json:"test"`
diff --git a/golden/go/web/web_test.go b/golden/go/web/web_test.go
index f2cbff2..240d00d 100644
--- a/golden/go/web/web_test.go
+++ b/golden/go/web/web_test.go
@@ -161,7 +161,12 @@
 		panic(err) // this means our static data is horribly broken
 	}
 
-	return indexer.SearchIndexForTesting(cpxTile, [2]digest_counter.DigestCounter{dc, dc}, [2]paramsets.ParamSummary{ps, ps}, exp, b)
+	si, err := indexer.SearchIndexForTesting(cpxTile, [2]digest_counter.DigestCounter{dc, dc}, [2]paramsets.ParamSummary{ps, ps}, exp, b)
+	if err != nil {
+		// Something is horribly broken with our test data
+		panic(err.Error())
+	}
+	return si
 }
 
 // TestGetChangeListsSunnyDay tests the core functionality of