From 86aed20d227bc18326a7747315294cee599e3e02 Mon Sep 17 00:00:00 2001 From: Bartlomiej Plotka Date: Wed, 3 Jun 2020 19:28:49 +0100 Subject: [PATCH] store/proxy: Deduplicate chunks on StoreAPI level. Recommend chunk sorting for StoreAPI + Optimized iter chunk dedup. (#2710) * Deduplicate chunk dups on proxy StoreAPI level. Recommend chunk sorting for StoreAPI. Also: Merge same series together on proxy level instead select. This allows better dedup efficiency. Partially fixes: https://github.com/thanos-io/thanos/issues/2303 Cases like overlapped data from store and sidecar and 1:1 duplicates are optimized as soon as it's possible. This case was highly visible on GitLab repro data and exists in most of Thanos setup. Signed-off-by: Bartlomiej Plotka * Optimized algorithm to combine series only on start. Signed-off-by: Bartlomiej Plotka * Optimized chunk comparision for overlaps. Signed-off-by: Bartlomiej Plotka * Optimized deduplication for deduplicated chunk on query level as well. Never use proto .String() in fast path! Signed-off-by: Bartlomiej Plotka # Conflicts: # CHANGELOG.md # pkg/store/storepb/custom.go # pkg/store/storepb/custom_test.go --- CHANGELOG.md | 1 + pkg/query/iter.go | 8 +- pkg/store/bucket.go | 5 +- pkg/store/proxy.go | 1 + pkg/store/proxy_test.go | 12 +- pkg/store/storepb/custom.go | 201 ++++++++++++++++++++++++---- pkg/store/storepb/custom_test.go | 220 +++++++++++++++++++++++++------ pkg/store/storepb/rpc.pb.go | 6 + pkg/store/storepb/rpc.proto | 3 + pkg/ui/bindata.go | 218 +++++++++++++++--------------- 10 files changed, 482 insertions(+), 193 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dad87278b..239b3b4157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ anymore. - Receive,Rule: TSDB now holds less WAL files after Head Truncation. - [#2450](https://github.com/thanos-io/thanos/pull/2450) Store: Added Regex-set optimization for `label=~"a|b|c"` matchers. - [#2526](https://github.com/thanos-io/thanos/pull/2526) Compact: In case there are no labels left after deduplication via `--deduplication.replica-label`, assign first `replica-label` with value `deduped`. +- [2603](https://github.com/thanos-io/thanos/pull/2603) Store/Querier: Significantly optimize cases where StoreAPIs or blocks returns exact overlapping chunks (e.g Store GW and sidecar or brute force Store Gateway HA). ## [v0.12.2](https://github.com/thanos-io/thanos/releases/tag/v0.12.2) - 2020.04.30 diff --git a/pkg/query/iter.go b/pkg/query/iter.go index 6136285f10..055210d008 100644 --- a/pkg/query/iter.go +++ b/pkg/query/iter.go @@ -62,10 +62,8 @@ func (s *promSeriesSet) Next() bool { return s.currChunks[i].MinTime < s.currChunks[j].MinTime }) - // newChunkSeriesIterator will handle overlaps well, however we don't need to iterate over those samples, - // removed early duplicates here. - // TODO(bwplotka): Remove chunk duplicates on proxy level as well to avoid decoding those. - // https://github.com/thanos-io/thanos/issues/2546, consider skipping removal here then. + // Proxy handles duplicates between different series, let's handle duplicates within single series now as well. + // We don't need to decode those. s.currChunks = removeExactDuplicates(s.currChunks) return true } @@ -81,7 +79,7 @@ func removeExactDuplicates(chks []storepb.AggrChunk) []storepb.AggrChunk { ret = append(ret, chks[0]) for _, c := range chks[1:] { - if ret[len(ret)-1].String() == c.String() { + if ret[len(ret)-1].Compare(c) == 0 { continue } ret = append(ret, c) diff --git a/pkg/store/bucket.go b/pkg/store/bucket.go index 2f28f54052..4ec9db4c3e 100644 --- a/pkg/store/bucket.go +++ b/pkg/store/bucket.go @@ -984,9 +984,8 @@ func (s *BucketStore) Series(req *storepb.SeriesRequest, srv storepb.Store_Serie tracing.DoInSpan(ctx, "bucket_store_merge_all", func(ctx context.Context) { begin := time.Now() - // Merge series set into an union of all block sets. This exposes all blocks are single seriesSet. - // Chunks of returned series might be out of order w.r.t to their time range. - // This must be accounted for later by clients. + // NOTE: We "carefully" assume series and chunks are sorted within each SeriesSet. This should be guaranteed by + // blockSeries method. In worst case deduplication logic won't deduplicate correctly, which will be accounted later. set := storepb.MergeSeriesSets(res...) for set.Next() { var series storepb.Series diff --git a/pkg/store/proxy.go b/pkg/store/proxy.go index 36da2e62fd..4fc34eb850 100644 --- a/pkg/store/proxy.go +++ b/pkg/store/proxy.go @@ -473,6 +473,7 @@ func (s *streamSeriesSet) At() ([]storepb.Label, []storepb.AggrChunk) { } return s.currSeries.Labels, s.currSeries.Chunks } + func (s *streamSeriesSet) Err() error { s.errMtx.Lock() defer s.errMtx.Unlock() diff --git a/pkg/store/proxy_test.go b/pkg/store/proxy_test.go index f4e8849904..3235a3220e 100644 --- a/pkg/store/proxy_test.go +++ b/pkg/store/proxy_test.go @@ -294,15 +294,11 @@ func TestProxyStore_Series(t *testing.T) { expectedSeries: []rawSeries{ { lset: []storepb.Label{{Name: "a", Value: "a"}}, - chunks: [][]sample{{{0, 0}, {2, 1}, {3, 2}}, {{4, 3}}}, - }, - { - lset: []storepb.Label{{Name: "a", Value: "a"}}, - chunks: [][]sample{{{5, 4}}}, + chunks: [][]sample{{{0, 0}, {2, 1}, {3, 2}}, {{4, 3}}, {{5, 4}}}, }, { lset: []storepb.Label{{Name: "a", Value: "b"}}, - chunks: [][]sample{{{2, 2}, {3, 3}, {4, 4}}, {{1, 1}, {2, 2}, {3, 3}}}, // No sort merge. + chunks: [][]sample{{{1, 1}, {2, 2}, {3, 3}}, {{2, 2}, {3, 3}, {4, 4}}}, }, { lset: []storepb.Label{{Name: "a", Value: "c"}}, @@ -343,7 +339,7 @@ func TestProxyStore_Series(t *testing.T) { expectedSeries: []rawSeries{ { lset: []storepb.Label{{Name: "a", Value: "b"}}, - chunks: [][]sample{{{1, 1}, {2, 2}, {3, 3}}, {{1, 11}, {2, 22}, {3, 33}}}, + chunks: [][]sample{{{1, 11}, {2, 22}, {3, 33}}, {{1, 1}, {2, 2}, {3, 3}}}, }, }, }, @@ -1220,7 +1216,7 @@ type rawSeries struct { } func seriesEquals(t *testing.T, expected []rawSeries, got []storepb.Series) { - testutil.Equals(t, len(expected), len(got), "got: %v", got) + testutil.Equals(t, len(expected), len(got), "got unexpected number of series: \n %v", got) for i, series := range got { testutil.Equals(t, expected[i].lset, series.Labels) diff --git a/pkg/store/storepb/custom.go b/pkg/store/storepb/custom.go index 781c6d761f..dfc1a4f8ec 100644 --- a/pkg/store/storepb/custom.go +++ b/pkg/store/storepb/custom.go @@ -4,6 +4,7 @@ package storepb import ( + "bytes" "strings" "unsafe" @@ -45,6 +46,7 @@ func NewHintsSeriesResponse(hints *types.Any) *SeriesResponse { } // CompareLabels compares two sets of labels. +// After lexicographical order, the set with fewer labels comes first. func CompareLabels(a, b []Label) int { l := len(a) if len(b) < l { @@ -58,7 +60,7 @@ func CompareLabels(a, b []Label) int { return d } } - // If all labels so far were in common, the set with fewer labels comes first. + return len(a) - len(b) } @@ -73,13 +75,25 @@ func EmptySeriesSet() SeriesSet { return emptySeriesSet{} } -// MergeSeriesSets returns a new series set that is the union of the input sets. +// MergeSeriesSets takes all series sets and returns as a union single series set. +// It assumes series are sorted by labels within single SeriesSet, similar to remote read guarantees. +// However, they can be partial: in such case, if the single SeriesSet returns the same series within many iterations, +// MergeSeriesSets will merge those into one. +// +// It also assumes in a "best effort" way that chunks are sorted by min time. It's done as an optimization only, so if input +// series' chunks are NOT sorted, the only consequence is that the duplicates might be not correctly removed. This is double checked +// which on just-before PromQL level as well, so the only consequence is increased network bandwidth. +// If all chunks were sorted, MergeSeriesSet ALSO returns sorted chunks by min time. +// +// Chunks within the same series can also overlap (within all SeriesSet +// as well as single SeriesSet alone). If the chunk ranges overlap, the *exact* chunk duplicates will be removed +// (except one), and any other overlaps will be appended into on chunks slice. func MergeSeriesSets(all ...SeriesSet) SeriesSet { switch len(all) { case 0: return emptySeriesSet{} case 1: - return all[0] + return newUniqueSeriesSet(all[0]) } h := len(all) / 2 @@ -106,11 +120,6 @@ type mergedSeriesSet struct { adone, bdone bool } -// newMergedSeriesSet takes two series sets as a single series set. -// Series that occur in both sets should have disjoint time ranges. -// If the ranges overlap b samples are appended to a samples. -// If the single SeriesSet returns same series within many iterations, -// merge series set will not try to merge those. func newMergedSeriesSet(a, b SeriesSet) *mergedSeriesSet { s := &mergedSeriesSet{a: a, b: b} // Initialize first elements of both sets as Next() needs @@ -150,33 +159,175 @@ func (s *mergedSeriesSet) Next() bool { } d := s.compare() - - // Both sets contain the current series. Chain them into a single one. if d > 0 { s.lset, s.chunks = s.b.At() s.bdone = !s.b.Next() - } else if d < 0 { + return true + } + if d < 0 { s.lset, s.chunks = s.a.At() s.adone = !s.a.Next() - } else { - // Concatenate chunks from both series sets. They may be expected of order - // w.r.t to their time range. This must be accounted for later. - lset, chksA := s.a.At() - _, chksB := s.b.At() - - s.lset = lset - // Slice reuse is not generally safe with nested merge iterators. - // We err on the safe side an create a new slice. - s.chunks = make([]AggrChunk, 0, len(chksA)+len(chksB)) - s.chunks = append(s.chunks, chksA...) - s.chunks = append(s.chunks, chksB...) + return true + } - s.adone = !s.a.Next() - s.bdone = !s.b.Next() + // Both a and b contains the same series. Go through all chunks, remove duplicates and concatenate chunks from both + // series sets. We best effortly assume chunks are sorted by min time. If not, we will not detect all deduplicate which will + // be account on select layer anyway. We do it still for early optimization. + lset, chksA := s.a.At() + _, chksB := s.b.At() + s.lset = lset + + // Slice reuse is not generally safe with nested merge iterators. + // We err on the safe side an create a new slice. + s.chunks = make([]AggrChunk, 0, len(chksA)+len(chksB)) + + b := 0 +Outer: + for a := range chksA { + for { + if b >= len(chksB) { + // No more b chunks. + s.chunks = append(s.chunks, chksA[a:]...) + break Outer + } + + cmp := chksA[a].Compare(chksB[b]) + if cmp > 0 { + s.chunks = append(s.chunks, chksA[a]) + break + } + if cmp < 0 { + s.chunks = append(s.chunks, chksB[b]) + b++ + continue + } + + // Exact duplicated chunks, discard one from b. + b++ + } } + + if b < len(chksB) { + s.chunks = append(s.chunks, chksB[b:]...) + } + + s.adone = !s.a.Next() + s.bdone = !s.b.Next() return true } +// uniqueSeriesSet takes one series set and ensures each iteration contains single, full series. +type uniqueSeriesSet struct { + SeriesSet + done bool + + peek *Series + + lset []Label + chunks []AggrChunk +} + +func newUniqueSeriesSet(wrapped SeriesSet) *uniqueSeriesSet { + return &uniqueSeriesSet{SeriesSet: wrapped} +} + +func (s *uniqueSeriesSet) At() ([]Label, []AggrChunk) { + return s.lset, s.chunks +} + +func (s *uniqueSeriesSet) Next() bool { + if s.Err() != nil { + return false + } + + for !s.done { + if s.done = !s.SeriesSet.Next(); s.done { + break + } + lset, chks := s.SeriesSet.At() + if s.peek == nil { + s.peek = &Series{Labels: lset, Chunks: chks} + continue + } + + if CompareLabels(lset, s.peek.Labels) != 0 { + s.lset, s.chunks = s.peek.Labels, s.peek.Chunks + s.peek = &Series{Labels: lset, Chunks: chks} + return true + } + + // We assume non-overlapping, sorted chunks. This is best effort only, if it's otherwise it + // will just be duplicated, but well handled by StoreAPI consumers. + s.peek.Chunks = append(s.peek.Chunks, chks...) + } + + if s.peek == nil { + return false + } + + s.lset, s.chunks = s.peek.Labels, s.peek.Chunks + s.peek = nil + return true +} + +// Compare returns positive 1 if chunk is smaller -1 if larger than b by min time, then max time. +// It returns 0 if chunks are exactly the same. +func (m AggrChunk) Compare(b AggrChunk) int { + if m.MinTime < b.MinTime { + return 1 + } + if m.MinTime > b.MinTime { + return -1 + } + + // Same min time. + if m.MaxTime < b.MaxTime { + return 1 + } + if m.MaxTime > b.MaxTime { + return -1 + } + + // We could use proto.Equal, but we need ordering as well. + for _, cmp := range []func() int{ + func() int { return m.Raw.Compare(b.Raw) }, + func() int { return m.Count.Compare(b.Count) }, + func() int { return m.Sum.Compare(b.Sum) }, + func() int { return m.Min.Compare(b.Min) }, + func() int { return m.Max.Compare(b.Max) }, + func() int { return m.Counter.Compare(b.Counter) }, + } { + if c := cmp(); c == 0 { + continue + } else { + return c + } + } + return 0 +} + +// Compare returns positive 1 if chunk is smaller -1 if larger. +// It returns 0 if chunks are exactly the same. +func (m *Chunk) Compare(b *Chunk) int { + if m == nil && b == nil { + return 0 + } + if b == nil { + return 1 + } + if m == nil { + return -1 + } + + if m.Type < b.Type { + return 1 + } + if m.Type > b.Type { + return -1 + } + return bytes.Compare(m.Data, b.Data) +} + // LabelsToPromLabels converts Thanos proto labels to Prometheus labels in type safe manner. func LabelsToPromLabels(lset []Label) labels.Labels { ret := make(labels.Labels, len(lset)) diff --git a/pkg/store/storepb/custom_test.go b/pkg/store/storepb/custom_test.go index cbeaed6950..832c6e0b9b 100644 --- a/pkg/store/storepb/custom_test.go +++ b/pkg/store/storepb/custom_test.go @@ -87,7 +87,7 @@ func (errSeriesSet) At() ([]Label, []AggrChunk) { return nil, nil } func (e errSeriesSet) Err() error { return e.err } -func TestMergeSeriesSet(t *testing.T) { +func TestMergeSeriesSets(t *testing.T) { for _, tcase := range []struct { desc string in [][]rawSeries @@ -139,7 +139,7 @@ func TestMergeSeriesSet(t *testing.T) { }, }, { - desc: "two seriesSets, {a=c} series to merge", + desc: "two seriesSets, {a=c} series to merge, sorted", in: [][]rawSeries{ { { @@ -165,14 +165,12 @@ func TestMergeSeriesSet(t *testing.T) { chunks: [][]sample{{{1, 1}, {2, 2}}, {{3, 3}, {4, 4}}}, }, { lset: labels.FromStrings("a", "c"), - chunks: [][]sample{{{11, 1}, {12, 2}}, {{13, 3}, {14, 4}}, {{7, 1}, {8, 2}}, {{9, 3}, {10, 4}, {11, 4444}}}, + chunks: [][]sample{{{7, 1}, {8, 2}}, {{9, 3}, {10, 4}, {11, 4444}}, {{11, 1}, {12, 2}}, {{13, 3}, {14, 4}}}, }, }, }, { - // SeriesSet can return same series within different iterations. MergeSeries should not try to merge those. - // We do it on last step possible: Querier promSet. - desc: "single seriesSets, {a=c} series to merge.", + desc: "single seriesSets, {a=c} series to merge, sorted", in: [][]rawSeries{ { { @@ -196,25 +194,146 @@ func TestMergeSeriesSet(t *testing.T) { chunks: [][]sample{{{1, 1}, {2, 2}}, {{3, 3}, {4, 4}}}, }, { lset: labels.FromStrings("a", "c"), - chunks: [][]sample{{{7, 1}, {8, 2}}, {{9, 3}, {10, 4}, {11, 4444}}}, + chunks: [][]sample{{{7, 1}, {8, 2}}, {{9, 3}, {10, 4}, {11, 4444}}, {{11, 1}, {12, 2}}, {{13, 3}, {14, 4}}}, + }, + }, + }, + { + desc: "four seriesSets, {a=c} series to merge AND deduplicate exactly the same chunks", + in: [][]rawSeries{ + { + { + lset: labels.FromStrings("a", "a"), + chunks: [][]sample{{{1, 1}, {2, 2}}, {{3, 3}, {4, 4}}}, + }, + { + lset: labels.FromStrings("a", "c"), + chunks: [][]sample{ + {{11, 11}, {12, 12}, {13, 13}, {14, 14}}, + {{15, 15}, {16, 16}, {17, 17}, {18, 18}}, + }, + }, + { + lset: labels.FromStrings("a", "c"), + chunks: [][]sample{ + {{20, 20}, {21, 21}, {22, 22}, {24, 24}}, + }, + }, + }, + { + { + lset: labels.FromStrings("a", "c"), + chunks: [][]sample{ + {{1, 1}, {2, 2}, {3, 3}, {4, 4}}, + {{11, 11}, {12, 12}, {13, 13}, {14, 14}}, // Same chunk as in set 1. + }, + }, + { + lset: labels.FromStrings("a", "d"), + chunks: [][]sample{{{11, 1}, {12, 2}}, {{13, 3}, {14, 4}}}, + }, + }, + { + { + lset: labels.FromStrings("a", "c"), + chunks: [][]sample{ + {{11, 11}, {12, 12}, {13, 13}, {14, 14}}, // Same chunk as in set 1. + {{20, 20}, {21, 21}, {22, 23}, {24, 24}}, // Almost same chunk as in set 1 (one value is different). + }, + }, + }, + { + { + lset: labels.FromStrings("a", "c"), + chunks: [][]sample{ + {{11, 11}, {12, 12}, {14, 14}}, // Almost same chunk as in set 1 (one sample is missing). + {{20, 20}, {21, 21}, {22, 22}, {24, 24}}, // Same chunk as in set 1. + }, + }, + }, + }, + + expected: []rawSeries{ + { + lset: labels.Labels{labels.Label{Name: "a", Value: "a"}}, + chunks: [][]sample{{{t: 1, v: 1}, {t: 2, v: 2}}, {{t: 3, v: 3}, {t: 4, v: 4}}}, }, { - lset: labels.FromStrings("a", "c"), - chunks: [][]sample{{{11, 1}, {12, 2}}, {{13, 3}, {14, 4}}}, + lset: labels.Labels{labels.Label{Name: "a", Value: "c"}}, + chunks: [][]sample{ + {{t: 1, v: 1}, {t: 2, v: 2}, {t: 3, v: 3}, {t: 4, v: 4}}, + {{t: 11, v: 11}, {t: 12, v: 12}, {t: 13, v: 13}, {t: 14, v: 14}}, + {{t: 11, v: 11}, {t: 12, v: 12}, {t: 14, v: 14}}, + {{t: 15, v: 15}, {t: 16, v: 16}, {t: 17, v: 17}, {t: 18, v: 18}}, + {{t: 20, v: 20}, {t: 21, v: 21}, {t: 22, v: 22}, {t: 24, v: 24}}, + {{t: 20, v: 20}, {t: 21, v: 21}, {t: 22, v: 23}, {t: 24, v: 24}}, + }, + }, { + lset: labels.Labels{labels.Label{Name: "a", Value: "d"}}, + chunks: [][]sample{{{t: 11, v: 1}, {t: 12, v: 2}}, {{t: 13, v: 3}, {t: 14, v: 4}}}, + }, + }, + }, + { + desc: "four seriesSets, {a=c} series to merge, unsorted chunks, so dedup is expected to not be fully done", + in: [][]rawSeries{ + { + { + lset: labels.FromStrings("a", "c"), + chunks: [][]sample{ + {{20, 20}, {21, 21}, {22, 22}, {24, 24}}, + }, + }, + { + lset: labels.FromStrings("a", "c"), + chunks: [][]sample{ + {{11, 11}, {12, 12}, {13, 13}, {14, 14}}, + {{15, 15}, {16, 16}, {17, 17}, {18, 18}}, + }, + }, + }, + { + { + lset: labels.FromStrings("a", "c"), + chunks: [][]sample{ + {{11, 11}, {12, 12}, {13, 13}, {14, 14}}, // Same chunk as in set 1. + {{1, 1}, {2, 2}, {3, 3}, {4, 4}}, + }, + }, + }, + { + { + lset: labels.FromStrings("a", "c"), + chunks: [][]sample{ + {{20, 20}, {21, 21}, {22, 23}, {24, 24}}, // Almost same chunk as in set 1 (one value is different). + {{11, 11}, {12, 12}, {13, 13}, {14, 14}}, // Same chunk as in set 1. + }, + }, + }, + }, + + expected: []rawSeries{ + { + lset: labels.Labels{labels.Label{Name: "a", Value: "c"}}, + chunks: [][]sample{ + {{t: 11, v: 11}, {t: 12, v: 12}, {t: 13, v: 13}, {t: 14, v: 14}}, + {{t: 1, v: 1}, {t: 2, v: 2}, {t: 3, v: 3}, {t: 4, v: 4}}, + {{t: 20, v: 20}, {t: 21, v: 21}, {t: 22, v: 22}, {t: 24, v: 24}}, + {{t: 11, v: 11}, {t: 12, v: 12}, {t: 13, v: 13}, {t: 14, v: 14}}, + {{t: 15, v: 15}, {t: 16, v: 16}, {t: 17, v: 17}, {t: 18, v: 18}}, + {{t: 20, v: 20}, {t: 21, v: 21}, {t: 22, v: 23}, {t: 24, v: 24}}, + {{t: 11, v: 11}, {t: 12, v: 12}, {t: 13, v: 13}, {t: 14, v: 14}}, + }, }, }, }, } { - if ok := t.Run(tcase.desc, func(t *testing.T) { + t.Run(tcase.desc, func(t *testing.T) { var input []SeriesSet for _, iss := range tcase.in { input = append(input, newListSeriesSet(t, iss)) } - ss := MergeSeriesSets(input...) - seriesEquals(t, tcase.expected, ss) - testutil.Ok(t, ss.Err()) - }); !ok { - return - } + testutil.Equals(t, tcase.expected, expandSeriesSet(t, MergeSeriesSets(input...))) + }) } } @@ -239,42 +358,48 @@ type rawSeries struct { chunks [][]sample } -func seriesEquals(t *testing.T, expected []rawSeries, gotSS SeriesSet) { - var got []Series +func expandSeriesSet(t *testing.T, gotSS SeriesSet) (ret []rawSeries) { for gotSS.Next() { lset, chks := gotSS.At() - got = append(got, Series{Labels: lset, Chunks: chks}) - } - - testutil.Equals(t, len(expected), len(got), "got: %v", got) - - for i, series := range got { - testutil.Equals(t, expected[i].lset, LabelsToPromLabels(series.Labels)) - testutil.Equals(t, len(expected[i].chunks), len(series.Chunks), "unexpected number of chunks") - for k, chk := range series.Chunks { + r := rawSeries{lset: LabelsToPromLabels(lset), chunks: make([][]sample, len(chks))} + for i, chk := range chks { c, err := chunkenc.FromData(chunkenc.EncXOR, chk.Raw.Data) testutil.Ok(t, err) - j := 0 iter := c.Iterator(nil) for iter.Next() { - testutil.Assert(t, j < len(expected[i].chunks[k]), "more samples than expected for %s chunk %d", series.Labels, k) - - tv, v := iter.At() - testutil.Equals(t, expected[i].chunks[k][j], sample{tv, v}) - j++ + t, v := iter.At() + r.chunks[i] = append(r.chunks[i], sample{t: t, v: v}) } testutil.Ok(t, iter.Err()) - testutil.Equals(t, len(expected[i].chunks[k]), j) } + ret = append(ret, r) } - + testutil.Ok(t, gotSS.Err()) + return ret } // Test the cost of merging series sets for different number of merged sets and their size. -// The subset are all equivalent so this does not capture merging of partial or non-overlapping sets well. func BenchmarkMergedSeriesSet(b *testing.B) { + b.Run("overlapping chunks", func(b *testing.B) { + benchmarkMergedSeriesSet(testutil.NewTB(b), true) + }) + b.Run("non-overlapping chunks", func(b *testing.B) { + benchmarkMergedSeriesSet(testutil.NewTB(b), false) + }) +} + +func TestMergedSeriesSet_Labels(t *testing.T) { + t.Run("overlapping chunks", func(t *testing.T) { + benchmarkMergedSeriesSet(testutil.NewTB(t), true) + }) + t.Run("non-overlapping chunks", func(t *testing.T) { + benchmarkMergedSeriesSet(testutil.NewTB(t), false) + }) +} + +func benchmarkMergedSeriesSet(b testutil.TB, overlappingChunks bool) { var sel func(sets []SeriesSet) SeriesSet sel = func(sets []SeriesSet) SeriesSet { if len(sets) == 0 { @@ -295,25 +420,34 @@ func BenchmarkMergedSeriesSet(b *testing.B) { 20000, } { for _, j := range []int{1, 2, 4, 8, 16, 32} { - b.Run(fmt.Sprintf("series=%d,blocks=%d", k, j), func(b *testing.B) { + b.Run(fmt.Sprintf("series=%d,blocks=%d", k, j), func(b testutil.TB) { lbls, err := labels.ReadLabels(filepath.Join("../../testutil/testdata", "20kseries.json"), k) testutil.Ok(b, err) sort.Sort(labels.Slice(lbls)) - in := make([][]rawSeries, j) - + blocks := make([][]rawSeries, j) for _, l := range lbls { - for j := range in { - in[j] = append(in[j], rawSeries{lset: l, chunks: chunks}) + for j := range blocks { + if overlappingChunks { + blocks[j] = append(blocks[j], rawSeries{lset: l, chunks: chunks}) + continue + } + blocks[j] = append(blocks[j], rawSeries{ + lset: l, + chunks: [][]sample{ + {{int64(4*j) + 1, 1}, {int64(4*j) + 2, 2}}, + {{int64(4*j) + 3, 3}, {int64(4*j) + 4, 4}}, + }, + }) } } b.ResetTimer() - for i := 0; i < b.N; i++ { + for i := 0; i < b.N(); i++ { var sets []SeriesSet - for _, s := range in { + for _, s := range blocks { sets = append(sets, newListSeriesSet(b, s)) } ms := sel(sets) diff --git a/pkg/store/storepb/rpc.pb.go b/pkg/store/storepb/rpc.pb.go index 7f1ee6a5eb..8ed700da80 100644 --- a/pkg/store/storepb/rpc.pb.go +++ b/pkg/store/storepb/rpc.pb.go @@ -738,6 +738,9 @@ type StoreClient interface { /// partition of the single series, but once a new series is started to be streamed it means that no more data will /// be sent for previous one. /// Series has to be sorted. + /// + /// There is no requirements on chunk sorting, however it is recommended to have chunk sorted by chunk min time. + /// This heavily optimizes the resource usage on Querier / Federated Queries. Series(ctx context.Context, in *SeriesRequest, opts ...grpc.CallOption) (Store_SeriesClient, error) /// LabelNames returns all label names that is available. /// Currently unimplemented in all Thanos implementations, because Query API does not implement this either. @@ -824,6 +827,9 @@ type StoreServer interface { /// partition of the single series, but once a new series is started to be streamed it means that no more data will /// be sent for previous one. /// Series has to be sorted. + /// + /// There is no requirements on chunk sorting, however it is recommended to have chunk sorted by chunk min time. + /// This heavily optimizes the resource usage on Querier / Federated Queries. Series(*SeriesRequest, Store_SeriesServer) error /// LabelNames returns all label names that is available. /// Currently unimplemented in all Thanos implementations, because Query API does not implement this either. diff --git a/pkg/store/storepb/rpc.proto b/pkg/store/storepb/rpc.proto index 9d1ca8623c..3f96f9c2f5 100644 --- a/pkg/store/storepb/rpc.proto +++ b/pkg/store/storepb/rpc.proto @@ -34,6 +34,9 @@ service Store { /// partition of the single series, but once a new series is started to be streamed it means that no more data will /// be sent for previous one. /// Series has to be sorted. + /// + /// There is no requirements on chunk sorting, however it is recommended to have chunk sorted by chunk min time. + /// This heavily optimizes the resource usage on Querier / Federated Queries. rpc Series(SeriesRequest) returns (stream SeriesResponse); /// LabelNames returns all label names that is available. diff --git a/pkg/ui/bindata.go b/pkg/ui/bindata.go index 2c75df0560..82ba631bd4 100644 --- a/pkg/ui/bindata.go +++ b/pkg/ui/bindata.go @@ -1931,141 +1931,141 @@ type bintree struct { } var _bintree = &bintree{nil, map[string]*bintree{ - "pkg": &bintree{nil, map[string]*bintree{ - "ui": &bintree{nil, map[string]*bintree{ - "static": &bintree{nil, map[string]*bintree{ - "css": &bintree{nil, map[string]*bintree{ - "alerts.css": &bintree{pkgUiStaticCssAlertsCss, map[string]*bintree{}}, - "graph.css": &bintree{pkgUiStaticCssGraphCss, map[string]*bintree{}}, - "prometheus.css": &bintree{pkgUiStaticCssPrometheusCss, map[string]*bintree{}}, - "rules.css": &bintree{pkgUiStaticCssRulesCss, map[string]*bintree{}}, + "pkg": {nil, map[string]*bintree{ + "ui": {nil, map[string]*bintree{ + "static": {nil, map[string]*bintree{ + "css": {nil, map[string]*bintree{ + "alerts.css": {pkgUiStaticCssAlertsCss, map[string]*bintree{}}, + "graph.css": {pkgUiStaticCssGraphCss, map[string]*bintree{}}, + "prometheus.css": {pkgUiStaticCssPrometheusCss, map[string]*bintree{}}, + "rules.css": {pkgUiStaticCssRulesCss, map[string]*bintree{}}, }}, - "img": &bintree{nil, map[string]*bintree{ - "ajax-loader.gif": &bintree{pkgUiStaticImgAjaxLoaderGif, map[string]*bintree{}}, - "favicon.ico": &bintree{pkgUiStaticImgFaviconIco, map[string]*bintree{}}, + "img": {nil, map[string]*bintree{ + "ajax-loader.gif": {pkgUiStaticImgAjaxLoaderGif, map[string]*bintree{}}, + "favicon.ico": {pkgUiStaticImgFaviconIco, map[string]*bintree{}}, }}, - "js": &bintree{nil, map[string]*bintree{ - "alerts.js": &bintree{pkgUiStaticJsAlertsJs, map[string]*bintree{}}, - "bucket.js": &bintree{pkgUiStaticJsBucketJs, map[string]*bintree{}}, - "graph.js": &bintree{pkgUiStaticJsGraphJs, map[string]*bintree{}}, - "graph_template.handlebar": &bintree{pkgUiStaticJsGraph_templateHandlebar, map[string]*bintree{}}, + "js": {nil, map[string]*bintree{ + "alerts.js": {pkgUiStaticJsAlertsJs, map[string]*bintree{}}, + "bucket.js": {pkgUiStaticJsBucketJs, map[string]*bintree{}}, + "graph.js": {pkgUiStaticJsGraphJs, map[string]*bintree{}}, + "graph_template.handlebar": {pkgUiStaticJsGraph_templateHandlebar, map[string]*bintree{}}, }}, - "react": &bintree{nil, map[string]*bintree{ - "asset-manifest.json": &bintree{pkgUiStaticReactAssetManifestJson, map[string]*bintree{}}, - "favicon.ico": &bintree{pkgUiStaticReactFaviconIco, map[string]*bintree{}}, - "index.html": &bintree{pkgUiStaticReactIndexHtml, map[string]*bintree{}}, - "manifest.json": &bintree{pkgUiStaticReactManifestJson, map[string]*bintree{}}, - "precache-manifest.5b38a4fc0bde14c6f31f0e40057889e9.js": &bintree{pkgUiStaticReactPrecacheManifest5b38a4fc0bde14c6f31f0e40057889e9Js, map[string]*bintree{}}, - "service-worker.js": &bintree{pkgUiStaticReactServiceWorkerJs, map[string]*bintree{}}, - "static": &bintree{nil, map[string]*bintree{ - "css": &bintree{nil, map[string]*bintree{ - "2.df42c974.chunk.css": &bintree{pkgUiStaticReactStaticCss2Df42c974ChunkCss, map[string]*bintree{}}, - "main.02392ede.chunk.css": &bintree{pkgUiStaticReactStaticCssMain02392edeChunkCss, map[string]*bintree{}}, + "react": {nil, map[string]*bintree{ + "asset-manifest.json": {pkgUiStaticReactAssetManifestJson, map[string]*bintree{}}, + "favicon.ico": {pkgUiStaticReactFaviconIco, map[string]*bintree{}}, + "index.html": {pkgUiStaticReactIndexHtml, map[string]*bintree{}}, + "manifest.json": {pkgUiStaticReactManifestJson, map[string]*bintree{}}, + "precache-manifest.5b38a4fc0bde14c6f31f0e40057889e9.js": {pkgUiStaticReactPrecacheManifest5b38a4fc0bde14c6f31f0e40057889e9Js, map[string]*bintree{}}, + "service-worker.js": {pkgUiStaticReactServiceWorkerJs, map[string]*bintree{}}, + "static": {nil, map[string]*bintree{ + "css": {nil, map[string]*bintree{ + "2.df42c974.chunk.css": {pkgUiStaticReactStaticCss2Df42c974ChunkCss, map[string]*bintree{}}, + "main.02392ede.chunk.css": {pkgUiStaticReactStaticCssMain02392edeChunkCss, map[string]*bintree{}}, }}, - "js": &bintree{nil, map[string]*bintree{ - "2.b309ab18.chunk.js": &bintree{pkgUiStaticReactStaticJs2B309ab18ChunkJs, map[string]*bintree{}}, - "2.b309ab18.chunk.js.LICENSE.txt": &bintree{pkgUiStaticReactStaticJs2B309ab18ChunkJsLicenseTxt, map[string]*bintree{}}, - "main.bd8c49dc.chunk.js": &bintree{pkgUiStaticReactStaticJsMainBd8c49dcChunkJs, map[string]*bintree{}}, - "runtime-main.5db206b5.js": &bintree{pkgUiStaticReactStaticJsRuntimeMain5db206b5Js, map[string]*bintree{}}, + "js": {nil, map[string]*bintree{ + "2.b309ab18.chunk.js": {pkgUiStaticReactStaticJs2B309ab18ChunkJs, map[string]*bintree{}}, + "2.b309ab18.chunk.js.LICENSE.txt": {pkgUiStaticReactStaticJs2B309ab18ChunkJsLicenseTxt, map[string]*bintree{}}, + "main.bd8c49dc.chunk.js": {pkgUiStaticReactStaticJsMainBd8c49dcChunkJs, map[string]*bintree{}}, + "runtime-main.5db206b5.js": {pkgUiStaticReactStaticJsRuntimeMain5db206b5Js, map[string]*bintree{}}, }}, }}, }}, - "vendor": &bintree{nil, map[string]*bintree{ - "bootstrap-4.1.3": &bintree{nil, map[string]*bintree{ - "css": &bintree{nil, map[string]*bintree{ - "bootstrap-grid.css": &bintree{pkgUiStaticVendorBootstrap413CssBootstrapGridCss, map[string]*bintree{}}, - "bootstrap-grid.min.css": &bintree{pkgUiStaticVendorBootstrap413CssBootstrapGridMinCss, map[string]*bintree{}}, - "bootstrap-reboot.css": &bintree{pkgUiStaticVendorBootstrap413CssBootstrapRebootCss, map[string]*bintree{}}, - "bootstrap-reboot.min.css": &bintree{pkgUiStaticVendorBootstrap413CssBootstrapRebootMinCss, map[string]*bintree{}}, - "bootstrap.min.css": &bintree{pkgUiStaticVendorBootstrap413CssBootstrapMinCss, map[string]*bintree{}}, + "vendor": {nil, map[string]*bintree{ + "bootstrap-4.1.3": {nil, map[string]*bintree{ + "css": {nil, map[string]*bintree{ + "bootstrap-grid.css": {pkgUiStaticVendorBootstrap413CssBootstrapGridCss, map[string]*bintree{}}, + "bootstrap-grid.min.css": {pkgUiStaticVendorBootstrap413CssBootstrapGridMinCss, map[string]*bintree{}}, + "bootstrap-reboot.css": {pkgUiStaticVendorBootstrap413CssBootstrapRebootCss, map[string]*bintree{}}, + "bootstrap-reboot.min.css": {pkgUiStaticVendorBootstrap413CssBootstrapRebootMinCss, map[string]*bintree{}}, + "bootstrap.min.css": {pkgUiStaticVendorBootstrap413CssBootstrapMinCss, map[string]*bintree{}}, }}, - "js": &bintree{nil, map[string]*bintree{ - "bootstrap.bundle.js": &bintree{pkgUiStaticVendorBootstrap413JsBootstrapBundleJs, map[string]*bintree{}}, - "bootstrap.bundle.min.js": &bintree{pkgUiStaticVendorBootstrap413JsBootstrapBundleMinJs, map[string]*bintree{}}, - "bootstrap.min.js": &bintree{pkgUiStaticVendorBootstrap413JsBootstrapMinJs, map[string]*bintree{}}, + "js": {nil, map[string]*bintree{ + "bootstrap.bundle.js": {pkgUiStaticVendorBootstrap413JsBootstrapBundleJs, map[string]*bintree{}}, + "bootstrap.bundle.min.js": {pkgUiStaticVendorBootstrap413JsBootstrapBundleMinJs, map[string]*bintree{}}, + "bootstrap.min.js": {pkgUiStaticVendorBootstrap413JsBootstrapMinJs, map[string]*bintree{}}, }}, }}, - "bootstrap3-typeahead": &bintree{nil, map[string]*bintree{ - "bootstrap3-typeahead.min.js": &bintree{pkgUiStaticVendorBootstrap3TypeaheadBootstrap3TypeaheadMinJs, map[string]*bintree{}}, + "bootstrap3-typeahead": {nil, map[string]*bintree{ + "bootstrap3-typeahead.min.js": {pkgUiStaticVendorBootstrap3TypeaheadBootstrap3TypeaheadMinJs, map[string]*bintree{}}, }}, - "bootstrap4-glyphicons": &bintree{nil, map[string]*bintree{ - "css": &bintree{nil, map[string]*bintree{ - "bootstrap-glyphicons.css": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsCssBootstrapGlyphiconsCss, map[string]*bintree{}}, - "bootstrap-glyphicons.min.css": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsCssBootstrapGlyphiconsMinCss, map[string]*bintree{}}, + "bootstrap4-glyphicons": {nil, map[string]*bintree{ + "css": {nil, map[string]*bintree{ + "bootstrap-glyphicons.css": {pkgUiStaticVendorBootstrap4GlyphiconsCssBootstrapGlyphiconsCss, map[string]*bintree{}}, + "bootstrap-glyphicons.min.css": {pkgUiStaticVendorBootstrap4GlyphiconsCssBootstrapGlyphiconsMinCss, map[string]*bintree{}}, }}, - "fonts": &bintree{nil, map[string]*bintree{ - "fontawesome": &bintree{nil, map[string]*bintree{ - "fa-brands-400.eot": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaBrands400Eot, map[string]*bintree{}}, - "fa-brands-400.svg": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaBrands400Svg, map[string]*bintree{}}, - "fa-brands-400.ttf": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaBrands400Ttf, map[string]*bintree{}}, - "fa-brands-400.woff": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaBrands400Woff, map[string]*bintree{}}, - "fa-brands-400.woff2": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaBrands400Woff2, map[string]*bintree{}}, - "fa-regular-400.eot": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaRegular400Eot, map[string]*bintree{}}, - "fa-regular-400.svg": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaRegular400Svg, map[string]*bintree{}}, - "fa-regular-400.ttf": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaRegular400Ttf, map[string]*bintree{}}, - "fa-regular-400.woff": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaRegular400Woff, map[string]*bintree{}}, - "fa-regular-400.woff2": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaRegular400Woff2, map[string]*bintree{}}, - "fa-solid-900.eot": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaSolid900Eot, map[string]*bintree{}}, - "fa-solid-900.svg": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaSolid900Svg, map[string]*bintree{}}, - "fa-solid-900.ttf": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaSolid900Ttf, map[string]*bintree{}}, - "fa-solid-900.woff": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaSolid900Woff, map[string]*bintree{}}, - "fa-solid-900.woff2": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaSolid900Woff2, map[string]*bintree{}}, + "fonts": {nil, map[string]*bintree{ + "fontawesome": {nil, map[string]*bintree{ + "fa-brands-400.eot": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaBrands400Eot, map[string]*bintree{}}, + "fa-brands-400.svg": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaBrands400Svg, map[string]*bintree{}}, + "fa-brands-400.ttf": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaBrands400Ttf, map[string]*bintree{}}, + "fa-brands-400.woff": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaBrands400Woff, map[string]*bintree{}}, + "fa-brands-400.woff2": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaBrands400Woff2, map[string]*bintree{}}, + "fa-regular-400.eot": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaRegular400Eot, map[string]*bintree{}}, + "fa-regular-400.svg": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaRegular400Svg, map[string]*bintree{}}, + "fa-regular-400.ttf": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaRegular400Ttf, map[string]*bintree{}}, + "fa-regular-400.woff": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaRegular400Woff, map[string]*bintree{}}, + "fa-regular-400.woff2": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaRegular400Woff2, map[string]*bintree{}}, + "fa-solid-900.eot": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaSolid900Eot, map[string]*bintree{}}, + "fa-solid-900.svg": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaSolid900Svg, map[string]*bintree{}}, + "fa-solid-900.ttf": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaSolid900Ttf, map[string]*bintree{}}, + "fa-solid-900.woff": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaSolid900Woff, map[string]*bintree{}}, + "fa-solid-900.woff2": {pkgUiStaticVendorBootstrap4GlyphiconsFontsFontawesomeFaSolid900Woff2, map[string]*bintree{}}, }}, - "glyphicons": &bintree{nil, map[string]*bintree{ - "glyphicons-halflings-regular.eot": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsGlyphiconsGlyphiconsHalflingsRegularEot, map[string]*bintree{}}, - "glyphicons-halflings-regular.svg": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsGlyphiconsGlyphiconsHalflingsRegularSvg, map[string]*bintree{}}, - "glyphicons-halflings-regular.ttf": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsGlyphiconsGlyphiconsHalflingsRegularTtf, map[string]*bintree{}}, - "glyphicons-halflings-regular.woff": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsGlyphiconsGlyphiconsHalflingsRegularWoff, map[string]*bintree{}}, - "glyphicons-halflings-regular.woff2": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsFontsGlyphiconsGlyphiconsHalflingsRegularWoff2, map[string]*bintree{}}, + "glyphicons": {nil, map[string]*bintree{ + "glyphicons-halflings-regular.eot": {pkgUiStaticVendorBootstrap4GlyphiconsFontsGlyphiconsGlyphiconsHalflingsRegularEot, map[string]*bintree{}}, + "glyphicons-halflings-regular.svg": {pkgUiStaticVendorBootstrap4GlyphiconsFontsGlyphiconsGlyphiconsHalflingsRegularSvg, map[string]*bintree{}}, + "glyphicons-halflings-regular.ttf": {pkgUiStaticVendorBootstrap4GlyphiconsFontsGlyphiconsGlyphiconsHalflingsRegularTtf, map[string]*bintree{}}, + "glyphicons-halflings-regular.woff": {pkgUiStaticVendorBootstrap4GlyphiconsFontsGlyphiconsGlyphiconsHalflingsRegularWoff, map[string]*bintree{}}, + "glyphicons-halflings-regular.woff2": {pkgUiStaticVendorBootstrap4GlyphiconsFontsGlyphiconsGlyphiconsHalflingsRegularWoff2, map[string]*bintree{}}, }}, }}, - "maps": &bintree{nil, map[string]*bintree{ - "glyphicons-fontawesome.css": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsMapsGlyphiconsFontawesomeCss, map[string]*bintree{}}, - "glyphicons-fontawesome.less": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsMapsGlyphiconsFontawesomeLess, map[string]*bintree{}}, - "glyphicons-fontawesome.min.css": &bintree{pkgUiStaticVendorBootstrap4GlyphiconsMapsGlyphiconsFontawesomeMinCss, map[string]*bintree{}}, + "maps": {nil, map[string]*bintree{ + "glyphicons-fontawesome.css": {pkgUiStaticVendorBootstrap4GlyphiconsMapsGlyphiconsFontawesomeCss, map[string]*bintree{}}, + "glyphicons-fontawesome.less": {pkgUiStaticVendorBootstrap4GlyphiconsMapsGlyphiconsFontawesomeLess, map[string]*bintree{}}, + "glyphicons-fontawesome.min.css": {pkgUiStaticVendorBootstrap4GlyphiconsMapsGlyphiconsFontawesomeMinCss, map[string]*bintree{}}, }}, }}, - "eonasdan-bootstrap-datetimepicker": &bintree{nil, map[string]*bintree{ - "bootstrap-datetimepicker.min.css": &bintree{pkgUiStaticVendorEonasdanBootstrapDatetimepickerBootstrapDatetimepickerMinCss, map[string]*bintree{}}, - "bootstrap-datetimepicker.min.js": &bintree{pkgUiStaticVendorEonasdanBootstrapDatetimepickerBootstrapDatetimepickerMinJs, map[string]*bintree{}}, + "eonasdan-bootstrap-datetimepicker": {nil, map[string]*bintree{ + "bootstrap-datetimepicker.min.css": {pkgUiStaticVendorEonasdanBootstrapDatetimepickerBootstrapDatetimepickerMinCss, map[string]*bintree{}}, + "bootstrap-datetimepicker.min.js": {pkgUiStaticVendorEonasdanBootstrapDatetimepickerBootstrapDatetimepickerMinJs, map[string]*bintree{}}, }}, - "fuzzy": &bintree{nil, map[string]*bintree{ - "fuzzy.js": &bintree{pkgUiStaticVendorFuzzyFuzzyJs, map[string]*bintree{}}, + "fuzzy": {nil, map[string]*bintree{ + "fuzzy.js": {pkgUiStaticVendorFuzzyFuzzyJs, map[string]*bintree{}}, }}, - "js": &bintree{nil, map[string]*bintree{ - "jquery-3.5.0.min.js": &bintree{pkgUiStaticVendorJsJquery350MinJs, map[string]*bintree{}}, - "jquery.hotkeys.js": &bintree{pkgUiStaticVendorJsJqueryHotkeysJs, map[string]*bintree{}}, - "jquery.selection.js": &bintree{pkgUiStaticVendorJsJquerySelectionJs, map[string]*bintree{}}, - "popper.min.js": &bintree{pkgUiStaticVendorJsPopperMinJs, map[string]*bintree{}}, + "js": {nil, map[string]*bintree{ + "jquery-3.5.0.min.js": {pkgUiStaticVendorJsJquery350MinJs, map[string]*bintree{}}, + "jquery.hotkeys.js": {pkgUiStaticVendorJsJqueryHotkeysJs, map[string]*bintree{}}, + "jquery.selection.js": {pkgUiStaticVendorJsJquerySelectionJs, map[string]*bintree{}}, + "popper.min.js": {pkgUiStaticVendorJsPopperMinJs, map[string]*bintree{}}, }}, - "moment": &bintree{nil, map[string]*bintree{ - "moment-timezone-with-data.min.js": &bintree{pkgUiStaticVendorMomentMomentTimezoneWithDataMinJs, map[string]*bintree{}}, - "moment.min.js": &bintree{pkgUiStaticVendorMomentMomentMinJs, map[string]*bintree{}}, + "moment": {nil, map[string]*bintree{ + "moment-timezone-with-data.min.js": {pkgUiStaticVendorMomentMomentTimezoneWithDataMinJs, map[string]*bintree{}}, + "moment.min.js": {pkgUiStaticVendorMomentMomentMinJs, map[string]*bintree{}}, }}, - "mustache": &bintree{nil, map[string]*bintree{ - "mustache.min.js": &bintree{pkgUiStaticVendorMustacheMustacheMinJs, map[string]*bintree{}}, + "mustache": {nil, map[string]*bintree{ + "mustache.min.js": {pkgUiStaticVendorMustacheMustacheMinJs, map[string]*bintree{}}, }}, - "rickshaw": &bintree{nil, map[string]*bintree{ - "rickshaw.min.css": &bintree{pkgUiStaticVendorRickshawRickshawMinCss, map[string]*bintree{}}, - "rickshaw.min.js": &bintree{pkgUiStaticVendorRickshawRickshawMinJs, map[string]*bintree{}}, - "vendor": &bintree{nil, map[string]*bintree{ - "d3.layout.min.js": &bintree{pkgUiStaticVendorRickshawVendorD3LayoutMinJs, map[string]*bintree{}}, - "d3.v3.js": &bintree{pkgUiStaticVendorRickshawVendorD3V3Js, map[string]*bintree{}}, + "rickshaw": {nil, map[string]*bintree{ + "rickshaw.min.css": {pkgUiStaticVendorRickshawRickshawMinCss, map[string]*bintree{}}, + "rickshaw.min.js": {pkgUiStaticVendorRickshawRickshawMinJs, map[string]*bintree{}}, + "vendor": {nil, map[string]*bintree{ + "d3.layout.min.js": {pkgUiStaticVendorRickshawVendorD3LayoutMinJs, map[string]*bintree{}}, + "d3.v3.js": {pkgUiStaticVendorRickshawVendorD3V3Js, map[string]*bintree{}}, }}, }}, }}, }}, - "templates": &bintree{nil, map[string]*bintree{ - "_base.html": &bintree{pkgUiTemplates_baseHtml, map[string]*bintree{}}, - "alerts.html": &bintree{pkgUiTemplatesAlertsHtml, map[string]*bintree{}}, - "bucket.html": &bintree{pkgUiTemplatesBucketHtml, map[string]*bintree{}}, - "bucket_menu.html": &bintree{pkgUiTemplatesBucket_menuHtml, map[string]*bintree{}}, - "graph.html": &bintree{pkgUiTemplatesGraphHtml, map[string]*bintree{}}, - "query_menu.html": &bintree{pkgUiTemplatesQuery_menuHtml, map[string]*bintree{}}, - "rule_menu.html": &bintree{pkgUiTemplatesRule_menuHtml, map[string]*bintree{}}, - "rules.html": &bintree{pkgUiTemplatesRulesHtml, map[string]*bintree{}}, - "status.html": &bintree{pkgUiTemplatesStatusHtml, map[string]*bintree{}}, - "stores.html": &bintree{pkgUiTemplatesStoresHtml, map[string]*bintree{}}, + "templates": {nil, map[string]*bintree{ + "_base.html": {pkgUiTemplates_baseHtml, map[string]*bintree{}}, + "alerts.html": {pkgUiTemplatesAlertsHtml, map[string]*bintree{}}, + "bucket.html": {pkgUiTemplatesBucketHtml, map[string]*bintree{}}, + "bucket_menu.html": {pkgUiTemplatesBucket_menuHtml, map[string]*bintree{}}, + "graph.html": {pkgUiTemplatesGraphHtml, map[string]*bintree{}}, + "query_menu.html": {pkgUiTemplatesQuery_menuHtml, map[string]*bintree{}}, + "rule_menu.html": {pkgUiTemplatesRule_menuHtml, map[string]*bintree{}}, + "rules.html": {pkgUiTemplatesRulesHtml, map[string]*bintree{}}, + "status.html": {pkgUiTemplatesStatusHtml, map[string]*bintree{}}, + "stores.html": {pkgUiTemplatesStoresHtml, map[string]*bintree{}}, }}, }}, }},