fix: optimizations to AppenderV2
Some checks failed
CI / Go tests (push) Has been cancelled
CI / More Go tests (push) Has been cancelled
CI / Go tests with previous Go version (push) Has been cancelled
CI / UI tests (push) Has been cancelled
CI / Go tests on Windows (push) Has been cancelled
CI / Mixins tests (push) Has been cancelled
CI / Build Prometheus for common architectures (push) Has been cancelled
CI / Build Prometheus for all architectures (push) Has been cancelled
CI / Check generated parser (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
CI / fuzzing (push) Has been cancelled
CI / codeql (push) Has been cancelled
CI / Report status of build Prometheus for all architectures (push) Has been cancelled
CI / Publish main branch artifacts (push) Has been cancelled
CI / Publish release artefacts (push) Has been cancelled
CI / Publish UI on npm Registry (push) Has been cancelled

Signed-off-by: bwplotka <bwplotka@gmail.com>

fix: 48B alloc optimization to AppenderV2

Signed-off-by: bwplotka <bwplotka@gmail.com>
This commit is contained in:
bwplotka 2026-01-26 11:38:33 +00:00
parent f292e7bdd3
commit bf0734878c
2 changed files with 31 additions and 25 deletions

View file

@ -32,6 +32,8 @@ import (
"github.com/prometheus/prometheus/storage"
)
const exemplarsForParsing = 1 // const for readability.
// appenderWithLimits returns an appender with additional validation.
func appenderV2WithLimits(app storage.AppenderV2, sampleLimit, bucketLimit int, maxSchema int32) storage.AppenderV2 {
app = &timeLimitAppenderV2{
@ -122,17 +124,18 @@ func (sl *scrapeLoopAppenderV2) append(b []byte, contentType string, ts time.Tim
"err", err,
)
}
var (
appErrs = appendErrors{}
sampleLimitErr error
bucketLimitErr error
lset labels.Labels // Escapes to heap so hoisted out of loop.
e exemplar.Exemplar // Escapes to heap so hoisted out of loop.
lastMeta *metaEntry
lastMFName []byte
)
exemplars := make([]exemplar.Exemplar, 0, 1)
// Below escape to heap, so they're hoisted out of loop.
lset labels.Labels
exemplars = make([]exemplar.Exemplar, exemplarsForParsing) // First sample is a buffer for parser fetches.
)
// Take an appender with limits.
app := appenderV2WithLimits(sl.AppenderV2, sl.sampleLimit, sl.bucketLimit, sl.maxSchema)
@ -240,8 +243,7 @@ loop:
}
}
exemplars = exemplars[:0] // Reset and reuse the exemplar slice.
appOpts := storage.AOptions{}
if seriesAlreadyScraped && parsedTimestamp == nil {
err = storage.ErrDuplicateSampleForTimestamp
} else {
@ -259,8 +261,14 @@ loop:
st = p.StartTimestamp()
}
for hasExemplar := p.Exemplar(&e); hasExemplar; hasExemplar = p.Exemplar(&e) {
if !e.HasTs {
// Fetch exemplars from the parser, if any, while using exemplar slice.
exemplars = exemplars[:exemplarsForParsing]
for {
exemplars[0] = exemplar.Exemplar{} // Always reset, before fetching.
if !p.Exemplar(&(exemplars[0])) {
break
}
if !exemplars[0].HasTs {
if isHistogram {
// We drop exemplars for native histograms if they don't have a timestamp.
// Missing timestamps are deliberately not supported as we want to start
@ -269,21 +277,18 @@ loop:
// between repeated exemplars and new instances with the same values.
// This is done silently without logs as it is not an error but out of spec.
// This does not affect classic histograms so that behaviour is unchanged.
e = exemplar.Exemplar{} // Reset for the next fetch.
continue
}
e.Ts = t
exemplars[0].Ts = t
}
exemplars = append(exemplars, e)
e = exemplar.Exemplar{} // Reset for the next fetch.
exemplars = append(exemplars, exemplars[0])
}
// Prepare append call.
appOpts := storage.AOptions{}
if len(exemplars) > 0 {
if len(exemplars) > exemplarsForParsing {
// Sort so that checking for duplicates / out of order is more efficient during validation.
slices.SortFunc(exemplars, exemplar.Compare)
appOpts.Exemplars = exemplars
appOpts.Exemplars = exemplars[exemplarsForParsing:]
slices.SortFunc(appOpts.Exemplars, exemplar.Compare)
}
// Metadata path mimicks the scrape appender V1 flow. Once we remove v2
@ -314,7 +319,7 @@ loop:
// Append sample to the storage.
ref, err = app.Append(ref, lset, st, t, val, h, fh, appOpts)
}
sampleAdded, err = sl.checkAddError(met, exemplars, err, &sampleLimitErr, &bucketLimitErr, &appErrs)
sampleAdded, err = sl.checkAddError(met, appOpts.Exemplars, err, &sampleLimitErr, &bucketLimitErr, &appErrs)
if err != nil {
if !errors.Is(err, storage.ErrNotFound) {
sl.l.Debug("Unexpected error", "series", string(met), "err", err)

View file

@ -109,22 +109,23 @@ type headAppenderV2 struct {
func (a *headAppenderV2) Append(ref storage.SeriesRef, ls labels.Labels, st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts storage.AOptions) (storage.SeriesRef, error) {
var (
// Avoid shadowing err variables for reliability.
valErr, appErr, partialErr error
sampleMetricType = sampleMetricTypeFloat
isStale bool
appErr, partialErr error
sampleMetricType = sampleMetricTypeFloat
isStale bool
)
// Fail fast on incorrect histograms.
switch {
case fh != nil:
sampleMetricType = sampleMetricTypeHistogram
valErr = fh.Validate()
if err := h.Validate(); err != nil {
return 0, err
}
case h != nil:
sampleMetricType = sampleMetricTypeHistogram
valErr = h.Validate()
}
if valErr != nil {
return 0, valErr
if err := h.Validate(); err != nil {
return 0, err
}
}
// Fail fast if OOO is disabled and the sample is out of bounds.