Skip to content

Commit 41f0330

Browse files
authored
Amortize measurement option allocations (#7215)
This is not a performance critical exporter, but it is acting as the foundation for all other self-observability work. We want to ensure performance is considered for all this other work. - Do not allocate the add and record measurement option slices. - Do not allocate the `metric.attrOpt` to head when on default path (i.e. `WithAttributeSet` or `WithAttributes`) There are three additional allocations in the self-observability. It appears these are added in the error scenario where we need to dynamically build the attributes that are being recorded. ### Benchmarks ```terminal $ benchstat main-49be00144.txt amortize-opts-stdouttrace.txt goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/exporters/stdout/stdouttrace cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz │ main-49be00144.txt │ amortize-opts-stdouttrace.txt │ │ sec/op │ sec/op vs base │ ExporterExportSpans/SelfObservability-8 26.89µ ± 3% 27.40µ ± 3% ~ (p=0.143 n=10) ExporterExportSpans/NoObservability-8 25.99µ ± 3% 27.22µ ± 2% +4.76% (p=0.004 n=10) │ main-49be00144.txt │ amortize-opts-stdouttrace.txt │ │ B/op │ B/op vs base │ ExporterExportSpans/SelfObservability-8 5.459Ki ± 0% 4.081Ki ± 0% -25.24% (p=0.000 n=10) ExporterExportSpans/NoObservability-8 3.873Ki ± 0% 3.873Ki ± 0% ~ (p=1.000 n=10) │ main-49be00144.txt │ amortize-opts-stdouttrace.txt │ │ allocs/op │ allocs/op vs base │ ExporterExportSpans/SelfObservability-8 80.00 ± 0% 67.00 ± 0% -16.25% (p=0.000 n=10) ExporterExportSpans/NoObservability-8 65.00 ± 0% 65.00 ± 0% ~ (p=1.000 n=10) ¹ ¹ all samples are equal ``` ## Follow-up - Investigate if the `semconv` helper packages can be updated to accept some of this complexity (e.g. add a `AddSet` method that accepts an `attribute.Set` instead of just `...attribute.KeyValue`). ## Alternatives A cleaner version is found in #7226. That relies on an external [`bind`](https://github.com/MrAlias/bind) package and the clean up mentioned above.
1 parent c8b89e9 commit 41f0330

File tree

3 files changed

+88
-27
lines changed

3 files changed

+88
-27
lines changed

.codespellignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ ans
77
nam
88
valu
99
thirdparty
10+
addOpt

exporters/stdout/stdouttrace/trace.go

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ func New(options ...Option) (*Exporter, error) {
5555
semconv.OTelComponentName(fmt.Sprintf("%s/%d", otelComponentType, counter.NextExporterID())),
5656
semconv.OTelComponentTypeKey.String(otelComponentType),
5757
}
58+
s := attribute.NewSet(exporter.selfObservabilityAttrs...)
59+
exporter.selfObservabilitySetOpt = metric.WithAttributeSet(s)
5860

5961
mp := otel.GetMeterProvider()
6062
m := mp.Meter(
@@ -91,39 +93,66 @@ type Exporter struct {
9193

9294
selfObservabilityEnabled bool
9395
selfObservabilityAttrs []attribute.KeyValue // selfObservability common attributes
96+
selfObservabilitySetOpt metric.MeasurementOption
9497
spanInflightMetric otelconv.SDKExporterSpanInflight
9598
spanExportedMetric otelconv.SDKExporterSpanExported
9699
operationDurationMetric otelconv.SDKExporterOperationDuration
97100
}
98101

99-
var measureAttrsPool = sync.Pool{
100-
New: func() any {
101-
// "component.name" + "component.type" + "error.type"
102-
const n = 1 + 1 + 1
103-
s := make([]attribute.KeyValue, 0, n)
104-
// Return a pointer to a slice instead of a slice itself
105-
// to avoid allocations on every call.
106-
return &s
107-
},
108-
}
102+
var (
103+
measureAttrsPool = sync.Pool{
104+
New: func() any {
105+
// "component.name" + "component.type" + "error.type"
106+
const n = 1 + 1 + 1
107+
s := make([]attribute.KeyValue, 0, n)
108+
// Return a pointer to a slice instead of a slice itself
109+
// to avoid allocations on every call.
110+
return &s
111+
},
112+
}
113+
114+
addOptPool = &sync.Pool{
115+
New: func() any {
116+
const n = 1 // WithAttributeSet
117+
o := make([]metric.AddOption, 0, n)
118+
return &o
119+
},
120+
}
121+
122+
recordOptPool = &sync.Pool{
123+
New: func() any {
124+
const n = 1 // WithAttributeSet
125+
o := make([]metric.RecordOption, 0, n)
126+
return &o
127+
},
128+
}
129+
)
109130

110131
// ExportSpans writes spans in json format to stdout.
111132
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) (err error) {
112133
var success int64
113134
if e.selfObservabilityEnabled {
114135
count := int64(len(spans))
115136

116-
e.spanInflightMetric.Add(ctx, count, e.selfObservabilityAttrs...)
137+
addOpt := addOptPool.Get().(*[]metric.AddOption)
138+
defer func() {
139+
*addOpt = (*addOpt)[:0]
140+
addOptPool.Put(addOpt)
141+
}()
142+
143+
*addOpt = append(*addOpt, e.selfObservabilitySetOpt)
144+
145+
e.spanInflightMetric.Inst().Add(ctx, count, *addOpt...)
117146
defer func(starting time.Time) {
118-
e.spanInflightMetric.Add(ctx, -count, e.selfObservabilityAttrs...)
147+
e.spanInflightMetric.Inst().Add(ctx, -count, *addOpt...)
119148

120149
// Record the success and duration of the operation.
121150
//
122151
// Do not exclude 0 values, as they are valid and indicate no spans
123152
// were exported which is meaningful for certain aggregations.
124-
e.spanExportedMetric.Add(ctx, success, e.selfObservabilityAttrs...)
153+
e.spanExportedMetric.Inst().Add(ctx, success, *addOpt...)
125154

126-
attr := e.selfObservabilityAttrs
155+
mOpt := e.selfObservabilitySetOpt
127156
if err != nil {
128157
// additional attributes for self-observability,
129158
// only spanExportedMetric and operationDurationMetric are supported.
@@ -134,12 +163,34 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
134163
}()
135164
*attrs = append(*attrs, e.selfObservabilityAttrs...)
136165
*attrs = append(*attrs, semconv.ErrorType(err))
137-
attr = *attrs
138166

139-
e.spanExportedMetric.Add(ctx, count-success, attr...)
167+
// Do not inefficiently make a copy of attrs by using
168+
// WithAttributes instead of WithAttributeSet.
169+
set := attribute.NewSet(*attrs...)
170+
mOpt = metric.WithAttributeSet(set)
171+
172+
// Reset addOpt with new attribute set.
173+
*addOpt = append((*addOpt)[:0], mOpt)
174+
175+
e.spanExportedMetric.Inst().Add(
176+
ctx,
177+
count-success,
178+
*addOpt...,
179+
)
140180
}
141181

142-
e.operationDurationMetric.Record(ctx, time.Since(starting).Seconds(), attr...)
182+
recordOpt := recordOptPool.Get().(*[]metric.RecordOption)
183+
defer func() {
184+
*recordOpt = (*recordOpt)[:0]
185+
recordOptPool.Put(recordOpt)
186+
}()
187+
188+
*recordOpt = append(*recordOpt, mOpt)
189+
e.operationDurationMetric.Inst().Record(
190+
ctx,
191+
time.Since(starting).Seconds(),
192+
*recordOpt...,
193+
)
143194
}(time.Now())
144195
}
145196

exporters/stdout/stdouttrace/trace_test.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,6 @@ func TestSelfObservabilityInstrumentErrors(t *testing.T) {
669669
}
670670

671671
func BenchmarkExporterExportSpans(b *testing.B) {
672-
b.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "true")
673672
ss := tracetest.SpanStubs{
674673
{Name: "/foo"},
675674
{
@@ -678,15 +677,25 @@ func BenchmarkExporterExportSpans(b *testing.B) {
678677
},
679678
{Name: "/bar"},
680679
}.Snapshots()
681-
ex, err := stdouttrace.New(stdouttrace.WithWriter(io.Discard))
682-
if err != nil {
683-
b.Fatalf("failed to create exporter: %v", err)
684-
}
685680

686-
b.ReportAllocs()
687-
b.ResetTimer()
688-
for i := 0; i < b.N; i++ {
689-
err = ex.ExportSpans(context.Background(), ss)
681+
run := func(b *testing.B) {
682+
ex, err := stdouttrace.New(stdouttrace.WithWriter(io.Discard))
683+
if err != nil {
684+
b.Fatalf("failed to create exporter: %v", err)
685+
}
686+
687+
b.ReportAllocs()
688+
b.ResetTimer()
689+
for i := 0; i < b.N; i++ {
690+
err = ex.ExportSpans(context.Background(), ss)
691+
}
692+
_ = err
690693
}
691-
_ = err
694+
695+
b.Run("SelfObservability", func(b *testing.B) {
696+
b.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "true")
697+
run(b)
698+
})
699+
700+
b.Run("NoObservability", run)
692701
}

0 commit comments

Comments
 (0)