Skip to content

Commit e232882

Browse files
authored
Measure time spent on encoding and the compaction ratio (#871)
Add metrics to measure time spent on encoding CBOR+ZSTD, and the compaction ratio achieved by ZSTD. Fixes #863
1 parent 30fac2e commit e232882

File tree

4 files changed

+127
-11
lines changed

4 files changed

+127
-11
lines changed

internal/encoding/encoding.go

+64-11
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ package encoding
22

33
import (
44
"bytes"
5+
"context"
56
"fmt"
7+
"reflect"
68
"sync"
9+
"time"
710

811
"github.com/klauspost/compress/zstd"
912
cbg "github.com/whyrusleeping/cbor-gen"
13+
"go.opentelemetry.io/otel/attribute"
14+
"go.opentelemetry.io/otel/metric"
1015
)
1116

1217
// maxDecompressedSize is the default maximum amount of memory allocated by the
@@ -37,15 +42,29 @@ func NewCBOR[T CBORMarshalUnmarshaler]() *CBOR[T] {
3742
return &CBOR[T]{}
3843
}
3944

40-
func (c *CBOR[T]) Encode(m T) ([]byte, error) {
45+
func (c *CBOR[T]) Encode(m T) (_ []byte, _err error) {
46+
defer func(start time.Time) {
47+
if _err != nil {
48+
metrics.encodingTime.Record(context.Background(),
49+
time.Since(start).Seconds(),
50+
metric.WithAttributeSet(attrSetCborEncode))
51+
}
52+
}(time.Now())
4153
var out bytes.Buffer
4254
if err := m.MarshalCBOR(&out); err != nil {
4355
return nil, err
4456
}
4557
return out.Bytes(), nil
4658
}
4759

48-
func (c *CBOR[T]) Decode(v []byte, t T) error {
60+
func (c *CBOR[T]) Decode(v []byte, t T) (_err error) {
61+
defer func(start time.Time) {
62+
if _err != nil {
63+
metrics.encodingTime.Record(context.Background(),
64+
time.Since(start).Seconds(),
65+
metric.WithAttributeSet(attrSetCborDecode))
66+
}
67+
}(time.Now())
4968
r := bytes.NewReader(v)
5069
return t.UnmarshalCBOR(r)
5170
}
@@ -54,6 +73,9 @@ type ZSTD[T CBORMarshalUnmarshaler] struct {
5473
cborEncoding *CBOR[T]
5574
compressor *zstd.Encoder
5675
decompressor *zstd.Decoder
76+
77+
metricAttr attribute.KeyValue
78+
metricAttrLoader sync.Once
5779
}
5880

5981
func NewZSTD[T CBORMarshalUnmarshaler]() (*ZSTD[T], error) {
@@ -74,26 +96,57 @@ func NewZSTD[T CBORMarshalUnmarshaler]() (*ZSTD[T], error) {
7496
}, nil
7597
}
7698

77-
func (c *ZSTD[T]) Encode(m T) ([]byte, error) {
78-
cborEncoded, err := c.cborEncoding.Encode(m)
79-
if len(cborEncoded) > maxDecompressedSize {
99+
func (c *ZSTD[T]) Encode(t T) (_ []byte, _err error) {
100+
defer func(start time.Time) {
101+
metrics.encodingTime.Record(context.Background(),
102+
time.Since(start).Seconds(),
103+
metric.WithAttributeSet(attrSetZstdEncode))
104+
}(time.Now())
105+
decompressed, err := c.cborEncoding.Encode(t)
106+
if len(decompressed) > maxDecompressedSize {
80107
// Error out early if the encoded value is too large to be decompressed.
81-
return nil, fmt.Errorf("encoded value cannot exceed maximum size: %d > %d", len(cborEncoded), maxDecompressedSize)
108+
return nil, fmt.Errorf("encoded value cannot exceed maximum size: %d > %d", len(decompressed), maxDecompressedSize)
82109
}
83110
if err != nil {
84111
return nil, err
85112
}
86-
compressed := c.compressor.EncodeAll(cborEncoded, make([]byte, 0, len(cborEncoded)))
113+
compressed := c.compressor.EncodeAll(decompressed, make([]byte, 0, len(decompressed)))
114+
c.meterCompressionRatio(len(decompressed), len(compressed))
87115
return compressed, nil
88116
}
89117

90-
func (c *ZSTD[T]) Decode(v []byte, t T) error {
118+
func (c *ZSTD[T]) Decode(compressed []byte, t T) (_err error) {
119+
defer func(start time.Time) {
120+
if _err != nil {
121+
metrics.encodingTime.Record(context.Background(),
122+
time.Since(start).Seconds(),
123+
metric.WithAttributeSet(attrSetZstdDecode))
124+
}
125+
}(time.Now())
91126
buf := bufferPool.Get().(*[]byte)
92127
defer bufferPool.Put(buf)
93-
94-
cborEncoded, err := c.decompressor.DecodeAll(v, (*buf)[:0])
128+
decompressed, err := c.decompressor.DecodeAll(compressed, (*buf)[:0])
95129
if err != nil {
96130
return err
97131
}
98-
return c.cborEncoding.Decode(cborEncoded, t)
132+
c.meterCompressionRatio(len(decompressed), len(compressed))
133+
return c.cborEncoding.Decode(decompressed, t)
134+
}
135+
136+
func (c *ZSTD[T]) meterCompressionRatio(decompressedSize, compressedSize int) {
137+
compressionRatio := float64(decompressedSize) / float64(compressedSize)
138+
metrics.zstdCompressionRatio.Record(context.Background(), compressionRatio, metric.WithAttributes(c.getMetricAttribute()))
139+
}
140+
141+
func (c *ZSTD[T]) getMetricAttribute() attribute.KeyValue {
142+
c.metricAttrLoader.Do(func() {
143+
const key = "type"
144+
switch target := reflect.TypeFor[T](); {
145+
case target.Kind() == reflect.Ptr:
146+
c.metricAttr = attribute.String(key, target.Elem().Name())
147+
default:
148+
c.metricAttr = attribute.String(key, target.Name())
149+
}
150+
})
151+
return c.metricAttr
99152
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package encoding
2+
3+
import "go.opentelemetry.io/otel/attribute"
4+
5+
// GetMetricAttribute returns the attribute for metric collection, exported for
6+
// testing purposes.
7+
func (c *ZSTD[T]) GetMetricAttribute() attribute.KeyValue { return c.getMetricAttribute() }

internal/encoding/encoding_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/klauspost/compress/zstd"
1010
"github.com/stretchr/testify/require"
1111
cbg "github.com/whyrusleeping/cbor-gen"
12+
"go.opentelemetry.io/otel/attribute"
1213
)
1314

1415
var (
@@ -77,3 +78,20 @@ func TestZSTDLimits(t *testing.T) {
7778
var dest testValue
7879
require.ErrorContains(t, subject.Decode(tooLargeACompression, &dest), "decompressed size exceeds configured limit")
7980
}
81+
82+
func TestZSTD_GetMetricAttribute(t *testing.T) {
83+
t.Run("By Pointer", func(t *testing.T) {
84+
subject, err := encoding.NewZSTD[*testValue]()
85+
require.NoError(t, err)
86+
require.Equal(t, attribute.String("type", "testValue"), subject.GetMetricAttribute())
87+
})
88+
t.Run("By Value", func(t *testing.T) {
89+
type anotherTestValue struct {
90+
cbg.CBORUnmarshaler
91+
cbg.CBORMarshaler
92+
}
93+
subject, err := encoding.NewZSTD[anotherTestValue]()
94+
require.NoError(t, err)
95+
require.Equal(t, attribute.String("type", "anotherTestValue"), subject.GetMetricAttribute())
96+
})
97+
}

internal/encoding/metrics.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package encoding
2+
3+
import (
4+
"github.com/filecoin-project/go-f3/internal/measurements"
5+
"go.opentelemetry.io/otel"
6+
"go.opentelemetry.io/otel/attribute"
7+
"go.opentelemetry.io/otel/metric"
8+
)
9+
10+
var (
11+
attrCodecCbor = attribute.String("codec", "cbor")
12+
attrCodecZstd = attribute.String("codec", "zstd")
13+
attrActionEncode = attribute.String("action", "encode")
14+
attrActionDecode = attribute.String("action", "decode")
15+
attrSetCborEncode = attribute.NewSet(attrCodecCbor, attrActionEncode)
16+
attrSetCborDecode = attribute.NewSet(attrCodecCbor, attrActionDecode)
17+
attrSetZstdEncode = attribute.NewSet(attrCodecZstd, attrActionEncode)
18+
attrSetZstdDecode = attribute.NewSet(attrCodecZstd, attrActionDecode)
19+
20+
meter = otel.Meter("f3/internal/encoding")
21+
22+
metrics = struct {
23+
encodingTime metric.Float64Histogram
24+
zstdCompressionRatio metric.Float64Histogram
25+
}{
26+
encodingTime: measurements.Must(meter.Float64Histogram(
27+
"f3_internal_encoding_time",
28+
metric.WithDescription("The time spent on encoding/decoding in seconds."),
29+
metric.WithUnit("s"),
30+
metric.WithExplicitBucketBoundaries(0.001, 0.003, 0.005, 0.01, 0.03, 0.05, 0.1, 0.3, 0.5, 1.0, 2.0, 5.0, 10.0),
31+
)),
32+
zstdCompressionRatio: measurements.Must(meter.Float64Histogram(
33+
"f3_internal_encoding_zstd_compression_ratio",
34+
metric.WithDescription("The ratio of compressed to uncompressed data size for zstd encoding."),
35+
metric.WithExplicitBucketBoundaries(0.0, 0.1, 0.2, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 10.0),
36+
)),
37+
}
38+
)

0 commit comments

Comments
 (0)