VictoriaMetrics/lib/bytesutil/fast_string_transformer.go
Aliaksandr Valialkin f3e5c9c246
lib/bytesutil: cache results for all the input strings, which were passed during the last 5 minutes from FastStringMatcher.Match(), FastStringTransformer.Transform() and InternString()
Previously only up to 100K results were cached.
This could result in sub-optimal performance when more than 100K unique strings were actually used.
For example, when the relabeling rule was applied to a million of unique Graphite metric names
like in the https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3466

This commit should reduce the long-term CPU usage for https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3466
after all the unique Graphite metrics are registered in the FastStringMatcher.Transform() cache.

It is expected that the number of unique strings, which are passed to FastStringMatcher.Match(),
FastStringTransformer.Transform() and to InternString() during the last 5 minutes,
is limited, so the function results fit memory. Otherwise OOM crash can occur.
This should be the case for typical production workloads.
2022-12-12 14:47:00 -08:00

86 lines
2.5 KiB
Go

package bytesutil
import (
"strings"
"sync"
"sync/atomic"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
)
// FastStringTransformer implements fast transformer for strings.
//
// It caches transformed strings and returns them back on the next calls
// without calling the transformFunc, which may be expensive.
type FastStringTransformer struct {
lastCleanupTime uint64
m sync.Map
transformFunc func(s string) string
}
type fstEntry struct {
lastAccessTime uint64
s string
}
// NewFastStringTransformer creates new transformer, which applies transformFunc to strings passed to Transform()
//
// transformFunc must return the same result for the same input.
func NewFastStringTransformer(transformFunc func(s string) string) *FastStringTransformer {
return &FastStringTransformer{
lastCleanupTime: fasttime.UnixTimestamp(),
transformFunc: transformFunc,
}
}
// Transform applies transformFunc to s and returns the result.
func (fst *FastStringTransformer) Transform(s string) string {
ct := fasttime.UnixTimestamp()
v, ok := fst.m.Load(s)
if ok {
// Fast path - the transformed s is found in the cache.
e := v.(*fstEntry)
if atomic.LoadUint64(&e.lastAccessTime)+10 < ct {
// Reduce the frequency of e.lastAccessTime update to once per 10 seconds
// in order to improve the fast path speed on systems with many CPU cores.
atomic.StoreUint64(&e.lastAccessTime, ct)
}
return e.s
}
// Slow path - transform s and store it in the cache.
sTransformed := fst.transformFunc(s)
// Make a copy of s in order to limit memory usage to the s length,
// since the s may point to bigger string.
// This also protects from the case when s contains unsafe string, which points to a temporary byte slice.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3227
s = strings.Clone(s)
if sTransformed == s {
// point sTransformed to just allocated s, since it may point to s,
// which, in turn, can point to bigger string.
sTransformed = s
}
e := &fstEntry{
lastAccessTime: ct,
s: sTransformed,
}
fst.m.Store(s, e)
if atomic.LoadUint64(&fst.lastCleanupTime)+61 < ct {
// Perform a global cleanup for fst.m by removing items, which weren't accessed
// during the last 5 minutes.
atomic.StoreUint64(&fst.lastCleanupTime, ct)
m := &fst.m
m.Range(func(k, v interface{}) bool {
e := v.(*fstEntry)
if atomic.LoadUint64(&e.lastAccessTime)+5*60 < ct {
m.Delete(k)
}
return true
})
}
return sTransformed
}