diff --git a/model/labels/labels.go b/model/labels/labels.go index 01514abf3..8aacd4801 100644 --- a/model/labels/labels.go +++ b/model/labels/labels.go @@ -19,6 +19,7 @@ import ( "bytes" "slices" "strings" + "unsafe" "github.com/cespare/xxhash/v2" ) @@ -487,3 +488,8 @@ func (b *ScratchBuilder) Labels() Labels { func (b *ScratchBuilder) Overwrite(ls *Labels) { *ls = append((*ls)[:0], b.add...) } + +// SizeOfLabels returns the approximate space required for n copies of a label. +func SizeOfLabels(value string, n uint64) uint64 { + return (uint64(len(value)) + uint64(unsafe.Sizeof(value))) * n +} diff --git a/model/labels/labels_dedupelabels.go b/model/labels/labels_dedupelabels.go index 0e5bb048b..0793292c0 100644 --- a/model/labels/labels_dedupelabels.go +++ b/model/labels/labels_dedupelabels.go @@ -815,3 +815,8 @@ func (b *ScratchBuilder) Overwrite(ls *Labels) { ls.syms = b.syms.nameTable ls.data = yoloString(b.overwriteBuffer) } + +// SizeOfLabels returns the approximate space required for n copies of a label. +func SizeOfLabels(value string, n uint64) uint64 { + return uint64(len(value)) + n*2 // Assuming most symbol-table entries are 2 bytes long. +} diff --git a/model/labels/labels_stringlabels.go b/model/labels/labels_stringlabels.go index bccceb61f..6da5e3176 100644 --- a/model/labels/labels_stringlabels.go +++ b/model/labels/labels_stringlabels.go @@ -694,3 +694,8 @@ func NewScratchBuilderWithSymbolTable(_ *SymbolTable, n int) ScratchBuilder { func (b *ScratchBuilder) SetSymbolTable(_ *SymbolTable) { // no-op } + +// SizeOfLabels returns the approximate space required for n copies of a label. +func SizeOfLabels(value string, n uint64) uint64 { + return uint64(len(value)+sizeVarint(uint64(len(value)))) * n +} diff --git a/tsdb/head.go b/tsdb/head.go index b7bfaa0fd..1d857127c 100644 --- a/tsdb/head.go +++ b/tsdb/head.go @@ -1036,7 +1036,7 @@ func (h *Head) PostingsCardinalityStats(statsByLabelName string, limit int) *ind return h.cardinalityCache } h.cardinalityCacheKey = cacheKey - h.cardinalityCache = h.postings.Stats(statsByLabelName, limit) + h.cardinalityCache = h.postings.Stats(statsByLabelName, limit, labels.SizeOfLabels) h.lastPostingsStatsCall = time.Duration(time.Now().Unix()) * time.Second return h.cardinalityCache diff --git a/tsdb/index/postings.go b/tsdb/index/postings.go index bfe74c323..4a01f7152 100644 --- a/tsdb/index/postings.go +++ b/tsdb/index/postings.go @@ -163,7 +163,8 @@ type PostingsStats struct { } // Stats calculates the cardinality statistics from postings. -func (p *MemPostings) Stats(label string, limit int) *PostingsStats { +// Caller can pass in a function which computes the space required for n series with a given label. +func (p *MemPostings) Stats(label string, limit int, bytes func(string, uint64) uint64) *PostingsStats { var size uint64 p.mtx.RLock() @@ -191,7 +192,7 @@ func (p *MemPostings) Stats(label string, limit int) *PostingsStats { } seriesCnt := uint64(len(values)) labelValuePairs.push(Stat{Name: n + "=" + name, Count: seriesCnt}) - size += uint64(len(name)) * seriesCnt + size += bytes(name, seriesCnt) } labelValueLength.push(Stat{Name: n, Count: size}) } diff --git a/tsdb/index/postings_test.go b/tsdb/index/postings_test.go index 96c9ed124..ee523a6c8 100644 --- a/tsdb/index/postings_test.go +++ b/tsdb/index/postings_test.go @@ -939,7 +939,7 @@ func BenchmarkPostings_Stats(b *testing.B) { } b.ResetTimer() for n := 0; n < b.N; n++ { - p.Stats("__name__", 10) + p.Stats("__name__", 10, labels.SizeOfLabels) } } @@ -954,7 +954,7 @@ func TestMemPostingsStats(t *testing.T) { p.Add(2, labels.FromStrings("label", "value1")) // call the Stats method to calculate the cardinality statistics - stats := p.Stats("label", 10) + stats := p.Stats("label", 10, func(s string, n uint64) uint64 { return uint64(len(s)) * n }) // assert that the expected statistics were calculated require.Equal(t, uint64(2), stats.CardinalityMetricsStats[0].Count)