Skip to content

Commit 3a3fdea

Browse files
committed
Benchmark all routes
1 parent 0db1219 commit 3a3fdea

File tree

1 file changed

+106
-53
lines changed

1 file changed

+106
-53
lines changed

spec/performance/bench.rb

Lines changed: 106 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
#!/usr/bin/env ruby
22
# frozen_string_literal: true
33

4+
require "English"
45
require "json"
56
require "fileutils"
67
require "net/http"
78
require "uri"
89

910
# Benchmark parameters
11+
PRO = ENV.fetch("PRO", "false") == "true"
12+
APP_DIR = PRO ? "react_on_rails_pro/spec/dummy" : "spec/dummy"
1013
BASE_URL = ENV.fetch("BASE_URL", "localhost:3001")
11-
ROUTE = ENV.fetch("ROUTE", "server_side_hello_world_hooks")
12-
TARGET = URI.parse("http://#{BASE_URL}/#{ROUTE}")
1314
# requests per second; if "max" will get maximum number of queries instead of a fixed rate
1415
RATE = ENV.fetch("RATE", "50")
1516
# concurrent connections/virtual users
@@ -67,6 +68,30 @@ def add_summary_line(*parts)
6768
end
6869
end
6970

71+
# Get routes from the Rails app filtered by pages# and react_router# controllers
72+
def get_benchmark_routes(app_dir)
73+
routes_output = `cd #{app_dir} && bundle exec rails routes 2>&1`
74+
raise "Failed to get routes from #{app_dir}" unless $CHILD_STATUS.success?
75+
76+
routes = []
77+
routes_output.each_line do |line|
78+
# Parse lines like: "server_side_hello_world GET /server_side_hello_world(.:format) pages#server_side_hello_world"
79+
# We want GET routes only (not POST, etc.) served by pages# or react_router# controllers
80+
# Capture path up to (.:format) part using [^(\s]+ (everything except '(' and whitespace)
81+
next unless (match = line.match(/GET\s+([^(\s]+).*(pages|react_router)#/))
82+
83+
path = match[1]
84+
path = "/" if path.empty? # Handle root route
85+
routes << path
86+
end
87+
raise "No pages# or react_router# routes found in #{app_dir}" if routes.empty?
88+
89+
routes
90+
end
91+
92+
# Get all routes to benchmark
93+
routes = get_benchmark_routes(APP_DIR)
94+
7095
validate_rate(RATE)
7196
validate_positive_integer(CONNECTIONS, "CONNECTIONS")
7297
validate_positive_integer(MAX_CONNECTIONS, "MAX_CONNECTIONS")
@@ -83,6 +108,8 @@ def add_summary_line(*parts)
83108

84109
puts <<~PARAMS
85110
Benchmark parameters:
111+
- APP_DIR: #{APP_DIR}
112+
- BASE_URL: #{BASE_URL}
86113
- RATE: #{RATE}
87114
- DURATION: #{DURATION}
88115
- REQUEST_TIMEOUT: #{REQUEST_TIMEOUT}
@@ -104,47 +131,42 @@ def server_responding?(uri)
104131

105132
# Wait for the server to be ready
106133
TIMEOUT_SEC = 60
134+
puts "Checking server availability at #{BASE_URL}..."
135+
test_uri = URI.parse("http://#{BASE_URL}#{routes.first}")
107136
start_time = Time.now
108137
loop do
109-
break if server_responding?(TARGET)
138+
break if server_responding?(test_uri)
110139

111-
raise "Target #{TARGET} not responding within #{TIMEOUT_SEC}s" if Time.now - start_time > TIMEOUT_SEC
140+
raise "Server at #{BASE_URL} not responding within #{TIMEOUT_SEC}s" if Time.now - start_time > TIMEOUT_SEC
112141

113142
sleep 1
114143
end
115-
116-
# Warm up server
117-
puts "Warming up server with 10 requests..."
118-
10.times do
119-
server_responding?(TARGET)
120-
sleep 0.5
121-
end
122-
puts "Warm-up complete"
144+
puts "Server is ready!"
123145

124146
FileUtils.mkdir_p(OUTDIR)
125147

126148
# Validate RATE=max constraint
127-
is_max_rate = RATE == "max"
128-
if is_max_rate && CONNECTIONS != MAX_CONNECTIONS
149+
IS_MAX_RATE = RATE == "max"
150+
if IS_MAX_RATE && CONNECTIONS != MAX_CONNECTIONS
129151
raise "For RATE=max, CONNECTIONS must be equal to MAX_CONNECTIONS (got #{CONNECTIONS} and #{MAX_CONNECTIONS})"
130152
end
131153

132-
# Initialize summary file
133-
File.write(SUMMARY_TXT, "")
134-
add_summary_line("Tool", "RPS", "p50(ms)", "p90(ms)", "p99(ms)", "Status")
154+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
135155

136-
# Fortio
137-
if TOOLS.include?("fortio")
138-
fortio_metrics = begin
139-
puts "===> Fortio"
156+
# Benchmark a single route with Fortio
157+
def run_fortio_benchmark(target, route_name)
158+
return nil unless TOOLS.include?("fortio")
140159

141-
fortio_json = "#{OUTDIR}/fortio.json"
142-
fortio_txt = "#{OUTDIR}/fortio.txt"
160+
begin
161+
puts "===> Fortio: #{route_name}"
162+
163+
fortio_json = "#{OUTDIR}/#{route_name}_fortio.json"
164+
fortio_txt = "#{OUTDIR}/#{route_name}_fortio.txt"
143165

144166
# Configure Fortio arguments
145167
# See https://github.com/fortio/fortio/wiki/FAQ#i-want-to-get-the-best-results-what-flags-should-i-pass
146168
fortio_args =
147-
if is_max_rate
169+
if IS_MAX_RATE
148170
["-qps", 0, "-c", CONNECTIONS]
149171
else
150172
["-qps", RATE, "-uniform", "-nocatchup", "-c", CONNECTIONS]
@@ -156,7 +178,7 @@ def server_responding?(uri)
156178
"-t", DURATION,
157179
"-timeout", REQUEST_TIMEOUT,
158180
"-json", fortio_json,
159-
TARGET
181+
target
160182
].join(" ")
161183
raise "Fortio benchmark failed" unless system("#{fortio_cmd} | tee #{fortio_txt}")
162184

@@ -180,29 +202,29 @@ def server_responding?(uri)
180202
puts "Error: #{error.message}"
181203
failure_metrics(error)
182204
end
183-
184-
add_summary_line("Fortio", *fortio_metrics)
185205
end
186206

187-
# Vegeta
188-
if TOOLS.include?("vegeta")
189-
vegeta_metrics = begin
190-
puts "\n===> Vegeta"
207+
# Benchmark a single route with Vegeta
208+
def run_vegeta_benchmark(target, route_name)
209+
return nil unless TOOLS.include?("vegeta")
210+
211+
begin
212+
puts "\n===> Vegeta: #{route_name}"
191213

192-
vegeta_bin = "#{OUTDIR}/vegeta.bin"
193-
vegeta_json = "#{OUTDIR}/vegeta.json"
194-
vegeta_txt = "#{OUTDIR}/vegeta.txt"
214+
vegeta_bin = "#{OUTDIR}/#{route_name}_vegeta.bin"
215+
vegeta_json = "#{OUTDIR}/#{route_name}_vegeta.json"
216+
vegeta_txt = "#{OUTDIR}/#{route_name}_vegeta.txt"
195217

196218
# Configure Vegeta arguments
197219
vegeta_args =
198-
if is_max_rate
220+
if IS_MAX_RATE
199221
["-rate=infinity", "--workers=#{CONNECTIONS}", "--max-workers=#{CONNECTIONS}"]
200222
else
201223
["-rate=#{RATE}", "--workers=#{CONNECTIONS}", "--max-workers=#{MAX_CONNECTIONS}"]
202224
end
203225

204226
vegeta_cmd = [
205-
"echo 'GET #{TARGET}' |",
227+
"echo 'GET #{target}' |",
206228
"vegeta", "attack",
207229
*vegeta_args,
208230
"-duration=#{DURATION}",
@@ -212,7 +234,6 @@ def server_responding?(uri)
212234
raise "Vegeta report generation failed" unless system("vegeta report -type=json #{vegeta_bin} > #{vegeta_json}")
213235

214236
vegeta_data = parse_json_file(vegeta_json, "Vegeta")
215-
# .throughput is successful_reqs/total_period, .rate is all_requests/attack_period
216237
vegeta_rps = vegeta_data["throughput"]&.round(2) || "missing"
217238
vegeta_p50 = vegeta_data.dig("latencies", "50th")&./(1_000_000.0)&.round(2) || "missing"
218239
vegeta_p90 = vegeta_data.dig("latencies", "90th")&./(1_000_000.0)&.round(2) || "missing"
@@ -224,22 +245,22 @@ def server_responding?(uri)
224245
puts "Error: #{error.message}"
225246
failure_metrics(error)
226247
end
227-
228-
add_summary_line("Vegeta", *vegeta_metrics)
229248
end
230249

231-
# k6
232-
if TOOLS.include?("k6")
233-
k6_metrics = begin
234-
puts "\n===> k6"
250+
# Benchmark a single route with k6
251+
def run_k6_benchmark(target, route_name)
252+
return nil unless TOOLS.include?("k6")
253+
254+
begin
255+
puts "\n===> k6: #{route_name}"
235256

236-
k6_script_file = "#{OUTDIR}/k6_test.js"
237-
k6_summary_json = "#{OUTDIR}/k6_summary.json"
238-
k6_txt = "#{OUTDIR}/k6.txt"
257+
k6_script_file = "#{OUTDIR}/#{route_name}_k6_test.js"
258+
k6_summary_json = "#{OUTDIR}/#{route_name}_k6_summary.json"
259+
k6_txt = "#{OUTDIR}/#{route_name}_k6.txt"
239260

240261
# Configure k6 scenarios
241262
k6_scenarios =
242-
if is_max_rate
263+
if IS_MAX_RATE
243264
<<~JS.strip
244265
{
245266
max_rate: {
@@ -273,11 +294,9 @@ def server_responding?(uri)
273294
};
274295
275296
export default function () {
276-
const response = http.get('#{TARGET}', { timeout: '#{REQUEST_TIMEOUT}' });
297+
const response = http.get('#{target}', { timeout: '#{REQUEST_TIMEOUT}' });
277298
check(response, {
278299
'status=200': r => r.status === 200,
279-
// you can add more if needed:
280-
// 'status=500': r => r.status === 500,
281300
});
282301
}
283302
JS
@@ -294,8 +313,6 @@ def server_responding?(uri)
294313
# Status: compute successful vs failed requests
295314
k6_reqs_total = k6_data.dig("metrics", "http_reqs", "count") || 0
296315
k6_checks = k6_data.dig("root_group", "checks") || {}
297-
# Extract status code from check name (e.g., "status=200" -> "200")
298-
# Handle both "status=XXX" format and other potential formats
299316
k6_status_parts = k6_checks.map do |name, check|
300317
status_label = name.start_with?("status=") ? name.delete_prefix("status=") : name
301318
"#{status_label}=#{check['passes']}"
@@ -310,8 +327,44 @@ def server_responding?(uri)
310327
puts "Error: #{error.message}"
311328
failure_metrics(error)
312329
end
330+
end
331+
332+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
333+
334+
# Initialize summary file
335+
File.write(SUMMARY_TXT, "")
336+
add_summary_line("Route", "Tool", "RPS", "p50(ms)", "p90(ms)", "p99(ms)", "Status")
337+
338+
# Run benchmarks for each route
339+
routes.each do |route|
340+
separator = "=" * 80
341+
puts "\n#{separator}"
342+
puts "Benchmarking route: #{route}"
343+
puts separator
344+
345+
target = URI.parse("http://#{BASE_URL}#{route}")
346+
347+
# Warm up server for this route
348+
puts "Warming up server for #{route} with 10 requests..."
349+
10.times do
350+
server_responding?(target)
351+
sleep 0.5
352+
end
353+
puts "Warm-up complete for #{route}"
354+
355+
# Sanitize route name for filenames
356+
route_name = route.gsub(%r{^/}, "").tr("/", "_")
357+
route_name = "root" if route_name.empty?
358+
359+
# Run each benchmark tool
360+
fortio_metrics = run_fortio_benchmark(target, route_name)
361+
add_summary_line(route, "Fortio", *fortio_metrics) if fortio_metrics
362+
363+
vegeta_metrics = run_vegeta_benchmark(target, route_name)
364+
add_summary_line(route, "Vegeta", *vegeta_metrics) if vegeta_metrics
313365

314-
add_summary_line("k6", *k6_metrics)
366+
k6_metrics = run_k6_benchmark(target, route_name)
367+
add_summary_line(route, "k6", *k6_metrics) if k6_metrics
315368
end
316369

317370
puts "\nSummary saved to #{SUMMARY_TXT}"

0 commit comments

Comments
 (0)