Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- uses: prometheus/promci@443c7fc2397e946bc9f5029e313a9c3441b9b86d # v0.4.7
- uses: ./.github/promci/actions/setup_environment
- run: go test --tags=dedupelabels ./...
- run: go test --tags=slicelabels -race ./cmd/prometheus ./prompb/io/prometheus/client
- run: go test --tags=slicelabels -race ./cmd/prometheus ./model/textparse ./prompb/...
- run: go test --tags=forcedirectio -race ./tsdb/
- run: GOARCH=386 go test ./...
- uses: ./.github/promci/actions/check_proto
Expand Down
15 changes: 9 additions & 6 deletions model/labels/labels_dedupelabels.go
Original file line number Diff line number Diff line change
Expand Up @@ -775,23 +775,26 @@ func (b *ScratchBuilder) SetSymbolTable(s *SymbolTable) {
b.syms = s
}

// SetUnsafeAdd allows turning on/off the assumptions that added strings are unsafe
// for reuse. ScratchBuilder implementations that do reuse strings, must clone
// the strings.
//
// DedupeLabels implementation copies any new strings to the symbolTable when
// Labels() is called, so this operation is noop.
func (ScratchBuilder) SetUnsafeAdd(bool) {}

func (b *ScratchBuilder) Reset() {
b.add = b.add[:0]
b.output = EmptyLabels()
}

// Add a name/value pair.
// Note if you Add the same name twice you will get a duplicate label, which is invalid.
// The values must remain live until Labels() is called.
func (b *ScratchBuilder) Add(name, value string) {
b.add = append(b.add, Label{Name: name, Value: value})
}

// UnsafeAddBytes adds a name/value pair, using []byte instead of string to reduce memory allocations.
// The values must remain live until Labels() is called.
func (b *ScratchBuilder) UnsafeAddBytes(name, value []byte) {
b.add = append(b.add, Label{Name: yoloString(name), Value: yoloString(value)})
}

// Sort the labels added so far by name.
func (b *ScratchBuilder) Sort() {
slices.SortFunc(b.add, func(a, b Label) int { return strings.Compare(a.Name, b.Name) })
Expand Down
27 changes: 19 additions & 8 deletions model/labels/labels_slicelabels.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"bytes"
"slices"
"strings"
"unique"
"unsafe"

"github.com/cespare/xxhash/v2"
Expand Down Expand Up @@ -437,7 +438,8 @@ func (b *Builder) Labels() Labels {

// ScratchBuilder allows efficient construction of a Labels from scratch.
type ScratchBuilder struct {
add Labels
add Labels
unsafeAdd bool
}

// SymbolTable is no-op, just for api parity with dedupelabels.
Expand Down Expand Up @@ -466,23 +468,32 @@ func (*ScratchBuilder) SetSymbolTable(*SymbolTable) {
// no-op
}

// SetUnsafeAdd allows turning on/off the assumptions that added strings are unsafe
// for reuse. ScratchBuilder implementations that do reuse strings, must clone
// the strings.
//
// SliceLabels will clone all added strings when this option is true.
func (b *ScratchBuilder) SetUnsafeAdd(unsafeAdd bool) {
b.unsafeAdd = unsafeAdd
}

func (b *ScratchBuilder) Reset() {
b.add = b.add[:0]
}

// Add a name/value pair.
// Note if you Add the same name twice you will get a duplicate label, which is invalid.
// If SetUnsafeAdd was set to false, the values must remain live until Labels() is called.
func (b *ScratchBuilder) Add(name, value string) {
if b.unsafeAdd {
// Underlying label structure for slicelabels shares memory, so we need to
// copy it if the input is unsafe.
name = unique.Make(name).Value()
value = unique.Make(value).Value()
}
b.add = append(b.add, Label{Name: name, Value: value})
}

// UnsafeAddBytes adds a name/value pair, using []byte instead of string.
// The default version of this function is unsafe, hence the name.
// This version is safe - it copies the strings immediately - but we keep the same name so everything compiles.
func (b *ScratchBuilder) UnsafeAddBytes(name, value []byte) {
b.add = append(b.add, Label{Name: string(name), Value: string(value)})
}

// Sort the labels added so far by name.
func (b *ScratchBuilder) Sort() {
slices.SortFunc(b.add, func(a, b Label) int { return strings.Compare(a.Name, b.Name) })
Expand Down
58 changes: 58 additions & 0 deletions model/labels/labels_slicelabels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@

package labels

import (
"testing"

"github.com/stretchr/testify/require"
)

var expectedSizeOfLabels = []uint64{ // Values must line up with testCaseLabels.
72,
0,
Expand All @@ -25,3 +31,55 @@ var expectedSizeOfLabels = []uint64{ // Values must line up with testCaseLabels.
}

var expectedByteSize = expectedSizeOfLabels // They are identical

func TestScratchBuilderAdd_Strings(t *testing.T) {
t.Run("safe", func(t *testing.T) {
n := []byte("__name__")
v := []byte("metric1")

l := NewScratchBuilder(0)
l.Add(yoloString(n), yoloString(v))
ret := l.Labels()

// For slicelabels, in default mode strings are reused, so modifying the
// intput will cause `ret` labels to change too.
n[1] = byte('?')
v[2] = byte('?')

require.Empty(t, ret.Get("__name__"))
require.Equal(t, "me?ric1", ret.Get("_?name__"))
})
t.Run("unsafe", func(t *testing.T) {
n := []byte("__name__")
v := []byte("metric1")

l := NewScratchBuilder(0)
l.SetUnsafeAdd(true)
l.Add(yoloString(n), yoloString(v))
ret := l.Labels()

// Changing input strings should be now safe, because we marked adds as unsafe.
n[1] = byte('?')
v[2] = byte('?')

require.Equal(t, "metric1", ret.Get("__name__"))
})
}

/*
export bench=unsafe && go test -tags=slicelabels \
-run '^$' -bench '^BenchmarkScratchBuilderUnsafeAdd' \
-benchtime 5s -count 6 -cpu 2 -timeout 999m \
| tee ${bench}.txt
*/
func BenchmarkScratchBuilderUnsafeAdd(b *testing.B) {
l := NewScratchBuilder(0)
l.SetUnsafeAdd(true)

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
l.Add("__name__", "metric1")
l.add = l.add[:0] // Reset slice so add can be repeated without side effects.
}
}
14 changes: 8 additions & 6 deletions model/labels/labels_stringlabels.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,16 +614,11 @@ func (b *ScratchBuilder) Reset() {

// Add a name/value pair.
// Note if you Add the same name twice you will get a duplicate label, which is invalid.
// The values must remain live until Labels() is called.
func (b *ScratchBuilder) Add(name, value string) {
b.add = append(b.add, Label{Name: name, Value: value})
}

// UnsafeAddBytes adds a name/value pair using []byte instead of string to reduce memory allocations.
// The values must remain live until Labels() is called.
func (b *ScratchBuilder) UnsafeAddBytes(name, value []byte) {
b.add = append(b.add, Label{Name: yoloString(name), Value: yoloString(value)})
}

// Sort the labels added so far by name.
func (b *ScratchBuilder) Sort() {
slices.SortFunc(b.add, func(a, b Label) int { return strings.Compare(a.Name, b.Name) })
Expand Down Expand Up @@ -680,6 +675,13 @@ func (*ScratchBuilder) SetSymbolTable(*SymbolTable) {
// no-op
}

// SetUnsafeAdd allows turning on/off the assumptions that added strings are unsafe
// for reuse. ScratchBuilder implementations that do reuse strings, must clone
// the strings.
//
// StringLabels implementation copies all strings when Labels() is called, so this operation is noop.
func (ScratchBuilder) SetUnsafeAdd(bool) {}

// SizeOfLabels returns the approximate space required for n copies of a label.
func SizeOfLabels(name, value string, n uint64) uint64 {
return uint64(labelSize(&Label{Name: name, Value: value})) * n
Expand Down
9 changes: 4 additions & 5 deletions model/textparse/protobufparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,12 @@ type ProtobufParser struct {

// NewProtobufParser returns a parser for the payload in the byte slice.
func NewProtobufParser(b []byte, parseClassicHistograms, convertClassicHistogramsToNHCB, enableTypeAndUnitLabels bool, st *labels.SymbolTable) Parser {
builder := labels.NewScratchBuilderWithSymbolTable(st, 16)
builder.SetUnsafeAdd(true)
return &ProtobufParser{
dec: dto.NewMetricStreamingDecoder(b),
entryBytes: &bytes.Buffer{},
builder: labels.NewScratchBuilderWithSymbolTable(st, 16), // TODO(bwplotka): Try base builder.
builder: builder,

state: EntryInvalid,
parseClassicHistograms: parseClassicHistograms,
Expand Down Expand Up @@ -622,10 +624,7 @@ func (p *ProtobufParser) onSeriesOrHistogramUpdate() error {
Unit: p.dec.GetUnit(),
}
m.AddToLabels(&p.builder)
if err := p.dec.Label(schema.IgnoreOverriddenMetadataLabelsScratchBuilder{
Overwrite: m,
ScratchBuilder: &p.builder,
}); err != nil {
if err := p.dec.Label(m.NewIgnoreOverriddenMetadataLabelScratchBuilder(&p.builder)); err != nil {
return err
}
} else {
Expand Down
Loading
Loading