Skip to content

Commit 88d3fed

Browse files
authored
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 ```
1 parent 63ed041 commit 88d3fed

File tree

3 files changed

+82
-66
lines changed

3 files changed

+82
-66
lines changed

exporters/stdout/stdouttrace/internal/observ/instrumentation.go

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -145,61 +145,70 @@ func NewInstrumentation(id int64) (*Instrumentation, error) {
145145
return i, err
146146
}
147147

148-
// ExportSpansDone is a function that is called when a call to an Exporter's
149-
// ExportSpans method completes.
150-
//
151-
// The number of successful exports is provided as success. Any error that is
152-
// encountered is provided as err.
153-
type ExportSpansDone func(success int64, err error)
154-
155148
// ExportSpans instruments the ExportSpans method of the exporter. It returns a
156149
// function that needs to be deferred so it is called when the method returns.
157-
func (i *Instrumentation) ExportSpans(ctx context.Context, nSpans int) ExportSpansDone {
150+
func (i *Instrumentation) ExportSpans(ctx context.Context, nSpans int) ExportOp {
158151
start := time.Now()
159152

160153
addOpt := get[metric.AddOption](addOptPool)
161154
defer put(addOptPool, addOpt)
162155
*addOpt = append(*addOpt, i.setOpt)
163156
i.inflightSpans.Add(ctx, int64(nSpans), *addOpt...)
164157

165-
return i.end(ctx, start, int64(nSpans))
158+
return ExportOp{
159+
ctx: ctx,
160+
start: start,
161+
nSpans: int64(nSpans),
162+
inst: i,
163+
}
164+
}
165+
166+
// ExportOp is an in-progress ExportSpans operation.
167+
type ExportOp struct {
168+
ctx context.Context
169+
start time.Time
170+
nSpans int64
171+
inst *Instrumentation
166172
}
167173

168-
func (i *Instrumentation) end(ctx context.Context, start time.Time, n int64) ExportSpansDone {
169-
return func(success int64, err error) {
170-
addOpt := get[metric.AddOption](addOptPool)
171-
defer put(addOptPool, addOpt)
172-
*addOpt = append(*addOpt, i.setOpt)
173-
174-
i.inflightSpans.Add(ctx, -n, *addOpt...)
175-
176-
// Record the success and duration of the operation.
177-
//
178-
// Do not exclude 0 values, as they are valid and indicate no spans
179-
// were exported which is meaningful for certain aggregations.
180-
i.exportedSpans.Add(ctx, success, *addOpt...)
181-
182-
mOpt := i.setOpt
183-
if err != nil {
184-
attrs := get[attribute.KeyValue](measureAttrsPool)
185-
defer put(measureAttrsPool, attrs)
186-
*attrs = append(*attrs, i.attrs...)
187-
*attrs = append(*attrs, semconv.ErrorType(err))
188-
189-
// Do not inefficiently make a copy of attrs by using
190-
// WithAttributes instead of WithAttributeSet.
191-
set := attribute.NewSet(*attrs...)
192-
mOpt = metric.WithAttributeSet(set)
193-
194-
// Reset addOpt with new attribute set.
195-
*addOpt = append((*addOpt)[:0], mOpt)
196-
197-
i.exportedSpans.Add(ctx, n-success, *addOpt...)
198-
}
199-
200-
recordOpt := get[metric.RecordOption](recordOptPool)
201-
defer put(recordOptPool, recordOpt)
202-
*recordOpt = append(*recordOpt, mOpt)
203-
i.opDuration.Record(ctx, time.Since(start).Seconds(), *recordOpt...)
174+
// End ends the ExportSpans operation, recording its success and duration.
175+
//
176+
// The success parameter indicates how many spans were successfully exported.
177+
// The err parameter indicates whether the operation failed. If err is not nil,
178+
// the number of failed spans (nSpans - success) is also recorded.
179+
func (e ExportOp) End(success int64, err error) {
180+
addOpt := get[metric.AddOption](addOptPool)
181+
defer put(addOptPool, addOpt)
182+
*addOpt = append(*addOpt, e.inst.setOpt)
183+
184+
e.inst.inflightSpans.Add(e.ctx, -e.nSpans, *addOpt...)
185+
186+
// Record the success and duration of the operation.
187+
//
188+
// Do not exclude 0 values, as they are valid and indicate no spans
189+
// were exported which is meaningful for certain aggregations.
190+
e.inst.exportedSpans.Add(e.ctx, success, *addOpt...)
191+
192+
mOpt := e.inst.setOpt
193+
if err != nil {
194+
attrs := get[attribute.KeyValue](measureAttrsPool)
195+
defer put(measureAttrsPool, attrs)
196+
*attrs = append(*attrs, e.inst.attrs...)
197+
*attrs = append(*attrs, semconv.ErrorType(err))
198+
199+
// Do not inefficiently make a copy of attrs by using
200+
// WithAttributes instead of WithAttributeSet.
201+
set := attribute.NewSet(*attrs...)
202+
mOpt = metric.WithAttributeSet(set)
203+
204+
// Reset addOpt with new attribute set.
205+
*addOpt = append((*addOpt)[:0], mOpt)
206+
207+
e.inst.exportedSpans.Add(e.ctx, e.nSpans-success, *addOpt...)
204208
}
209+
210+
recordOpt := get[metric.RecordOption](recordOptPool)
211+
defer put(recordOptPool, recordOpt)
212+
*recordOpt = append(*recordOpt, mOpt)
213+
e.inst.opDuration.Record(e.ctx, time.Since(e.start).Seconds(), *recordOpt...)
205214
}

exporters/stdout/stdouttrace/internal/observ/instrumentation_test.go

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,7 @@ func TestInstrumentationExportSpans(t *testing.T) {
190190
inst, collect := setup(t)
191191

192192
const n = 10
193-
end := inst.ExportSpans(t.Context(), n)
194-
end(n, nil)
193+
inst.ExportSpans(t.Context(), n).End(n, nil)
195194

196195
assertMetrics(t, collect(), n, n, nil)
197196
}
@@ -200,9 +199,8 @@ func TestInstrumentationExportSpansAllErrored(t *testing.T) {
200199
inst, collect := setup(t)
201200

202201
const n = 10
203-
end := inst.ExportSpans(t.Context(), n)
204202
const success = 0
205-
end(success, assert.AnError)
203+
inst.ExportSpans(t.Context(), n).End(success, assert.AnError)
206204

207205
assertMetrics(t, collect(), n, success, assert.AnError)
208206
}
@@ -211,28 +209,37 @@ func TestInstrumentationExportSpansPartialErrored(t *testing.T) {
211209
inst, collect := setup(t)
212210

213211
const n = 10
214-
end := inst.ExportSpans(t.Context(), n)
215212
const success = 5
216-
end(success, assert.AnError)
213+
inst.ExportSpans(t.Context(), n).End(success, assert.AnError)
217214

218215
assertMetrics(t, collect(), n, success, assert.AnError)
219216
}
220217

221218
func BenchmarkInstrumentationExportSpans(b *testing.B) {
222-
b.Setenv("OTEL_GO_X_OBSERVABILITY", "true")
223-
inst, err := observ.NewInstrumentation(ID)
224-
if err != nil {
225-
b.Fatalf("failed to create instrumentation: %v", err)
219+
setup := func(b *testing.B) *observ.Instrumentation {
220+
b.Helper()
221+
b.Setenv("OTEL_GO_X_OBSERVABILITY", "true")
222+
inst, err := observ.NewInstrumentation(ID)
223+
if err != nil {
224+
b.Fatalf("failed to create instrumentation: %v", err)
225+
}
226+
return inst
226227
}
227228

228-
var end observ.ExportSpansDone
229-
err = errors.New("benchmark error")
230-
231-
b.ReportAllocs()
232-
b.ResetTimer()
233-
for b.Loop() {
234-
end = inst.ExportSpans(b.Context(), 10)
235-
end(4, err)
229+
const nSpans = 10
230+
err := errors.New("benchmark error")
231+
run := func(n int64, err error) func(*testing.B) {
232+
return func(b *testing.B) {
233+
inst := setup(b)
234+
b.ReportAllocs()
235+
b.ResetTimer()
236+
for b.Loop() {
237+
inst.ExportSpans(b.Context(), nSpans).End(n, err)
238+
}
239+
}
236240
}
237-
_ = end
241+
242+
b.Run("NoError", run(nSpans, nil))
243+
b.Run("PartialError", run(4, err))
244+
b.Run("FullError", run(0, err))
238245
}

exporters/stdout/stdouttrace/trace.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ type Exporter struct {
5656
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) (err error) {
5757
var success int64
5858
if e.inst != nil {
59-
end := e.inst.ExportSpans(ctx, len(spans))
60-
defer func() { end(success, err) }()
59+
op := e.inst.ExportSpans(ctx, len(spans))
60+
defer func() { op.End(success, err) }()
6161
}
6262

6363
if err := ctx.Err(); err != nil {

0 commit comments

Comments
 (0)