2424TOOLS = ENV . fetch ( "TOOLS" , "fortio,vegeta,k6" ) . split ( "," )
2525
2626OUTDIR = "bench_results"
27- FORTIO_JSON = "#{ OUTDIR } /fortio.json" . freeze
28- FORTIO_TXT = "#{ OUTDIR } /fortio.txt" . freeze
29- VEGETA_BIN = "#{ OUTDIR } /vegeta.bin" . freeze
30- VEGETA_JSON = "#{ OUTDIR } /vegeta.json" . freeze
31- VEGETA_TXT = "#{ OUTDIR } /vegeta.txt" . freeze
32- K6_TEST_JS = "#{ OUTDIR } /k6_test.js" . freeze
33- K6_SUMMARY_JSON = "#{ OUTDIR } /k6_summary.json" . freeze
34- K6_TXT = "#{ OUTDIR } /k6.txt" . freeze
3527SUMMARY_TXT = "#{ OUTDIR } /summary.txt" . freeze
3628
3729# Validate input parameters
@@ -73,11 +65,6 @@ def parse_json_file(file_path, tool_name)
7365
7466raise "MAX_CONNECTIONS (#{ MAX_CONNECTIONS } ) must be >= CONNECTIONS (#{ CONNECTIONS } )" if MAX_CONNECTIONS < CONNECTIONS
7567
76- # Precompute checks for each tool
77- run_fortio = TOOLS . include? ( "fortio" )
78- run_vegeta = TOOLS . include? ( "vegeta" )
79- run_k6 = TOOLS . include? ( "k6" )
80-
8168# Check required tools are installed
8269required_tools = TOOLS + %w[ column tee ]
8370required_tools . each do |cmd |
@@ -126,103 +113,43 @@ def server_responding?(uri)
126113
127114FileUtils . mkdir_p ( OUTDIR )
128115
129- # Configure tool-specific arguments
130- if RATE == "max"
131- if CONNECTIONS != MAX_CONNECTIONS
132- raise "For RATE=max, CONNECTIONS must be equal to MAX_CONNECTIONS (got #{ CONNECTIONS } and #{ MAX_CONNECTIONS } )"
133- end
134-
135- fortio_args = [ "-qps" , 0 , "-c" , CONNECTIONS ]
136- vegeta_args = [ "-rate=infinity" , "--workers=#{ CONNECTIONS } " , "--max-workers=#{ CONNECTIONS } " ]
137- k6_scenarios = <<~JS . strip
138- {
139- max_rate: {
140- executor: 'constant-vus',
141- vus: #{ CONNECTIONS } ,
142- duration: '#{ DURATION } '
143- }
144- }
145- JS
146- else
147- fortio_args = [ "-qps" , RATE , "-uniform" , "-nocatchup" , "-c" , CONNECTIONS ]
148- vegeta_args = [ "-rate=#{ RATE } " , "--workers=#{ CONNECTIONS } " , "--max-workers=#{ MAX_CONNECTIONS } " ]
149- k6_scenarios = <<~JS . strip
150- {
151- constant_rate: {
152- executor: 'constant-arrival-rate',
153- rate: #{ RATE } ,
154- timeUnit: '1s',
155- duration: '#{ DURATION } ',
156- preAllocatedVUs: #{ CONNECTIONS } ,
157- maxVUs: #{ MAX_CONNECTIONS }
158- }
159- }
160- JS
161- end
162-
163- # Run Fortio
164- if run_fortio
165- puts "===> Fortio"
166- # TODO: https://github.com/fortio/fortio/wiki/FAQ#i-want-to-get-the-best-results-what-flags-should-i-pass
167- fortio_cmd = [
168- "fortio" , "load" ,
169- *fortio_args ,
170- "-t" , DURATION ,
171- "-timeout" , REQUEST_TIMEOUT ,
172- "-json" , FORTIO_JSON ,
173- TARGET
174- ] . join ( " " )
175- raise "Fortio benchmark failed" unless system ( "#{ fortio_cmd } | tee #{ FORTIO_TXT } " )
116+ # Validate RATE=max constraint
117+ is_max_rate = RATE == "max"
118+ if is_max_rate && CONNECTIONS != MAX_CONNECTIONS
119+ raise "For RATE=max, CONNECTIONS must be equal to MAX_CONNECTIONS (got #{ CONNECTIONS } and #{ MAX_CONNECTIONS } )"
176120end
177121
178- # Run Vegeta
179- if run_vegeta
180- puts "\n ===> Vegeta"
181- vegeta_cmd = [
182- "echo" , "'GET #{ TARGET } '" , "|" ,
183- "vegeta" , "attack" ,
184- *vegeta_args ,
185- "-duration=#{ DURATION } " ,
186- "-timeout=#{ REQUEST_TIMEOUT } "
187- ] . join ( " " )
188- raise "Vegeta attack failed" unless system ( "#{ vegeta_cmd } | tee #{ VEGETA_BIN } | vegeta report | tee #{ VEGETA_TXT } " )
189- raise "Vegeta report generation failed" unless system ( "vegeta report -type=json #{ VEGETA_BIN } > #{ VEGETA_JSON } " )
190- end
191-
192- # Run k6
193- if run_k6
194- puts "\n ===> k6"
195- k6_script = <<~JS
196- import http from 'k6/http';
197- import { check } from 'k6';
198-
199- export const options = {
200- scenarios: #{ k6_scenarios } ,
201- };
202-
203- export default function () {
204- const response = http.get('#{ TARGET } ', { timeout: '#{ REQUEST_TIMEOUT } ' });
205- check(response, {
206- 'status=200': r => r.status === 200,
207- // you can add more if needed:
208- // 'status=500': r => r.status === 500,
209- });
210- }
211- JS
212- File . write ( K6_TEST_JS , k6_script )
213- k6_command = "k6 run --summary-export=#{ K6_SUMMARY_JSON } --summary-trend-stats 'min,avg,med,max,p(90),p(99)'"
214- raise "k6 benchmark failed" unless system ( "#{ k6_command } #{ K6_TEST_JS } | tee #{ K6_TXT } " )
215- end
216-
217- puts "\n ===> Parsing results and generating summary"
218-
219122# Initialize summary file
220123File . write ( SUMMARY_TXT , "Tool\t RPS\t p50(ms)\t p90(ms)\t p99(ms)\t Status\n " )
221124
222- # Parse Fortio results
223- if run_fortio
125+ # Fortio
126+ if TOOLS . include? ( "fortio" )
224127 begin
225- fortio_data = parse_json_file ( FORTIO_JSON , "Fortio" )
128+ puts "===> Fortio"
129+
130+ fortio_json = "#{ OUTDIR } /fortio.json"
131+ fortio_txt = "#{ OUTDIR } /fortio.txt"
132+
133+ # Configure Fortio arguments
134+ # See https://github.com/fortio/fortio/wiki/FAQ#i-want-to-get-the-best-results-what-flags-should-i-pass
135+ fortio_args =
136+ if is_max_rate
137+ [ "-qps" , 0 , "-c" , CONNECTIONS ]
138+ else
139+ [ "-qps" , RATE , "-uniform" , "-nocatchup" , "-c" , CONNECTIONS ]
140+ end
141+
142+ fortio_cmd = [
143+ "fortio" , "load" ,
144+ *fortio_args ,
145+ "-t" , DURATION ,
146+ "-timeout" , REQUEST_TIMEOUT ,
147+ "-json" , fortio_json ,
148+ TARGET
149+ ] . join ( " " )
150+ raise "Fortio benchmark failed" unless system ( "#{ fortio_cmd } | tee #{ fortio_txt } " )
151+
152+ fortio_data = parse_json_file ( fortio_json , "Fortio" )
226153 fortio_rps = fortio_data [ "ActualQPS" ] &.round ( 2 ) || "missing"
227154
228155 percentiles = fortio_data . dig ( "DurationHistogram" , "Percentiles" ) || [ ]
@@ -247,10 +174,34 @@ def server_responding?(uri)
247174 end
248175end
249176
250- # Parse Vegeta results
251- if run_vegeta
177+ # Vegeta
178+ if TOOLS . include? ( "vegeta" )
252179 begin
253- vegeta_data = parse_json_file ( VEGETA_JSON , "Vegeta" )
180+ puts "\n ===> Vegeta"
181+
182+ vegeta_bin = "#{ OUTDIR } /vegeta.bin"
183+ vegeta_json = "#{ OUTDIR } /vegeta.json"
184+ vegeta_txt = "#{ OUTDIR } /vegeta.txt"
185+
186+ # Configure Vegeta arguments
187+ vegeta_args =
188+ if is_max_rate
189+ [ "-rate=infinity" , "--workers=#{ CONNECTIONS } " , "--max-workers=#{ CONNECTIONS } " ]
190+ else
191+ [ "-rate=#{ RATE } " , "--workers=#{ CONNECTIONS } " , "--max-workers=#{ MAX_CONNECTIONS } " ]
192+ end
193+
194+ vegeta_cmd = [
195+ "echo 'GET #{ TARGET } ' |" ,
196+ "vegeta" , "attack" ,
197+ *vegeta_args ,
198+ "-duration=#{ DURATION } " ,
199+ "-timeout=#{ REQUEST_TIMEOUT } "
200+ ] . join ( " " )
201+ raise "Vegeta attack failed" unless system ( "#{ vegeta_cmd } | tee #{ vegeta_bin } | vegeta report | tee #{ vegeta_txt } " )
202+ raise "Vegeta report generation failed" unless system ( "vegeta report -type=json #{ vegeta_bin } > #{ vegeta_json } " )
203+
204+ vegeta_data = parse_json_file ( vegeta_json , "Vegeta" )
254205 # .throughput is successful_reqs/total_period, .rate is all_requests/attack_period
255206 vegeta_rps = vegeta_data [ "throughput" ] &.round ( 2 ) || "missing"
256207 vegeta_p50 = vegeta_data . dig ( "latencies" , "50th" ) &./( 1_000_000.0 ) &.round ( 2 ) || "missing"
@@ -271,10 +222,64 @@ def server_responding?(uri)
271222 end
272223end
273224
274- # Parse k6 results
275- if run_k6
225+ # k6
226+ if TOOLS . include? ( "k6" )
276227 begin
277- k6_data = parse_json_file ( K6_SUMMARY_JSON , "k6" )
228+ puts "\n ===> k6"
229+
230+ k6_script_file = "#{ OUTDIR } /k6_test.js"
231+ k6_summary_json = "#{ OUTDIR } /k6_summary.json"
232+ k6_txt = "#{ OUTDIR } /k6.txt"
233+
234+ # Configure k6 scenarios
235+ k6_scenarios =
236+ if is_max_rate
237+ <<~JS . strip
238+ {
239+ max_rate: {
240+ executor: 'constant-vus',
241+ vus: #{ CONNECTIONS } ,
242+ duration: '#{ DURATION } '
243+ }
244+ }
245+ JS
246+ else
247+ <<~JS . strip
248+ {
249+ constant_rate: {
250+ executor: 'constant-arrival-rate',
251+ rate: #{ RATE } ,
252+ timeUnit: '1s',
253+ duration: '#{ DURATION } ',
254+ preAllocatedVUs: #{ CONNECTIONS } ,
255+ maxVUs: #{ MAX_CONNECTIONS }
256+ }
257+ }
258+ JS
259+ end
260+
261+ k6_script = <<~JS
262+ import http from 'k6/http';
263+ import { check } from 'k6';
264+
265+ export const options = {
266+ scenarios: #{ k6_scenarios } ,
267+ };
268+
269+ export default function () {
270+ const response = http.get('#{ TARGET } ', { timeout: '#{ REQUEST_TIMEOUT } ' });
271+ check(response, {
272+ 'status=200': r => r.status === 200,
273+ // you can add more if needed:
274+ // 'status=500': r => r.status === 500,
275+ });
276+ }
277+ JS
278+ File . write ( k6_script_file , k6_script )
279+ k6_command = "k6 run --summary-export=#{ k6_summary_json } --summary-trend-stats 'min,avg,med,max,p(90),p(99)'"
280+ raise "k6 benchmark failed" unless system ( "#{ k6_command } #{ k6_script_file } | tee #{ k6_txt } " )
281+
282+ k6_data = parse_json_file ( k6_summary_json , "k6" )
278283 k6_rps = k6_data . dig ( "metrics" , "iterations" , "rate" ) &.round ( 2 ) || "missing"
279284 k6_p50 = k6_data . dig ( "metrics" , "http_req_duration" , "med" ) &.round ( 2 ) || "missing"
280285 k6_p90 = k6_data . dig ( "metrics" , "http_req_duration" , "p(90)" ) &.round ( 2 ) || "missing"
0 commit comments