diff --git a/model/labels/labels.go b/model/labels/labels.go index 2e8dc52cb3..a6e5654fa7 100644 --- a/model/labels/labels.go +++ b/model/labels/labels.go @@ -248,6 +248,17 @@ func (ls Labels) WithoutEmpty() Labels { return ls } +// ByteSize returns the approximate size of the labels in bytes including +// the two string headers size for name and value. +// Slice header size is ignored because it should be amortized to zero. +func (ls Labels) ByteSize() uint64 { + var size uint64 = 0 + for _, l := range ls { + size += uint64(len(l.Name)+len(l.Value)) + 2*uint64(unsafe.Sizeof("")) + } + return size +} + // Equal returns whether the two label sets are equal. func Equal(ls, o Labels) bool { return slices.Equal(ls, o) diff --git a/model/labels/labels_dedupelabels.go b/model/labels/labels_dedupelabels.go index 64e0a69b83..edc6ff8e82 100644 --- a/model/labels/labels_dedupelabels.go +++ b/model/labels/labels_dedupelabels.go @@ -417,6 +417,13 @@ func (ls Labels) WithoutEmpty() Labels { return ls } +// ByteSize returns the approximate size of the labels in bytes. +// String header size is ignored because it should be amortized to zero. +// SymbolTable size is also not taken into account. +func (ls Labels) ByteSize() uint64 { + return uint64(len(ls.data)) +} + // Equal returns whether the two label sets are equal. func Equal(a, b Labels) bool { if a.syms == b.syms { diff --git a/model/labels/labels_dedupelabels_test.go b/model/labels/labels_dedupelabels_test.go index baf0423db2..229bb45a8e 100644 --- a/model/labels/labels_dedupelabels_test.go +++ b/model/labels/labels_dedupelabels_test.go @@ -30,6 +30,15 @@ var expectedSizeOfLabels = []uint64{ // Values must line up with testCaseLabels. 325, } +var expectedByteSize = []uint64{ // Values must line up with testCaseLabels. + 8, + 0, + 8, + 8, + 8, + 32, +} + func TestVarint(t *testing.T) { cases := []struct { v int diff --git a/model/labels/labels_slicelabels_test.go b/model/labels/labels_slicelabels_test.go index e9edc9152d..2d592ef5b5 100644 --- a/model/labels/labels_slicelabels_test.go +++ b/model/labels/labels_slicelabels_test.go @@ -23,3 +23,5 @@ var expectedSizeOfLabels = []uint64{ // Values must line up with testCaseLabels. 327, 549, } + +var expectedByteSize = expectedSizeOfLabels // They are identical diff --git a/model/labels/labels_stringlabels.go b/model/labels/labels_stringlabels.go index a2b16cac76..4b9bfd15af 100644 --- a/model/labels/labels_stringlabels.go +++ b/model/labels/labels_stringlabels.go @@ -283,6 +283,13 @@ func (ls Labels) WithoutEmpty() Labels { return ls } +// ByteSize returns the approximate size of the labels in bytes. +// String header size is ignored because it should be amortized to zero +// because it may be shared across multiple copies of the Labels. +func (ls Labels) ByteSize() uint64 { + return uint64(len(ls.data)) +} + // Equal returns whether the two label sets are equal. func Equal(ls, o Labels) bool { return ls.data == o.data diff --git a/model/labels/labels_stringlabels_test.go b/model/labels/labels_stringlabels_test.go index aaa8b52415..0704a2ff36 100644 --- a/model/labels/labels_stringlabels_test.go +++ b/model/labels/labels_stringlabels_test.go @@ -23,3 +23,12 @@ var expectedSizeOfLabels = []uint64{ // Values must line up with testCaseLabels. 270, 309, } + +var expectedByteSize = []uint64{ // Values must line up with testCaseLabels. + 12, + 0, + 37, + 266, + 270, + 309, +} diff --git a/model/labels/labels_test.go b/model/labels/labels_test.go index 3b4f802160..4b23748a91 100644 --- a/model/labels/labels_test.go +++ b/model/labels/labels_test.go @@ -74,6 +74,33 @@ func TestSizeOfLabels(t *testing.T) { } } +func TestByteSize(t *testing.T) { + require.Len(t, expectedByteSize, len(testCaseLabels)) + for i, c := range expectedByteSize { // Declared in build-tag-specific files, e.g. labels_slicelabels_test.go. + require.Equal(t, c, testCaseLabels[i].ByteSize()) + } +} + +var GlobalTotal uint64 // Encourage the compiler not to elide the benchmark computation. + +func BenchmarkSize(b *testing.B) { + lb := New(benchmarkLabels...) + b.Run("SizeOfLabels", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var total uint64 + lb.Range(func(l Label) { + total += SizeOfLabels(l.Name, l.Value, 1) + }) + GlobalTotal = total + } + }) + b.Run("ByteSize", func(b *testing.B) { + for i := 0; i < b.N; i++ { + GlobalTotal = lb.ByteSize() + } + }) +} + func TestLabels_MatchLabels(t *testing.T) { labels := FromStrings( "__name__", "ALERTS",