|
| 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