1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-11-23 22:34:47 +02:00

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
https://github.com/open-telemetry/opentelemetry-go/pull/7226. That
relies on an external [`bind`](https://github.com/MrAlias/bind) package
and the clean up mentioned above.
This commit is contained in:
Tyler Yahn
2025-08-26 09:10:29 -07:00
committed by GitHub
parent c8b89e9780
commit 41f03302cd
3 changed files with 88 additions and 27 deletions

View File

@@ -7,3 +7,4 @@ ans
nam
valu
thirdparty
addOpt

View File

@@ -55,6 +55,8 @@ func New(options ...Option) (*Exporter, error) {
semconv.OTelComponentName(fmt.Sprintf("%s/%d", otelComponentType, counter.NextExporterID())),
semconv.OTelComponentTypeKey.String(otelComponentType),
}
s := attribute.NewSet(exporter.selfObservabilityAttrs...)
exporter.selfObservabilitySetOpt = metric.WithAttributeSet(s)
mp := otel.GetMeterProvider()
m := mp.Meter(
@@ -91,21 +93,40 @@ type Exporter struct {
selfObservabilityEnabled bool
selfObservabilityAttrs []attribute.KeyValue // selfObservability common attributes
selfObservabilitySetOpt metric.MeasurementOption
spanInflightMetric otelconv.SDKExporterSpanInflight
spanExportedMetric otelconv.SDKExporterSpanExported
operationDurationMetric otelconv.SDKExporterOperationDuration
}
var measureAttrsPool = sync.Pool{
New: func() any {
// "component.name" + "component.type" + "error.type"
const n = 1 + 1 + 1
s := make([]attribute.KeyValue, 0, n)
// Return a pointer to a slice instead of a slice itself
// to avoid allocations on every call.
return &s
},
}
var (
measureAttrsPool = sync.Pool{
New: func() any {
// "component.name" + "component.type" + "error.type"
const n = 1 + 1 + 1
s := make([]attribute.KeyValue, 0, n)
// Return a pointer to a slice instead of a slice itself
// to avoid allocations on every call.
return &s
},
}
addOptPool = &sync.Pool{
New: func() any {
const n = 1 // WithAttributeSet
o := make([]metric.AddOption, 0, n)
return &o
},
}
recordOptPool = &sync.Pool{
New: func() any {
const n = 1 // WithAttributeSet
o := make([]metric.RecordOption, 0, n)
return &o
},
}
)
// ExportSpans writes spans in json format to stdout.
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) (err error) {
@@ -113,17 +134,25 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
if e.selfObservabilityEnabled {
count := int64(len(spans))
e.spanInflightMetric.Add(ctx, count, e.selfObservabilityAttrs...)
addOpt := addOptPool.Get().(*[]metric.AddOption)
defer func() {
*addOpt = (*addOpt)[:0]
addOptPool.Put(addOpt)
}()
*addOpt = append(*addOpt, e.selfObservabilitySetOpt)
e.spanInflightMetric.Inst().Add(ctx, count, *addOpt...)
defer func(starting time.Time) {
e.spanInflightMetric.Add(ctx, -count, e.selfObservabilityAttrs...)
e.spanInflightMetric.Inst().Add(ctx, -count, *addOpt...)
// Record the success and duration of the operation.
//
// Do not exclude 0 values, as they are valid and indicate no spans
// were exported which is meaningful for certain aggregations.
e.spanExportedMetric.Add(ctx, success, e.selfObservabilityAttrs...)
e.spanExportedMetric.Inst().Add(ctx, success, *addOpt...)
attr := e.selfObservabilityAttrs
mOpt := e.selfObservabilitySetOpt
if err != nil {
// additional attributes for self-observability,
// only spanExportedMetric and operationDurationMetric are supported.
@@ -134,12 +163,34 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
}()
*attrs = append(*attrs, e.selfObservabilityAttrs...)
*attrs = append(*attrs, semconv.ErrorType(err))
attr = *attrs
e.spanExportedMetric.Add(ctx, count-success, attr...)
// Do not inefficiently make a copy of attrs by using
// WithAttributes instead of WithAttributeSet.
set := attribute.NewSet(*attrs...)
mOpt = metric.WithAttributeSet(set)
// Reset addOpt with new attribute set.
*addOpt = append((*addOpt)[:0], mOpt)
e.spanExportedMetric.Inst().Add(
ctx,
count-success,
*addOpt...,
)
}
e.operationDurationMetric.Record(ctx, time.Since(starting).Seconds(), attr...)
recordOpt := recordOptPool.Get().(*[]metric.RecordOption)
defer func() {
*recordOpt = (*recordOpt)[:0]
recordOptPool.Put(recordOpt)
}()
*recordOpt = append(*recordOpt, mOpt)
e.operationDurationMetric.Inst().Record(
ctx,
time.Since(starting).Seconds(),
*recordOpt...,
)
}(time.Now())
}

View File

@@ -669,7 +669,6 @@ func TestSelfObservabilityInstrumentErrors(t *testing.T) {
}
func BenchmarkExporterExportSpans(b *testing.B) {
b.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "true")
ss := tracetest.SpanStubs{
{Name: "/foo"},
{
@@ -678,15 +677,25 @@ func BenchmarkExporterExportSpans(b *testing.B) {
},
{Name: "/bar"},
}.Snapshots()
ex, err := stdouttrace.New(stdouttrace.WithWriter(io.Discard))
if err != nil {
b.Fatalf("failed to create exporter: %v", err)
run := func(b *testing.B) {
ex, err := stdouttrace.New(stdouttrace.WithWriter(io.Discard))
if err != nil {
b.Fatalf("failed to create exporter: %v", err)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err = ex.ExportSpans(context.Background(), ss)
}
_ = err
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err = ex.ExportSpans(context.Background(), ss)
}
_ = err
b.Run("SelfObservability", func(b *testing.B) {
b.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "true")
run(b)
})
b.Run("NoObservability", run)
}