Skip to content

Conversation

dougqh
Copy link
Contributor

@dougqh dougqh commented Sep 15, 2025

What Does This Do

This changes reuses a SpanBuilder within a thread.

Motivation

SpanBuilders are one of the major causes of allocation within the client library, but typically only one SpanBuilder is needed at a time in a thread. By reseting and reusing the same SpanBuilder, tracing allocation can be reduced significantly.

By reducing allocation, the garbage collector needs to run less frequently improving the maximum sustainable throughput of the host application. In local tests with Spring Petclinic, this reduces the throughput penalty with tiny heaps (<= 80m) significantly from -40% to -20%. On larger heaps, the gain is more modest, since garbage collector has less of an impact on the application.

In this initial version, the thread local cache isn't used in virtual threads. This restriction exists to avoid creating more memory churn than we save, since virtual threads are created more often than regular threads.

Initial performance experiment

The idea is to store a CoreSpanBuilder per thread, since usually only SpanBuilder is in use at a given time per thread -- and CoreSpanBuilder isn't thread safe

This simple change provides a giant boost in small heaps
Improving Spring petclinic throughput from -39% to -19% with 80m heap
@dougqh dougqh requested a review from a team as a code owner September 15, 2025 19:47
Copy link
Contributor

github-actions bot commented Sep 15, 2025

Hi! 👋 Thanks for your pull request! 🎉

To help us review it, please make sure to:

  • Add at least one type, and one component or instrumentation label to the pull request

If you need help, please check our contributing guidelines.

@dougqh dougqh added comp: core Tracer core type: enhancement Enhancements and improvements tag: performance Performance related changes labels Sep 15, 2025
@datadog-datadog-prod-us1
Copy link
Contributor

datadog-datadog-prod-us1 bot commented Sep 15, 2025

🎯 Code Coverage
Patch Coverage: 54.84%
Total Coverage: 55.20% (-4.49%)

View detailed report

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: e26959d | Docs | Was this helpful? Give us feedback!

@pr-commenter
Copy link

pr-commenter bot commented Sep 15, 2025

Benchmarks

Startup

Parameters

Baseline Candidate
baseline_or_candidate baseline candidate
git_branch master dougqh/spanbuilder-pooling
git_commit_date 1759870068 1759872257
git_commit_sha 054a9d5 e26959d
release_version 1.55.0-SNAPSHOT~054a9d5313 1.54.0-SNAPSHOT~e26959db92
See matching parameters
Baseline Candidate
application insecure-bank insecure-bank
ci_job_date 1759873976 1759873976
ci_job_id 1168383052 1168383052
ci_pipeline_id 78662479 78662479
cpu_model Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz
kernel_version Linux runner-zfyrx7zua-project-304-concurrent-1-8fuxq1ep 6.8.0-1031-aws #33~22.04.1-Ubuntu SMP Thu Jun 26 14:22:30 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux Linux runner-zfyrx7zua-project-304-concurrent-1-8fuxq1ep 6.8.0-1031-aws #33~22.04.1-Ubuntu SMP Thu Jun 26 14:22:30 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
module Agent Agent
parent None None

Summary

Found 0 performance improvements and 0 performance regressions! Performance is the same for 57 metrics, 8 unstable metrics.

Startup time reports for petclinic
gantt
    title petclinic - global startup overhead: candidate=1.54.0-SNAPSHOT~e26959db92, baseline=1.55.0-SNAPSHOT~054a9d5313

    dateFormat X
    axisFormat %s
section tracing
Agent [baseline] (1.015 s) : 0, 1015044
Total [baseline] (10.746 s) : 0, 10745925
Agent [candidate] (1.027 s) : 0, 1027337
Total [candidate] (10.787 s) : 0, 10787358
section appsec
Agent [baseline] (1.201 s) : 0, 1201204
Total [baseline] (11.126 s) : 0, 11126394
Agent [candidate] (1.194 s) : 0, 1194402
Total [candidate] (11.118 s) : 0, 11118193
section iast
Agent [baseline] (1.151 s) : 0, 1150762
Total [baseline] (10.997 s) : 0, 10997187
Agent [candidate] (1.156 s) : 0, 1155924
Total [candidate] (11.05 s) : 0, 11050035
section profiling
Agent [baseline] (1.174 s) : 0, 1174130
Total [baseline] (11.084 s) : 0, 11083988
Agent [candidate] (1.164 s) : 0, 1163885
Total [candidate] (11.009 s) : 0, 11009210
Loading
  • baseline results
Module Variant Duration Δ tracing
Agent tracing 1.015 s -
Agent appsec 1.201 s 186.159 ms (18.3%)
Agent iast 1.151 s 135.717 ms (13.4%)
Agent profiling 1.174 s 159.085 ms (15.7%)
Total tracing 10.746 s -
Total appsec 11.126 s 380.468 ms (3.5%)
Total iast 10.997 s 251.261 ms (2.3%)
Total profiling 11.084 s 338.062 ms (3.1%)
  • candidate results
Module Variant Duration Δ tracing
Agent tracing 1.027 s -
Agent appsec 1.194 s 167.065 ms (16.3%)
Agent iast 1.156 s 128.587 ms (12.5%)
Agent profiling 1.164 s 136.548 ms (13.3%)
Total tracing 10.787 s -
Total appsec 11.118 s 330.835 ms (3.1%)
Total iast 11.05 s 262.677 ms (2.4%)
Total profiling 11.009 s 221.852 ms (2.1%)
gantt
    title petclinic - break down per module: candidate=1.54.0-SNAPSHOT~e26959db92, baseline=1.55.0-SNAPSHOT~054a9d5313

    dateFormat X
    axisFormat %s
section tracing
crashtracking [baseline] (1.462 ms) : 0, 1462
crashtracking [candidate] (1.462 ms) : 0, 1462
BytebuddyAgent [baseline] (692.409 ms) : 0, 692409
BytebuddyAgent [candidate] (698.44 ms) : 0, 698440
GlobalTracer [baseline] (241.661 ms) : 0, 241661
GlobalTracer [candidate] (244.536 ms) : 0, 244536
AppSec [baseline] (32.868 ms) : 0, 32868
AppSec [candidate] (33.042 ms) : 0, 33042
Debugger [baseline] (6.436 ms) : 0, 6436
Debugger [candidate] (6.486 ms) : 0, 6486
Remote Config [baseline] (705.337 µs) : 0, 705
Remote Config [candidate] (706.981 µs) : 0, 707
Telemetry [baseline] (9.185 ms) : 0, 9185
Telemetry [candidate] (9.253 ms) : 0, 9253
Flare Poller [baseline] (9.024 ms) : 0, 9024
Flare Poller [candidate] (12.016 ms) : 0, 12016
section appsec
crashtracking [baseline] (1.472 ms) : 0, 1472
crashtracking [candidate] (1.453 ms) : 0, 1453
BytebuddyAgent [baseline] (721.685 ms) : 0, 721685
BytebuddyAgent [candidate] (717.106 ms) : 0, 717106
GlobalTracer [baseline] (236.014 ms) : 0, 236014
GlobalTracer [candidate] (235.493 ms) : 0, 235493
IAST [baseline] (24.935 ms) : 0, 24935
IAST [candidate] (24.777 ms) : 0, 24777
AppSec [baseline] (175.692 ms) : 0, 175692
AppSec [candidate] (175.362 ms) : 0, 175362
Debugger [baseline] (6.161 ms) : 0, 6161
Debugger [candidate] (6.036 ms) : 0, 6036
Remote Config [baseline] (663.139 µs) : 0, 663
Remote Config [candidate] (652.435 µs) : 0, 652
Telemetry [baseline] (8.572 ms) : 0, 8572
Telemetry [candidate] (8.485 ms) : 0, 8485
Flare Poller [baseline] (4.815 ms) : 0, 4815
Flare Poller [candidate] (3.94 ms) : 0, 3940
section iast
crashtracking [baseline] (1.47 ms) : 0, 1470
crashtracking [candidate] (1.472 ms) : 0, 1472
BytebuddyAgent [baseline] (814.62 ms) : 0, 814620
BytebuddyAgent [candidate] (818.778 ms) : 0, 818778
GlobalTracer [baseline] (232.1 ms) : 0, 232100
GlobalTracer [candidate] (232.921 ms) : 0, 232921
IAST [baseline] (26.246 ms) : 0, 26246
IAST [candidate] (26.41 ms) : 0, 26410
AppSec [baseline] (35.598 ms) : 0, 35598
AppSec [candidate] (35.605 ms) : 0, 35605
Debugger [baseline] (6.078 ms) : 0, 6078
Debugger [candidate] (6.089 ms) : 0, 6089
Remote Config [baseline] (604.187 µs) : 0, 604
Remote Config [candidate] (599.676 µs) : 0, 600
Telemetry [baseline] (8.548 ms) : 0, 8548
Telemetry [candidate] (8.499 ms) : 0, 8499
Flare Poller [baseline] (4.149 ms) : 0, 4149
Flare Poller [candidate] (4.181 ms) : 0, 4181
section profiling
crashtracking [baseline] (1.447 ms) : 0, 1447
crashtracking [candidate] (1.444 ms) : 0, 1444
BytebuddyAgent [baseline] (729.124 ms) : 0, 729124
BytebuddyAgent [candidate] (721.344 ms) : 0, 721344
GlobalTracer [baseline] (219.449 ms) : 0, 219449
GlobalTracer [candidate] (218.937 ms) : 0, 218937
AppSec [baseline] (33.541 ms) : 0, 33541
AppSec [candidate] (33.138 ms) : 0, 33138
Debugger [baseline] (8.079 ms) : 0, 8079
Debugger [candidate] (6.497 ms) : 0, 6497
Remote Config [baseline] (737.493 µs) : 0, 737
Remote Config [candidate] (1.48 ms) : 0, 1480
Telemetry [baseline] (15.077 ms) : 0, 15077
Telemetry [candidate] (15.681 ms) : 0, 15681
Flare Poller [baseline] (4.223 ms) : 0, 4223
Flare Poller [candidate] (4.214 ms) : 0, 4214
ProfilingAgent [baseline] (108.463 ms) : 0, 108463
ProfilingAgent [candidate] (108.649 ms) : 0, 108649
Profiling [baseline] (109.901 ms) : 0, 109901
Profiling [candidate] (109.225 ms) : 0, 109225
Loading
Startup time reports for insecure-bank
gantt
    title insecure-bank - global startup overhead: candidate=1.54.0-SNAPSHOT~e26959db92, baseline=1.55.0-SNAPSHOT~054a9d5313

    dateFormat X
    axisFormat %s
section tracing
Agent [baseline] (1.023 s) : 0, 1022658
Total [baseline] (8.712 s) : 0, 8712057
Agent [candidate] (1.018 s) : 0, 1018268
Total [candidate] (8.653 s) : 0, 8653154
section iast
Agent [baseline] (1.16 s) : 0, 1160375
Total [baseline] (9.286 s) : 0, 9285827
Agent [candidate] (1.152 s) : 0, 1151825
Total [candidate] (9.266 s) : 0, 9265918
Loading
  • baseline results
Module Variant Duration Δ tracing
Agent tracing 1.023 s -
Agent iast 1.16 s 137.718 ms (13.5%)
Total tracing 8.712 s -
Total iast 9.286 s 573.77 ms (6.6%)
  • candidate results
Module Variant Duration Δ tracing
Agent tracing 1.018 s -
Agent iast 1.152 s 133.557 ms (13.1%)
Total tracing 8.653 s -
Total iast 9.266 s 612.764 ms (7.1%)
gantt
    title insecure-bank - break down per module: candidate=1.54.0-SNAPSHOT~e26959db92, baseline=1.55.0-SNAPSHOT~054a9d5313

    dateFormat X
    axisFormat %s
section tracing
crashtracking [baseline] (1.477 ms) : 0, 1477
crashtracking [candidate] (1.456 ms) : 0, 1456
BytebuddyAgent [baseline] (697.856 ms) : 0, 697856
BytebuddyAgent [candidate] (692.808 ms) : 0, 692808
GlobalTracer [baseline] (243.478 ms) : 0, 243478
GlobalTracer [candidate] (242.604 ms) : 0, 242604
AppSec [baseline] (33.12 ms) : 0, 33120
AppSec [candidate] (32.777 ms) : 0, 32777
Debugger [baseline] (6.491 ms) : 0, 6491
Debugger [candidate] (6.417 ms) : 0, 6417
Remote Config [baseline] (721.993 µs) : 0, 722
Remote Config [candidate] (696.666 µs) : 0, 697
Telemetry [baseline] (9.286 ms) : 0, 9286
Telemetry [candidate] (9.272 ms) : 0, 9272
Flare Poller [baseline] (8.887 ms) : 0, 8887
Flare Poller [candidate] (11.093 ms) : 0, 11093
section iast
crashtracking [baseline] (1.467 ms) : 0, 1467
crashtracking [candidate] (1.449 ms) : 0, 1449
BytebuddyAgent [baseline] (822.014 ms) : 0, 822014
BytebuddyAgent [candidate] (815.847 ms) : 0, 815847
GlobalTracer [baseline] (233.126 ms) : 0, 233126
GlobalTracer [candidate] (232.503 ms) : 0, 232503
AppSec [baseline] (35.935 ms) : 0, 35935
AppSec [candidate] (35.088 ms) : 0, 35088
Debugger [baseline] (6.144 ms) : 0, 6144
Debugger [candidate] (6.101 ms) : 0, 6101
Remote Config [baseline] (620.225 µs) : 0, 620
Remote Config [candidate] (606.308 µs) : 0, 606
Telemetry [baseline] (8.802 ms) : 0, 8802
Telemetry [candidate] (8.511 ms) : 0, 8511
Flare Poller [baseline] (4.268 ms) : 0, 4268
Flare Poller [candidate] (4.16 ms) : 0, 4160
IAST [baseline] (26.632 ms) : 0, 26632
IAST [candidate] (26.292 ms) : 0, 26292
Loading

Load

Parameters

Baseline Candidate
baseline_or_candidate baseline candidate
git_branch master dougqh/spanbuilder-pooling
git_commit_date 1759870068 1759872257
git_commit_sha 054a9d5 e26959d
release_version 1.55.0-SNAPSHOT~054a9d5313 1.54.0-SNAPSHOT~e26959db92
See matching parameters
Baseline Candidate
application insecure-bank insecure-bank
ci_job_date 1759873719 1759873719
ci_job_id 1168383053 1168383053
ci_pipeline_id 78662479 78662479
cpu_model Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz
kernel_version Linux runner-zfyrx7zua-project-304-concurrent-1-g48owdwm 6.8.0-1031-aws #33~22.04.1-Ubuntu SMP Thu Jun 26 14:22:30 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux Linux runner-zfyrx7zua-project-304-concurrent-1-g48owdwm 6.8.0-1031-aws #33~22.04.1-Ubuntu SMP Thu Jun 26 14:22:30 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

Summary

Found 2 performance improvements and 2 performance regressions! Performance is the same for 8 metrics, 12 unstable metrics.

scenario Δ mean http_req_duration Δ mean throughput candidate mean http_req_duration candidate mean throughput baseline mean http_req_duration baseline mean throughput
scenario:load:insecure-bank:profiling:high_load worse
[+268.633µs; +572.617µs] or [+3.086%; +6.578%]
unstable
[-88.165op/s; +40.165op/s] or [-16.551%; +7.540%]
9.126ms 508.688op/s 8.705ms 532.688op/s
scenario:load:insecure-bank:iast:high_load better
[-680.504µs; -321.529µs] or [-6.687%; -3.160%]
unstable
[-28.604op/s; +74.917op/s] or [-6.263%; +16.402%]
9.675ms 479.906op/s 10.176ms 456.750op/s
scenario:load:petclinic:tracing:high_load better
[-1.825ms; -0.997ms] or [-4.018%; -2.195%]
unstable
[-4.129op/s; +10.654op/s] or [-4.008%; +10.342%]
44.010ms 106.287op/s 45.421ms 103.025op/s
scenario:load:petclinic:profiling:high_load worse
[+4.247ms; +5.219ms] or [+9.165%; +11.261%]
unstable
[-16.275op/s; -2.400op/s] or [-16.118%; -2.377%]
51.076ms 91.638op/s 46.343ms 100.975op/s
Request duration reports for petclinic
gantt
    title petclinic - request duration [CI 0.99] : candidate=1.54.0-SNAPSHOT~e26959db92, baseline=1.55.0-SNAPSHOT~054a9d5313
    dateFormat X
    axisFormat %s
section baseline
no_agent (37.165 ms) : 36862, 37467
.   : milestone, 37165,
appsec (48.093 ms) : 47660, 48525
.   : milestone, 48093,
code_origins (46.409 ms) : 46006, 46812
.   : milestone, 46409,
iast (46.0 ms) : 45598, 46401
.   : milestone, 46000,
profiling (46.343 ms) : 45927, 46758
.   : milestone, 46343,
tracing (45.421 ms) : 45023, 45820
.   : milestone, 45421,
section candidate
no_agent (36.89 ms) : 36598, 37183
.   : milestone, 36890,
appsec (47.601 ms) : 47172, 48030
.   : milestone, 47601,
code_origins (46.61 ms) : 46196, 47023
.   : milestone, 46610,
iast (45.954 ms) : 45563, 46345
.   : milestone, 45954,
profiling (51.076 ms) : 50591, 51560
.   : milestone, 51076,
tracing (44.01 ms) : 43640, 44381
.   : milestone, 44010,
Loading
  • baseline results
Variant Request duration [CI 0.99] Δ no_agent
no_agent 37.165 ms [36.862 ms, 37.467 ms] -
appsec 48.093 ms [47.66 ms, 48.525 ms] 10.928 ms (29.4%)
code_origins 46.409 ms [46.006 ms, 46.812 ms] 9.245 ms (24.9%)
iast 46.0 ms [45.598 ms, 46.401 ms] 8.835 ms (23.8%)
profiling 46.343 ms [45.927 ms, 46.758 ms] 9.178 ms (24.7%)
tracing 45.421 ms [45.023 ms, 45.82 ms] 8.257 ms (22.2%)
  • candidate results
Variant Request duration [CI 0.99] Δ no_agent
no_agent 36.89 ms [36.598 ms, 37.183 ms] -
appsec 47.601 ms [47.172 ms, 48.03 ms] 10.71 ms (29.0%)
code_origins 46.61 ms [46.196 ms, 47.023 ms] 9.719 ms (26.3%)
iast 45.954 ms [45.563 ms, 46.345 ms] 9.064 ms (24.6%)
profiling 51.076 ms [50.591 ms, 51.56 ms] 14.185 ms (38.5%)
tracing 44.01 ms [43.64 ms, 44.381 ms] 7.12 ms (19.3%)
Request duration reports for insecure-bank
gantt
    title insecure-bank - request duration [CI 0.99] : candidate=1.54.0-SNAPSHOT~e26959db92, baseline=1.55.0-SNAPSHOT~054a9d5313
    dateFormat X
    axisFormat %s
section baseline
no_agent (4.406 ms) : 4357, 4455
.   : milestone, 4406,
iast (10.176 ms) : 10003, 10349
.   : milestone, 10176,
iast_FULL (14.313 ms) : 14029, 14597
.   : milestone, 14313,
iast_GLOBAL (10.894 ms) : 10701, 11088
.   : milestone, 10894,
profiling (8.705 ms) : 8567, 8843
.   : milestone, 8705,
tracing (7.795 ms) : 7686, 7905
.   : milestone, 7795,
section candidate
no_agent (4.528 ms) : 4469, 4587
.   : milestone, 4528,
iast (9.675 ms) : 9515, 9835
.   : milestone, 9675,
iast_FULL (13.92 ms) : 13643, 14196
.   : milestone, 13920,
iast_GLOBAL (11.09 ms) : 10889, 11291
.   : milestone, 11090,
profiling (9.126 ms) : 8981, 9270
.   : milestone, 9126,
tracing (7.778 ms) : 7669, 7888
.   : milestone, 7778,
Loading
  • baseline results
Variant Request duration [CI 0.99] Δ no_agent
no_agent 4.406 ms [4.357 ms, 4.455 ms] -
iast 10.176 ms [10.003 ms, 10.349 ms] 5.77 ms (131.0%)
iast_FULL 14.313 ms [14.029 ms, 14.597 ms] 9.907 ms (224.9%)
iast_GLOBAL 10.894 ms [10.701 ms, 11.088 ms] 6.488 ms (147.3%)
profiling 8.705 ms [8.567 ms, 8.843 ms] 4.299 ms (97.6%)
tracing 7.795 ms [7.686 ms, 7.905 ms] 3.389 ms (76.9%)
  • candidate results
Variant Request duration [CI 0.99] Δ no_agent
no_agent 4.528 ms [4.469 ms, 4.587 ms] -
iast 9.675 ms [9.515 ms, 9.835 ms] 5.147 ms (113.7%)
iast_FULL 13.92 ms [13.643 ms, 14.196 ms] 9.392 ms (207.4%)
iast_GLOBAL 11.09 ms [10.889 ms, 11.291 ms] 6.562 ms (144.9%)
profiling 9.126 ms [8.981 ms, 9.27 ms] 4.598 ms (101.5%)
tracing 7.778 ms [7.669 ms, 7.888 ms] 3.251 ms (71.8%)

Dacapo

Parameters

Baseline Candidate
baseline_or_candidate baseline candidate
git_branch master dougqh/spanbuilder-pooling
git_commit_date 1759870068 1759872257
git_commit_sha 054a9d5 e26959d
release_version 1.55.0-SNAPSHOT~054a9d5313 1.54.0-SNAPSHOT~e26959db92
See matching parameters
Baseline Candidate
application biojava biojava
ci_job_date 1759874243 1759874243
ci_job_id 1168383055 1168383055
ci_pipeline_id 78662479 78662479
cpu_model Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz
kernel_version Linux runner-zfyrx7zua-project-304-concurrent-1-by8u5ov0 6.8.0-1031-aws #33~22.04.1-Ubuntu SMP Thu Jun 26 14:22:30 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux Linux runner-zfyrx7zua-project-304-concurrent-1-by8u5ov0 6.8.0-1031-aws #33~22.04.1-Ubuntu SMP Thu Jun 26 14:22:30 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

Summary

Found 0 performance improvements and 0 performance regressions! Performance is the same for 11 metrics, 1 unstable metrics.

Execution time for tomcat
gantt
    title tomcat - execution time [CI 0.99] : candidate=1.54.0-SNAPSHOT~e26959db92, baseline=1.55.0-SNAPSHOT~054a9d5313
    dateFormat X
    axisFormat %s
section baseline
no_agent (1.472 ms) : 1461, 1484
.   : milestone, 1472,
appsec (3.715 ms) : 3497, 3933
.   : milestone, 3715,
iast (2.212 ms) : 2148, 2275
.   : milestone, 2212,
iast_GLOBAL (2.252 ms) : 2188, 2315
.   : milestone, 2252,
profiling (2.037 ms) : 1986, 2087
.   : milestone, 2037,
tracing (2.022 ms) : 1973, 2071
.   : milestone, 2022,
section candidate
no_agent (1.475 ms) : 1464, 1487
.   : milestone, 1475,
appsec (3.736 ms) : 3519, 3953
.   : milestone, 3736,
iast (2.215 ms) : 2152, 2279
.   : milestone, 2215,
iast_GLOBAL (2.237 ms) : 2173, 2300
.   : milestone, 2237,
profiling (2.054 ms) : 2003, 2105
.   : milestone, 2054,
tracing (2.025 ms) : 1975, 2074
.   : milestone, 2025,
Loading
  • baseline results
Variant Execution Time [CI 0.99] Δ no_agent
no_agent 1.472 ms [1.461 ms, 1.484 ms] -
appsec 3.715 ms [3.497 ms, 3.933 ms] 2.243 ms (152.3%)
iast 2.212 ms [2.148 ms, 2.275 ms] 739.271 µs (50.2%)
iast_GLOBAL 2.252 ms [2.188 ms, 2.315 ms] 779.016 µs (52.9%)
profiling 2.037 ms [1.986 ms, 2.087 ms] 564.42 µs (38.3%)
tracing 2.022 ms [1.973 ms, 2.071 ms] 549.738 µs (37.3%)
  • candidate results
Variant Execution Time [CI 0.99] Δ no_agent
no_agent 1.475 ms [1.464 ms, 1.487 ms] -
appsec 3.736 ms [3.519 ms, 3.953 ms] 2.261 ms (153.3%)
iast 2.215 ms [2.152 ms, 2.279 ms] 740.034 µs (50.2%)
iast_GLOBAL 2.237 ms [2.173 ms, 2.3 ms] 761.591 µs (51.6%)
profiling 2.054 ms [2.003 ms, 2.105 ms] 578.942 µs (39.2%)
tracing 2.025 ms [1.975 ms, 2.074 ms] 549.57 µs (37.3%)
Execution time for biojava
gantt
    title biojava - execution time [CI 0.99] : candidate=1.54.0-SNAPSHOT~e26959db92, baseline=1.55.0-SNAPSHOT~054a9d5313
    dateFormat X
    axisFormat %s
section baseline
no_agent (14.948 s) : 14948000, 14948000
.   : milestone, 14948000,
appsec (15.119 s) : 15119000, 15119000
.   : milestone, 15119000,
iast (18.496 s) : 18496000, 18496000
.   : milestone, 18496000,
iast_GLOBAL (18.058 s) : 18058000, 18058000
.   : milestone, 18058000,
profiling (14.841 s) : 14841000, 14841000
.   : milestone, 14841000,
tracing (15.317 s) : 15317000, 15317000
.   : milestone, 15317000,
section candidate
no_agent (15.078 s) : 15078000, 15078000
.   : milestone, 15078000,
appsec (14.797 s) : 14797000, 14797000
.   : milestone, 14797000,
iast (18.379 s) : 18379000, 18379000
.   : milestone, 18379000,
iast_GLOBAL (18.053 s) : 18053000, 18053000
.   : milestone, 18053000,
profiling (15.738 s) : 15738000, 15738000
.   : milestone, 15738000,
tracing (15.006 s) : 15006000, 15006000
.   : milestone, 15006000,
Loading
  • baseline results
Variant Execution Time [CI 0.99] Δ no_agent
no_agent 14.948 s [14.948 s, 14.948 s] -
appsec 15.119 s [15.119 s, 15.119 s] 171.0 ms (1.1%)
iast 18.496 s [18.496 s, 18.496 s] 3.548 s (23.7%)
iast_GLOBAL 18.058 s [18.058 s, 18.058 s] 3.11 s (20.8%)
profiling 14.841 s [14.841 s, 14.841 s] -107.0 ms (-0.7%)
tracing 15.317 s [15.317 s, 15.317 s] 369.0 ms (2.5%)
  • candidate results
Variant Execution Time [CI 0.99] Δ no_agent
no_agent 15.078 s [15.078 s, 15.078 s] -
appsec 14.797 s [14.797 s, 14.797 s] -281.0 ms (-1.9%)
iast 18.379 s [18.379 s, 18.379 s] 3.301 s (21.9%)
iast_GLOBAL 18.053 s [18.053 s, 18.053 s] 2.975 s (19.7%)
profiling 15.738 s [15.738 s, 15.738 s] 660.0 ms (4.4%)
tracing 15.006 s [15.006 s, 15.006 s] -72.0 ms (-0.5%)

public static final String OPTIMIZED_MAP_ENABLED = "optimized.map.enabled";
public static final String TAG_NAME_UTF8_CACHE_SIZE = "tag.name.utf8.cache.size";
public static final String TAG_VALUE_UTF8_CACHE_SIZE = "tag.value.utf8.cache.size";
public static final String SPAN_BUILDER_REUSE_ENABLED = "span.builder.reuse.enabled";
Copy link
Contributor Author

@dougqh dougqh Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is best class to be placing these in. Or if this is the proper namespace. dd.trace... might be better

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree dd.trace prefix fits better

Copy link
Contributor

@PerfectSlayer PerfectSlayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ question: ‏Quick question for my understanding: how is that supposed to work with manual tracing API? (DD or OTel)

Let’s say:

var spanBuilder = tracer.spanBuilder("span1");
// …
var span = tracer.spanBuilder("span2").startSpan();
// …
spanBuilder.setAttribute("key", "value").startSpan(); 

The second startSpan() call will start the span with the wrong name ("span2" instead of "span1") and there is nothing we can do on the API to prevent such use case.

@dougqh
Copy link
Contributor Author

dougqh commented Sep 16, 2025

The second startSpan() call will start the span with the wrong name ("span2" instead of "span1") and there is nothing we can do on the API to prevent such use case.
Actually, I updated my approach yesterday to handle that specific case; however, other cases are definitely worth considering.

The solution that I put in place yesterday was that I track whether or not the SpanBuilder is "in-use". A SpanBuilder is considered "in -use" from the point that it is reset during CoreTracer.buildSpan up until a span is created with SpanBuilder.buildSpan.

So CoreTracer.buildSpan can check if the ThreadLocal SpanBuilder is still "in-use". If it is, then the CoreTracer still constructs a new SpanBuilder and returns that instead.

So the 'in-use' check solves the case mentioned in the original comment. You'll still get two distinct SpanBuilders.
I definitely should add a test for that. Right now, I just wanted a sanity check from others and the automated benchmarking.

But there are still other cases that worry me, specifically, I'm worried that you can actually call build on the SpanBuilder multiple times. That's not idiomatic, but it would probably work today. My solution doesn't handle that.

I don't believe our own instrumentation produces multiple Spans per builder today, so we could solve the problem by having two slightly different sets of rules: one for automatic and one for manual. We can accomplish that by having two sets of methods: one set that will reuse SpanBuilders that we use in automatic instrumentation, and another set that always creates a new SpanBuilder that we use in manual instrumentation (and the OTel bridge).

UPDATE:
I realized that there was an easy middle ground with builder reuse issues.

buildSpan can maintain the existing semantics including allowing building multiple spans.
The startSpan convenience methods can safely reuse a SpanBuilder, since we know that it only builds a single span.
And to do that, I introduced a new singleSpanBuilder method that we can use in automatic instrumentation. singleSpanBuilder can use SpanBuilder recycling under the covers as long as we know it is not "in-use" (see above)

That does mean that we have to update some instrumentation to get the full benefit, but at least, we know the change is safe.

Refactored code, so tests work regardless of Config
To avoid breaking any potential code that builds multiple spans from the same SpanBuilder, updated the SpanBuilder pooling approach

Introduced a new method singleSpanBuilder which can build one and only one span, this method can be used by automatic instrumentation as an optimization.

singleSpanBuilder is now used inside the startSpan convenience methods, since we know they only build and return one span.  Any automatic instrumentation using startSpan gets the optimization for free.

buildSpan maintains its original semantics, so all existing continues to work as is.
@dougqh dougqh requested a review from a team as a code owner September 16, 2025 20:11
In a microbenchmark, buildSpan was performing worse than previously.

To address, that shortcoming and to clean-up the code...
Made CoreSpanBuilder abstract and introduced two child classes: MultiSpanBuilder and ReusableSingleSpanBuilder

MultiSpanBuilder is used by buildSpan
ReusableSingleSpanBuilder is used by singleSpanBuilder / startSpan (indirectly)
@PerfectSlayer
Copy link
Contributor

PerfectSlayer commented Sep 18, 2025

I would make the singleSpanBuilder() always reuse the builder and trying to reset it -if it's still unused, it can create a > new one. And if build() is called multiple time on a singleSpanBuilder() I would raise an error (or at least something > for the developer to be aware of).

The current code does try to always reuse the SpanBuilder, it just safe guards against the case where the prior caller hasn't finished with it.
I debated raising an exception, but I was worried about exception leakage.
I'm mostly okay with an exception in an internal method, but I might just go with an assert.

However, the assert won't be completely reliable. If the SpanBuilder gets reset before the next call to buildSpan, then I cannot detect that. Or at least, I don't think I can detect it without adding a wrapper object (which is counter to the aim).

An alternative option could be to only have the buildSpan() method but to overload it with a third parameter "reusable".
It could be false by default, enabling the optimisation by default, but set to true when creating builder using the > public APIs (dd-trace-api and OTel Tracing API) to ensure compatibility.

Yes, I considered that. I kind of like the explicitness of singleSpanBuilder(), since it tells you the imposed limitation. I'm curious what others think.

Comment on lines +247 to +248
private static final boolean SPAN_BUILDER_REUSE_ENABLED =
Config.get().isSpanBuilderReuseEnabled();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with dd-java-agent configuration, just curious, is there a possibility for remote on-the-fly configuration? Or this flag can be applied only with restart? Just thinking out loud about usability.

Copy link
Contributor Author

@dougqh dougqh Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not currently - although, that is something that we've discussed doing off and on
I'm honestly not expecting any customer to ever set this, so I'm going with the approach that works best for the JIT

Comment on lines +1935 to +1937
static final class ReusableSingleSpanBuilderThreadLocalCache
extends ThreadLocal<ReusableSingleSpanBuilder> {
private final CoreTracer tracer;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious if it will be OK on latest JDKs with virtual threads?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an init instead of calling reset when creating a new ReusableSingleSpanBuilder

Added some asserts to catch misuse of the API
Adding ThreadUtils class that enables checking if Threads are virtual threads
@dougqh dougqh requested a review from a team as a code owner October 7, 2025 15:23
@dougqh dougqh requested review from bric3 and removed request for a team October 7, 2025 15:23
@Override
public SpanBuilder singleSpanBuilder(
final String instrumentationName, final CharSequence spanName) {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: wouldn't it be better to have a DUMB instance in cases like this. That might be my null fear speaking.

// Used to track whether the ReusableSingleSpanBuilder is actively being used
// ReusableSingleSpanBuilder becomes "inUse" after a succesful reset and remains "inUse"
// until "buildSpan" is called
protected boolean inUse;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: protected visibility while class is final. Shouldn't it be package visible ?

return buildSpan(DEFAULT_INSTRUMENTATION_NAME, spanName);
}

SpanBuilder buildSpan(String instrumentationName, CharSequence spanName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Might be useful to add the javadoc here as well. Or is singleSpanBuilder temporary to keep other instrumentations operational first with the current method ?

Or even distinguish between the span with or without childs ?


/**
* Returns a SpanBuilder that can be used to produce one and only one span. By imposing the
* single span creation limitation, this method is more efficient than {@link buildSpan}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: For the link to work, a # is needed.

Suggested change
* single span creation limitation, this method is more efficient than {@link buildSpan}
* single span creation limitation, this method is more efficient than {@link #buildSpan}

// that case could result in permanently burning the cache for a given thread.

// That could be solved with additional logic during ReusableSingleSpanBuilder#buildSpan
// that checks to see if the cached the Builder is in use and then replaces it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo:

Suggested change
// that checks to see if the cached the Builder is in use and then replaces it
// that checks to see if the cached Builder is in use and then replaces it

}
}

static final class MultiSpanBuilder extends CoreSpanBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Some javadoc

  /**
   * A span builder that creates a new instance for each span.
   *
   * <p>This builder is used when span builder reuse is disabled or when building spans that may
   * have multiple children. Each call to {@link #buildSpan()} creates a fresh span instance
   * without any state reuse.
   */
  static final class MultiSpanBuilder extends CoreSpanBuilder {

}
}

static final class ReusableSingleSpanBuilder extends CoreSpanBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Some javadoc suggestions

Suggested change
static final class ReusableSingleSpanBuilder extends CoreSpanBuilder {
/**
* A reusable span builder optimized for building single spans with no children.
*
* <p>This builder is pooled in a thread-local cache to reduce allocation overhead for
* high-frequency span creation.
*
* <p><b>Virtual thread handling:</b> Virtual threads do not use thread-local caching because
* they are created and destroyed frequently. Instead, a new builder instance is allocated for
* each span created on a virtual thread.
*
* @see CoreTracer#singleSpanBuilder(String, CharSequence)
* @see ReusableSingleSpanBuilderThreadLocalCache
*/
static final class ReusableSingleSpanBuilder extends CoreSpanBuilder {

Comment on lines +1994 to +1999
/**
* Resets the ReusableSingleSpanBuilder, so it may be used to build another single span Returns
* true if the reset was successful Returns false if this ReusableSingleSpanBuilder is still
* "in-use"
*/
final boolean reset(String instrumentationName, CharSequence operationName) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion:

Suggested change
/**
* Resets the ReusableSingleSpanBuilder, so it may be used to build another single span Returns
* true if the reset was successful Returns false if this ReusableSingleSpanBuilder is still
* "in-use"
*/
final boolean reset(String instrumentationName, CharSequence operationName) {
/**
* Resets the {@link ReusableSingleSpanBuilder}, so it may be used to build another single span
* @returns <code>true</code> if the reset was successful, otherwise <code>false</code>
* if this <code>ReusableSingleSpanBuilder</code> is still "in-use".
*/


@Override
public CoreSpanBuilder ignoreActiveSpan() {
public final CoreSpanBuilder ignoreActiveSpan() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Just noted that some method are private, and may not need to be final, but I understand your perspective on anticipating changes here since the class is not final. Also it aligns with other methods. So feel free to dismiss :)

Comment on lines +2003 to +2019
this.instrumentationName = instrumentationName;
this.operationName = operationName;

if (this.tagLedger != null) this.tagLedger.reset();
this.timestampMicro = 0L;
this.parent = null;
this.serviceName = null;
this.resourceName = null;
this.errorFlag = false;
this.spanType = null;
this.ignoreScope = false;
this.builderRequestContextDataAppSec = null;
this.builderRequestContextDataIast = null;
this.builderCiVisibilityContextData = null;
this.links = null;
this.spanId = 0L;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: Is there way to test nothing go forbidden here if CoreSpanBuilder gets a new field ? Maybe via some tests ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
comp: core Tracer core tag: performance Performance related changes type: enhancement Enhancements and improvements
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants