1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2026-06-03 18:35:08 +02:00

Optimize the return type of ExportSpans (#7405)

Do not allocate a return function from `ExportSpans` to the heap. Use
the added `ExportOp` type instead.

### Benchmarks

#### `stdouttrace`

```terminal
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/exporters/stdout/stdouttrace
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
                                      │ main.bmark.results │ stdouttrace-optimize-end.bmark.results │
                                      │       sec/op       │     sec/op       vs base               │
ExporterExportSpans/Observability-8            23.37µ ± 2%       22.79µ ± 3%  -2.50% (p=0.025 n=10)
ExporterExportSpans/NoObservability-8          23.07µ ± 7%       22.29µ ± 1%  -3.38% (p=0.000 n=10)
geomean                                        23.22µ            22.54µ       -2.94%

                                      │ main.bmark.results │ stdouttrace-optimize-end.bmark.results │
                                      │        B/op        │      B/op        vs base               │
ExporterExportSpans/Observability-8           4.253Ki ± 0%      4.190Ki ± 0%  -1.47% (p=0.000 n=10)
ExporterExportSpans/NoObservability-8         3.975Ki ± 0%      3.975Ki ± 0%       ~ (p=0.474 n=10)
geomean                                       4.111Ki           4.081Ki       -0.74%

                                      │ main.bmark.results │ stdouttrace-optimize-end.bmark.results │
                                      │     allocs/op      │   allocs/op    vs base                 │
ExporterExportSpans/Observability-8             67.00 ± 0%      66.00 ± 0%  -1.49% (p=0.000 n=10)
ExporterExportSpans/NoObservability-8           64.00 ± 0%      64.00 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                         65.48           64.99       -0.75%
¹ all samples are equal
```

#### `stdouttrace/internal/observ`

```terminal

goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/exporters/stdout/stdouttrace/internal/observ
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
                                          │ main.bmark.results │ stdouttrace-optimize-end.bmark.results │
                                          │       sec/op       │     sec/op      vs base                │
InstrumentationExportSpans/NoError-8               197.9n ± 7%      153.3n ± 5%  -22.51% (p=0.000 n=10)
InstrumentationExportSpans/PartialError-8          754.4n ± 6%      663.2n ± 6%  -12.08% (p=0.001 n=10)
InstrumentationExportSpans/FullError-8             772.8n ± 4%      669.2n ± 4%  -13.39% (p=0.000 n=10)
geomean                                            486.8n           408.3n       -16.13%

                                          │ main.bmark.results │ stdouttrace-optimize-end.bmark.results  │
                                          │        B/op        │    B/op     vs base                     │
InstrumentationExportSpans/NoError-8                64.00 ± 0%    0.00 ± 0%  -100.00% (p=0.000 n=10)
InstrumentationExportSpans/PartialError-8           280.0 ± 0%   216.0 ± 0%   -22.86% (p=0.000 n=10)
InstrumentationExportSpans/FullError-8              280.0 ± 0%   216.0 ± 0%   -22.86% (p=0.000 n=10)
geomean                                             171.2                    ?                       ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean

                                          │ main.bmark.results │ stdouttrace-optimize-end.bmark.results  │
                                          │     allocs/op      │ allocs/op   vs base                     │
InstrumentationExportSpans/NoError-8                1.000 ± 0%   0.000 ± 0%  -100.00% (p=0.000 n=10)
InstrumentationExportSpans/PartialError-8           3.000 ± 0%   2.000 ± 0%   -33.33% (p=0.000 n=10)
InstrumentationExportSpans/FullError-8              3.000 ± 0%   2.000 ± 0%   -33.33% (p=0.000 n=10)
geomean                                             2.080                    ?                       ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean
```
This commit is contained in:
Tyler Yahn
2025-09-25 09:07:15 -07:00
committed by GitHub
parent 63ed041382
commit 88d3fedf1a
3 changed files with 84 additions and 68 deletions
@@ -145,16 +145,9 @@ func NewInstrumentation(id int64) (*Instrumentation, error) {
return i, err
}
// ExportSpansDone is a function that is called when a call to an Exporter's
// ExportSpans method completes.
//
// The number of successful exports is provided as success. Any error that is
// encountered is provided as err.
type ExportSpansDone func(success int64, err error)
// ExportSpans instruments the ExportSpans method of the exporter. It returns a
// function that needs to be deferred so it is called when the method returns.
func (i *Instrumentation) ExportSpans(ctx context.Context, nSpans int) ExportSpansDone {
func (i *Instrumentation) ExportSpans(ctx context.Context, nSpans int) ExportOp {
start := time.Now()
addOpt := get[metric.AddOption](addOptPool)
@@ -162,44 +155,60 @@ func (i *Instrumentation) ExportSpans(ctx context.Context, nSpans int) ExportSpa
*addOpt = append(*addOpt, i.setOpt)
i.inflightSpans.Add(ctx, int64(nSpans), *addOpt...)
return i.end(ctx, start, int64(nSpans))
}
func (i *Instrumentation) end(ctx context.Context, start time.Time, n int64) ExportSpansDone {
return func(success int64, err error) {
addOpt := get[metric.AddOption](addOptPool)
defer put(addOptPool, addOpt)
*addOpt = append(*addOpt, i.setOpt)
i.inflightSpans.Add(ctx, -n, *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.
i.exportedSpans.Add(ctx, success, *addOpt...)
mOpt := i.setOpt
if err != nil {
attrs := get[attribute.KeyValue](measureAttrsPool)
defer put(measureAttrsPool, attrs)
*attrs = append(*attrs, i.attrs...)
*attrs = append(*attrs, semconv.ErrorType(err))
// 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)
i.exportedSpans.Add(ctx, n-success, *addOpt...)
}
recordOpt := get[metric.RecordOption](recordOptPool)
defer put(recordOptPool, recordOpt)
*recordOpt = append(*recordOpt, mOpt)
i.opDuration.Record(ctx, time.Since(start).Seconds(), *recordOpt...)
return ExportOp{
ctx: ctx,
start: start,
nSpans: int64(nSpans),
inst: i,
}
}
// ExportOp is an in-progress ExportSpans operation.
type ExportOp struct {
ctx context.Context
start time.Time
nSpans int64
inst *Instrumentation
}
// End ends the ExportSpans operation, recording its success and duration.
//
// The success parameter indicates how many spans were successfully exported.
// The err parameter indicates whether the operation failed. If err is not nil,
// the number of failed spans (nSpans - success) is also recorded.
func (e ExportOp) End(success int64, err error) {
addOpt := get[metric.AddOption](addOptPool)
defer put(addOptPool, addOpt)
*addOpt = append(*addOpt, e.inst.setOpt)
e.inst.inflightSpans.Add(e.ctx, -e.nSpans, *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.inst.exportedSpans.Add(e.ctx, success, *addOpt...)
mOpt := e.inst.setOpt
if err != nil {
attrs := get[attribute.KeyValue](measureAttrsPool)
defer put(measureAttrsPool, attrs)
*attrs = append(*attrs, e.inst.attrs...)
*attrs = append(*attrs, semconv.ErrorType(err))
// 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.inst.exportedSpans.Add(e.ctx, e.nSpans-success, *addOpt...)
}
recordOpt := get[metric.RecordOption](recordOptPool)
defer put(recordOptPool, recordOpt)
*recordOpt = append(*recordOpt, mOpt)
e.inst.opDuration.Record(e.ctx, time.Since(e.start).Seconds(), *recordOpt...)
}
@@ -190,8 +190,7 @@ func TestInstrumentationExportSpans(t *testing.T) {
inst, collect := setup(t)
const n = 10
end := inst.ExportSpans(t.Context(), n)
end(n, nil)
inst.ExportSpans(t.Context(), n).End(n, nil)
assertMetrics(t, collect(), n, n, nil)
}
@@ -200,9 +199,8 @@ func TestInstrumentationExportSpansAllErrored(t *testing.T) {
inst, collect := setup(t)
const n = 10
end := inst.ExportSpans(t.Context(), n)
const success = 0
end(success, assert.AnError)
inst.ExportSpans(t.Context(), n).End(success, assert.AnError)
assertMetrics(t, collect(), n, success, assert.AnError)
}
@@ -211,28 +209,37 @@ func TestInstrumentationExportSpansPartialErrored(t *testing.T) {
inst, collect := setup(t)
const n = 10
end := inst.ExportSpans(t.Context(), n)
const success = 5
end(success, assert.AnError)
inst.ExportSpans(t.Context(), n).End(success, assert.AnError)
assertMetrics(t, collect(), n, success, assert.AnError)
}
func BenchmarkInstrumentationExportSpans(b *testing.B) {
b.Setenv("OTEL_GO_X_OBSERVABILITY", "true")
inst, err := observ.NewInstrumentation(ID)
if err != nil {
b.Fatalf("failed to create instrumentation: %v", err)
setup := func(b *testing.B) *observ.Instrumentation {
b.Helper()
b.Setenv("OTEL_GO_X_OBSERVABILITY", "true")
inst, err := observ.NewInstrumentation(ID)
if err != nil {
b.Fatalf("failed to create instrumentation: %v", err)
}
return inst
}
var end observ.ExportSpansDone
err = errors.New("benchmark error")
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
end = inst.ExportSpans(b.Context(), 10)
end(4, err)
const nSpans = 10
err := errors.New("benchmark error")
run := func(n int64, err error) func(*testing.B) {
return func(b *testing.B) {
inst := setup(b)
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
inst.ExportSpans(b.Context(), nSpans).End(n, err)
}
}
}
_ = end
b.Run("NoError", run(nSpans, nil))
b.Run("PartialError", run(4, err))
b.Run("FullError", run(0, err))
}
+2 -2
View File
@@ -56,8 +56,8 @@ type Exporter struct {
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) (err error) {
var success int64
if e.inst != nil {
end := e.inst.ExportSpans(ctx, len(spans))
defer func() { end(success, err) }()
op := e.inst.ExportSpans(ctx, len(spans))
defer func() { op.End(success, err) }()
}
if err := ctx.Err(); err != nil {