Files
Aliaksandr Valialkin 9fa9fc18a7 lib/protoparser/opentelemetry: eliminate memory allocations during parsing of samples send via OpenTelemetry protocol
This increases the parser performance by 4x-6x.

This commit uses the technique similar to https://github.com/VictoriaMetrics/VictoriaLogs/pull/720

goos: linux
goarch: amd64
pkg: github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/stream
cpu: AMD Ryzen 7 PRO 5850U with Radeon Graphics
                                                    │   old.txt    │               new.txt               │
                                                    │    sec/op    │   sec/op     vs base                │
ParseStream/default-metrics-labels-formatting-16      15.565µ ± 1%   2.150µ ± 3%  -86.19% (p=0.000 n=10)
ParseStream/prometheus-metrics-labels-formatting-16   24.228µ ± 2%   4.355µ ± 1%  -82.02% (p=0.000 n=10)
ParseStream/prometheus-metrics-formatting-16          23.028µ ± 2%   3.395µ ± 1%  -85.26% (p=0.000 n=10)
geomean                                                20.55µ        3.168µ       -84.59%

                                                    │   old.txt    │                new.txt                 │
                                                    │     B/s      │      B/s       vs base                 │
ParseStream/default-metrics-labels-formatting-16      127.9Mi ± 1%    918.3Mi ± 3%  +617.82% (p=0.000 n=10)
ParseStream/prometheus-metrics-labels-formatting-16   82.19Mi ± 2%   453.32Mi ± 1%  +451.57% (p=0.000 n=10)
ParseStream/prometheus-metrics-formatting-16          86.47Mi ± 2%   581.56Mi ± 1%  +572.52% (p=0.000 n=10)
geomean                                               96.88Mi         623.3Mi       +543.34%

                                                    │   old.txt    │                 new.txt                  │
                                                    │     B/op     │    B/op      vs base                     │
ParseStream/default-metrics-labels-formatting-16      12.53Ki ± 0%   0.00Ki ± 0%  -100.00% (p=0.000 n=10)
ParseStream/prometheus-metrics-labels-formatting-16   21.15Ki ± 1%   0.00Ki ±  ?  -100.00% (p=0.000 n=10)
ParseStream/prometheus-metrics-formatting-16          20.74Ki ± 1%   0.00Ki ±  ?  -100.00% (p=0.000 n=10)
geomean                                               17.65Ki                     ?                       ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean

                                                    │  old.txt   │                new.txt                 │
                                                    │ allocs/op  │ allocs/op  vs base                     │
ParseStream/default-metrics-labels-formatting-16      426.0 ± 0%    0.0 ± 0%  -100.00% (p=0.000 n=10)
ParseStream/prometheus-metrics-labels-formatting-16   514.0 ± 0%    0.0 ± 0%  -100.00% (p=0.000 n=10)
ParseStream/prometheus-metrics-formatting-16          514.0 ± 0%    0.0 ± 0%  -100.00% (p=0.000 n=10)
geomean                                               482.8                   ?                       ¹ ²
2025-12-10 16:11:59 +01:00

198 lines
4.5 KiB
Go

package pb
import (
"fmt"
"math"
"github.com/VictoriaMetrics/easyproto"
"github.com/valyala/fastjson"
)
// decodeArrayValueToJSON decodes a protobuf ArrayValue message into a JSON array represented by fastjson.Value.
func decodeArrayValueToJSON(src []byte, a *fastjson.Arena, fb *fmtBuffer) (*fastjson.Value, error) {
// message ArrayValue {
// repeated AnyValue values = 1;
// }
dst := a.NewArray()
var fc easyproto.FieldContext
i := 0
for len(src) > 0 {
var err error
src, err = fc.NextField(src)
if err != nil {
return nil, fmt.Errorf("cannot read the next field: %w", err)
}
switch fc.FieldNum {
case 1:
data, ok := fc.MessageData()
if !ok {
return nil, fmt.Errorf("cannot read Value data")
}
v, err := decodeAnyValueToJSON(data, a, fb)
if err != nil {
return nil, fmt.Errorf("cannot decode AnyValue: %w", err)
}
dst.SetArrayItem(i, v)
i++
}
}
return dst, nil
}
func decodeAnyValueToJSON(src []byte, a *fastjson.Arena, fb *fmtBuffer) (*fastjson.Value, error) {
// message AnyValue {
// oneof value {
// string string_value = 1;
// bool bool_value = 2;
// int64 int_value = 3;
// double double_value = 4;
// ArrayValue array_value = 5;
// KeyValueList kvlist_value = 6;
// bytes bytes_value = 7;
// }
// }
var fc easyproto.FieldContext
for len(src) > 0 {
var err error
src, err = fc.NextField(src)
if err != nil {
return nil, fmt.Errorf("cannot read the next field: %w", err)
}
switch fc.FieldNum {
case 1:
stringValue, ok := fc.String()
if !ok {
return nil, fmt.Errorf("cannot read StringValue")
}
return a.NewString(stringValue), nil
case 2:
boolValue, ok := fc.Bool()
if !ok {
return nil, fmt.Errorf("cannot read BoolValue")
}
if boolValue {
return a.NewTrue(), nil
} else {
return a.NewFalse(), nil
}
case 3:
intValue, ok := fc.Int64()
if !ok {
return nil, fmt.Errorf("cannot read IntValue")
}
if intValue >= math.MinInt && intValue <= math.MaxInt {
return a.NewNumberInt(int(intValue)), nil
}
return a.NewNumberFloat64(float64(intValue)), nil
case 4:
doubleValue, ok := fc.Double()
if !ok {
return nil, fmt.Errorf("cannot read DoubleValue")
}
return a.NewNumberFloat64(doubleValue), nil
case 5:
data, ok := fc.MessageData()
if !ok {
return nil, fmt.Errorf("cannot read ArrayValue")
}
arr, err := decodeArrayValueToJSON(data, a, fb)
if err != nil {
return nil, fmt.Errorf("cannot decode ArrayValue: %w", err)
}
return arr, nil
case 6:
data, ok := fc.MessageData()
if !ok {
return nil, fmt.Errorf("cannot read KeyValueList")
}
obj, err := decodeKeyValueListToJSON(data, a, fb)
if err != nil {
return nil, fmt.Errorf("cannot decode KeyValueList: %w", err)
}
return obj, nil
case 7:
bytesValue, ok := fc.Bytes()
if !ok {
return nil, fmt.Errorf("cannot read BytesValue")
}
v := fb.formatBase64(bytesValue)
return a.NewString(v), nil
}
}
return a.NewNull(), nil
}
func decodeKeyValueListToJSON(src []byte, a *fastjson.Arena, fb *fmtBuffer) (*fastjson.Value, error) {
// message KeyValueList {
// repeated KeyValue values = 1;
// }
dst := a.NewObject()
var fc easyproto.FieldContext
for len(src) > 0 {
var err error
src, err = fc.NextField(src)
if err != nil {
return nil, fmt.Errorf("cannot read the next field: %w", err)
}
switch fc.FieldNum {
case 1:
data, ok := fc.MessageData()
if !ok {
return nil, fmt.Errorf("cannot read Value data")
}
if err := decodeKeyValueToJSON(data, dst, a, fb); err != nil {
return nil, fmt.Errorf("cannot decode KeyValue: %w", err)
}
}
}
return dst, nil
}
func decodeKeyValueToJSON(src []byte, dst *fastjson.Value, a *fastjson.Arena, fb *fmtBuffer) error {
// message KeyValue {
// string key = 1;
// AnyValue value = 2;
// }
// Decode key
fieldName, ok, err := easyproto.GetString(src, 1)
if err != nil {
return fmt.Errorf("cannot find Key in KeyValue: %w", err)
}
if !ok {
// Key is missing, skip it.
// See https://github.com/VictoriaMetrics/VictoriaLogs/issues/869#issuecomment-3631307996
return nil
}
// Decode value
valueData, ok, err := easyproto.GetMessageData(src, 2)
if err != nil {
return fmt.Errorf("cannot find Value in KeyValue: %w", err)
}
if !ok {
// Value is null, skip it.
return nil
}
v, err := decodeAnyValueToJSON(valueData, a, fb)
if err != nil {
return fmt.Errorf("cannot decode AnyValue: %w", err)
}
dst.Set(fieldName, v)
return nil
}
var jsonArenaPool fastjson.ArenaPool