11#!/usr/bin/env ruby
22# frozen_string_literal: true
33
4+ require "English"
45require "json"
56require "fileutils"
67require "net/http"
78require "uri"
89
910# Benchmark parameters
11+ PRO = ENV . fetch ( "PRO" , "false" ) == "true"
12+ APP_DIR = PRO ? "react_on_rails_pro/spec/dummy" : "spec/dummy"
1013BASE_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
1415RATE = ENV . fetch ( "RATE" , "50" )
1516# concurrent connections/virtual users
@@ -67,6 +68,30 @@ def add_summary_line(*parts)
6768 end
6869end
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+
7095validate_rate ( RATE )
7196validate_positive_integer ( CONNECTIONS , "CONNECTIONS" )
7297validate_positive_integer ( MAX_CONNECTIONS , "MAX_CONNECTIONS" )
@@ -83,6 +108,8 @@ def add_summary_line(*parts)
83108
84109puts <<~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
106133TIMEOUT_SEC = 60
134+ puts "Checking server availability at #{ BASE_URL } ..."
135+ test_uri = URI . parse ( "http://#{ BASE_URL } #{ routes . first } " )
107136start_time = Time . now
108137loop 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
114143end
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
124146FileUtils . 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 } )"
130152end
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 )
185205end
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 )
229248end
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
315368end
316369
317370puts "\n Summary saved to #{ SUMMARY_TXT } "
0 commit comments