Skip to content

Commit ddc9a28

Browse files
committed
Add end-to-end benchmark using Datadog::Tracing.trace
Compare native vs HTTP transport through the full tracing pipeline: `Datadog::Tracing.trace` -> span creation -> trace flush -> transport. Uses `SyncWriter` (via `test_mode.async = false`) so each iteration completes a full round-trip synchronously. Configures the tracer once per transport mode, not per iteration. Usage: bundle exec ruby benchmarks/tracing_transport_e2e.rb
1 parent f569625 commit ddc9a28

1 file changed

Lines changed: 154 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Used to quickly run benchmark under RSpec as part of the usual test suite, to validate it didn't bitrot
2+
VALIDATE_BENCHMARK_MODE = ENV['VALIDATE_BENCHMARK'] == 'true'
3+
4+
return unless __FILE__ == $PROGRAM_NAME || VALIDATE_BENCHMARK_MODE
5+
6+
require_relative 'benchmarks_helper'
7+
require 'socket'
8+
9+
# End-to-end benchmark comparing native vs HTTP trace transport using
10+
# the full `Datadog::Tracing.trace` pipeline.
11+
#
12+
# Unlike `tracing_transport.rb` which benchmarks `send_traces` in
13+
# isolation with synthetic `TraceSegment`s, this benchmark exercises
14+
# the entire path: `Datadog::Tracing.trace` -> span creation -> trace
15+
# flush -> transport -> mock agent.
16+
#
17+
# Uses `SyncWriter` so each `trace {}` block completes a full
18+
# round-trip synchronously, giving stable per-iteration measurements.
19+
#
20+
# Usage:
21+
# bundle exec ruby benchmarks/tracing_transport_e2e.rb
22+
class TracingTransportE2EBenchmark
23+
# @param [Integer] depth number of nested spans per trace
24+
def initialize(depth: 10)
25+
Datadog.logger.level = Logger::FATAL
26+
@depth = depth
27+
@mock_agent = MockAgent.new
28+
@trace_code = build_trace_code(depth)
29+
end
30+
31+
def run_benchmark
32+
benchmark_time = VALIDATE_BENCHMARK_MODE ? {time: 0.01, warmup: 0} : {time: 12, warmup: 2}
33+
34+
Benchmark.ips do |x|
35+
x.config(**benchmark_time)
36+
37+
configure_tracer(:http)
38+
x.report("#{@depth} span trace - HTTP transport") do
39+
eval(@trace_code) # standard:disable Security/Eval
40+
end
41+
42+
configure_tracer(:native)
43+
x.report("#{@depth} span trace - Native transport") do
44+
eval(@trace_code) # standard:disable Security/Eval
45+
end
46+
47+
x.save! "#{File.basename(__FILE__, '.rb')}-results.json" unless VALIDATE_BENCHMARK_MODE
48+
x.compare!
49+
end
50+
ensure
51+
Datadog::Tracing.shutdown!
52+
@mock_agent.stop
53+
end
54+
55+
private
56+
57+
def build_trace_code(depth)
58+
opens = depth.times.map { |i| "Datadog::Tracing.trace('op.#{i}') {" }
59+
closes = depth.times.map { '}' }
60+
(opens + closes).join
61+
end
62+
63+
def agent_url
64+
"http://127.0.0.1:#{@mock_agent.port}"
65+
end
66+
67+
def configure_tracer(mode)
68+
Datadog.configure do |c|
69+
c.logger.level = Logger::FATAL
70+
c.tracing.enabled = true
71+
c.tracing.native_transport = (mode == :native)
72+
c.tracing.test_mode.enabled = true
73+
c.tracing.test_mode.async = false # forces SyncWriter
74+
c.tracing.test_mode.writer_options = {
75+
transport: build_transport(mode),
76+
}
77+
end
78+
end
79+
80+
def build_transport(mode)
81+
case mode
82+
when :http
83+
agent_settings = Struct.new(:url, :adapter, :ssl, :hostname, :port, :uds_path, :timeout_seconds)
84+
.new(agent_url, :net_http, false, '127.0.0.1', @mock_agent.port, nil, 5)
85+
Datadog::Tracing::Transport::HTTP.default(
86+
agent_settings: agent_settings,
87+
logger: Logger.new('/dev/null'),
88+
)
89+
when :native
90+
require 'datadog/tracing/transport/native'
91+
agent_settings = Struct.new(:url).new(agent_url)
92+
Datadog::Tracing::Transport::Native::Transport.new(
93+
agent_settings: agent_settings,
94+
logger: Logger.new('/dev/null'),
95+
)
96+
end
97+
end
98+
99+
# Mock agent: forked process with threaded request handling.
100+
class MockAgent
101+
attr_reader :port
102+
103+
def initialize
104+
server = TCPServer.new('127.0.0.1', 0)
105+
@port = server.addr[1]
106+
107+
@pid = fork do
108+
body = '{"rate_by_service":{"service:,env:":1.0}}'
109+
response = "HTTP/1.1 200 OK\r\nContent-Length: #{body.bytesize}\r\n" \
110+
"Content-Type: application/json\r\n\r\n#{body}"
111+
queue = Queue.new
112+
113+
4.times do
114+
Thread.new do
115+
loop do
116+
client = queue.pop
117+
begin
118+
request_line = client.gets
119+
next client.close if request_line.nil?
120+
121+
content_length = 0
122+
while (line = client.gets) && line != "\r\n"
123+
content_length = line.split(': ', 2).last.to_i if line.downcase.start_with?('content-length')
124+
end
125+
client.read(content_length) if content_length > 0
126+
127+
client.print response
128+
rescue # rubocop:disable Lint/SuppressedException
129+
ensure
130+
client.close rescue nil
131+
end
132+
end
133+
end
134+
end
135+
136+
loop do
137+
client = server.accept rescue break
138+
queue.push(client)
139+
end
140+
end
141+
142+
server.close
143+
end
144+
145+
def stop
146+
Process.kill('TERM', @pid) rescue nil
147+
Process.wait(@pid) rescue nil
148+
end
149+
end
150+
end
151+
152+
puts "Current pid is #{Process.pid}"
153+
154+
TracingTransportE2EBenchmark.new(depth: 10).run_benchmark

0 commit comments

Comments
 (0)