ppbench-locked 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ module Ppbench
2
+ VERSION = "0.0.4"
3
+ end
data/lib/ppbench.rb ADDED
@@ -0,0 +1,546 @@
1
+ require "ppbench/version"
2
+ require "parallel"
3
+ require "csv"
4
+ require "httpclient"
5
+ require "progressbar"
6
+ require "thread"
7
+ require "json"
8
+ require "timeout"
9
+ require "descriptive_statistics"
10
+
11
+ module Ppbench
12
+
13
+ def self.naming=(json)
14
+ @naming = json
15
+ end
16
+
17
+ def self.machine(key)
18
+ return key if @naming.empty?
19
+ return key unless @naming.key?('machines')
20
+ name = @naming['machines'][key]
21
+ name == nil ? key : name
22
+ end
23
+
24
+ def self.experiment(key)
25
+ return key if @naming.empty?
26
+ return key unless @naming.key?('experiments')
27
+ name = @naming['experiments'][key]
28
+ name == nil ? key : name
29
+ end
30
+
31
+ def self.precision=(v)
32
+ @precision = v
33
+ end
34
+
35
+ def self.precision
36
+ @precision
37
+ end
38
+
39
+ def self.alpha=(v)
40
+ @alpha = v
41
+ end
42
+
43
+ def self.alpha
44
+ @alpha
45
+ end
46
+
47
+ def self.precision_error(length)
48
+ """
49
+ Sorry, we have not enough data for messages of about #{length} byte length.
50
+ You may want to reduce the precision with the global --precision flag.
51
+ Current precison is #{Ppbench::precision}.
52
+ So you could collect more data (preferred) or reduce the precision value.
53
+ """
54
+ end
55
+
56
+ R_COLORS = [
57
+ '0.5,0.5,0.5',
58
+ '0.96,0.26,0.21',
59
+ '0.25,0.31,0.71',
60
+ '0.13,0.59,0.95',
61
+ '0,0.59,0.53',
62
+ '0.30,0.69,0.31',
63
+ '0.8,0.86,0.22',
64
+ '1,0.6,0.03',
65
+ '1,0.6,0',
66
+ '1,0.34,0.13'
67
+ ]
68
+
69
+ R_NO_SYMBOL = "16"
70
+
71
+ R_SYMBOLS = "c(1,2,3,4,5,6,7,8,9,10)"
72
+
73
+ LOG_HEADER = [
74
+ "Machine Tag",
75
+ "Experiment Tag",
76
+ "Document Path",
77
+ "Failed requests",
78
+ "Concurrency Level",
79
+ "Total transferred",
80
+ "Time per request",
81
+ "Transfer rate",
82
+ "Requests per second",
83
+ "Retries",
84
+ "Response Code"
85
+ ]
86
+
87
+ # Runs a benchmark against a host and stores benchmark data in a log file.
88
+ #
89
+ def self.run_bench(host, log, machine_tag: '', experiment_tag: '', timeout: 60, repetitions: 10, coverage: 0.1, min: 1, max: 500000, concurrency: 10)
90
+ rounds = ((max - min) * coverage).to_i
91
+
92
+ CSV.open(log, 'w', write_headers: true, headers: Ppbench::LOG_HEADER, force_quotes: true) do |logger|
93
+
94
+ logfile = Mutex.new
95
+ progress = ProgressBar.new("Running", rounds)
96
+
97
+ webclient = HTTPClient.new
98
+
99
+ Parallel.each(1.upto(rounds), in_threads: concurrency) do |_|
100
+
101
+ length = Random.rand(min..max)
102
+ document = "/mping/#{length}"
103
+
104
+ results = {
105
+ duration: [],
106
+ length: [],
107
+ code: [],
108
+ retries: [],
109
+ fails: []
110
+ }
111
+ begin
112
+ #uri = URI("#{host}#{document}")
113
+ 1.upto(repetitions) do
114
+ answer = {}
115
+ Timeout::timeout(timeout) do
116
+ response = webclient.get("#{host}#{document}").body
117
+ answer = JSON.parse(response)
118
+ end
119
+ results[:duration] << answer['duration']
120
+ results[:length] << answer['length']
121
+ results[:code] << answer['code']
122
+ results[:retries] << answer['retries']
123
+ results[:fails] << (answer['code'] == 200 ? 0 : 1)
124
+ end
125
+ rescue Exception => e
126
+ print ("Timeout of '#{host}#{document}'")
127
+ print ("#{e}")
128
+ end
129
+
130
+ unless results[:duration].empty?
131
+ time_taken = results[:duration].mean # in milliseconds
132
+ length = results[:length].median # message length
133
+ transfer_rate = results[:length].sum * 1000 / results[:duration].sum
134
+ code = results[:code].first # HTTP response code
135
+ retries = results[:retries].sum # Amount of retries
136
+ failed = results[:fails].sum # Amount of fails
137
+
138
+ requests_per_second = 1000 / time_taken
139
+
140
+ logfile.synchronize do
141
+ progress.inc
142
+
143
+ logger << [
144
+ "#{machine_tag}",
145
+ "#{experiment_tag}",
146
+ "#{document}",
147
+ "#{failed}",
148
+ "#{concurrency}",
149
+ "#{length}",
150
+ "#{time_taken}",
151
+ "#{transfer_rate}",
152
+ "#{requests_per_second}",
153
+ "#{retries}",
154
+ "#{code}"
155
+ ]
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ # Load CSV files and conversion to better analyzable format (List of hashes)
163
+ #
164
+ def self.load_data(files)
165
+ files.map do |file|
166
+ rows = CSV.read(file, headers: true)
167
+
168
+ rows.map do |row|
169
+ {
170
+ :experiment => row.key?('Experiment Tag') ? row['Experiment Tag'] : nil,
171
+ :machine => row.key?('Machine Tag') ? row['Machine Tag'] : nil,
172
+ :document => row.key?('Document Path') ? row['Document Path'] : nil,
173
+ :length => row.key?('Total transferred') ? row['Total transferred'].to_i : nil,
174
+ :failed => row.key?('Failed requests') ? row['Failed requests'].to_i : nil,
175
+ :tpr => row.key?('Time per request') ? row['Time per request'].to_f : nil,
176
+ :transfer_rate => row.key?('Transfer rate') ? row['Transfer rate'].to_f : nil,
177
+ :rps => row.key?('Requests per second') ? row['Requests per second'].to_f : nil,
178
+ :retries => row.key?('Retries') ? row['Retries'].to_i : nil,
179
+ :response_code => row.key?('Response Code') ? row['Response Code'].to_i : nil
180
+ }
181
+ end
182
+ end.flatten
183
+ end
184
+
185
+ # Filter benchmark data.
186
+ #
187
+ def self.filter(data, maxsize: 2 ** 64, experiments: [], machines: [], fails: 0)
188
+ data.select { |entry| entry[:tpr] > 0 }
189
+ .select { |entry| entry[:failed] <= fails }
190
+ .select { |entry| entry[:length] <= maxsize }
191
+ .select { |entry| machines.include?(entry[:machine]) || machines.empty? }
192
+ .select { |entry| experiments.include?(entry[:experiment]) || experiments.empty? }
193
+ end
194
+
195
+ # Aggregate benchmark data.
196
+ # {
197
+ # 'weave': {
198
+ # 'm.large': [{ machine: String, experiment: String, document: String, length: value, tpr: Integer, ... }]
199
+ # }, ...
200
+ # },
201
+ # 'docker': { ... },
202
+ # 'bare': { ... }
203
+ # }
204
+ def self.aggregate(data)
205
+ experiments = data.group_by { |entry| entry[:experiment] }
206
+ experiments.map do |experiment, values|
207
+ machines = values.group_by { |entry| entry[:machine] }
208
+ [
209
+ experiment,
210
+ machines
211
+ ]
212
+ end.to_h
213
+ end
214
+
215
+ # Determines biggest value of aggregated data.
216
+ #
217
+ def self.maximum(data, of: :tpr)
218
+ y = 0
219
+ for experiment, machines in data
220
+ for machine, values in machines
221
+ m = values.max_by { |e| e[of] }
222
+ y = (y > m[of] ? y : m[of])
223
+ end
224
+ end
225
+ y
226
+ end
227
+
228
+ # Prepares a plot to present absolute values.
229
+ #
230
+ def self.prepare_plot(
231
+ maxy,
232
+ receive_window: 87380,
233
+ length: 500000,
234
+ xaxis_title: "Message Length",
235
+ xaxis_unit: "kB",
236
+ yaxis_title: "Transfer Rate",
237
+ yaxis_unit: "MB/sec",
238
+ title: "Data Transfer Rates",
239
+ subtitle: ""
240
+ )
241
+ recwindow = receive_window == 0 ? '' : "abline(v = seq(#{receive_window}, #{length}, by=#{receive_window}), lty='dashed')"
242
+
243
+ """
244
+ plot(x=c(0), y=c(0), xlim=c(0, #{length}), ylim=c(0, #{maxy}), main='#{title}\\n(#{subtitle})', xlab='#{xaxis_title} (#{xaxis_unit})', ylab='#{yaxis_title} (#{yaxis_unit})', xaxt='n', yaxt='n', pch=NA)
245
+ #{recwindow if receive_window < length }
246
+ """
247
+ end
248
+
249
+ # Prepares a plot to present relative comparisons.
250
+ #
251
+ def self.prepare_comparisonplot(
252
+ maxy,
253
+ receive_window: 87300,
254
+ length: 50000,
255
+ xaxis_title: "Message Length (kB)",
256
+ xaxis_unit: "kB",
257
+ yaxis_title: "Relative performance compared with reference experiment (%)",
258
+ yaxis_unit: "%",
259
+ title: "Relative performance (Data Transfer Rate)",
260
+ subtitle: ""
261
+ )
262
+ recwindow = receive_window == 0 ? '' : "abline(v = seq(#{receive_window}, #{length}, by=#{receive_window}), lty='dashed')"
263
+
264
+ """
265
+ plot(x=c(0), y=c(0), xlim=c(0, #{length}), ylim=c(0, #{maxy}), main='#{title}\\n(#{subtitle})', xlab='#{xaxis_title} (#{xaxis_unit})', ylab='#{yaxis_title} (#{yaxis_unit})', xaxt='n', yaxt='n', pch=NA)
266
+ #{recwindow if receive_window < length}
267
+ """
268
+ end
269
+
270
+ # Adds a serie to a plot.
271
+ #
272
+ def self.add_series(
273
+ data,
274
+ to_plot: :tpr,
275
+ color: 'grey',
276
+ symbol: 1,
277
+ alpha: Ppbench::alpha,
278
+ length: 500000,
279
+ confidence: 90,
280
+ no_points: false,
281
+ with_bands: false
282
+ )
283
+ """
284
+ #{points(data, to_plot: to_plot, color: color, symbol: symbol, alpha: alpha) unless no_points }
285
+ #{bands(data, to_plot: to_plot, color: color, length: length, confidence: confidence) if with_bands }
286
+ """
287
+ end
288
+
289
+ # Adds a compare line to a comparison plot.
290
+ #
291
+ def self.add_comparisonplot(
292
+ reference,
293
+ serie,
294
+ to_plot: :tpr,
295
+ color: 'grey',
296
+ symbol: 1,
297
+ length: 500000,
298
+ n: Ppbench::precision,
299
+ nknots: Ppbench::precision
300
+ )
301
+ step = length / n
302
+ references = reference.map { |v| [v[:length], v[to_plot]] }
303
+ ref_values = 1.upto(n).map do |i|
304
+ vs = references.select { |p| p[0] < i * step && p[0] >= (i - 1) * step }.map { |p| p[1] }
305
+
306
+ if vs.empty?
307
+ $stderr.puts precision_error(i * step)
308
+ exit!
309
+ end
310
+
311
+ [
312
+ i * step,
313
+ vs.median
314
+ ]
315
+ end.to_h
316
+
317
+ series = serie.map { |v| [v[:length], v[to_plot]] }
318
+ serie_values = 1.upto(n).map do |i|
319
+ vs = series.select { |p| p[0] < i * step && p[0] >= (i - 1) * step }.map { |p| p[1] }
320
+
321
+ if vs.empty?
322
+ $stderr.puts precision_error(i * step)
323
+ exit!
324
+ end
325
+
326
+ [
327
+ i * step,
328
+ vs.median
329
+ ]
330
+ end.to_h
331
+
332
+ xs = []
333
+ ys = []
334
+
335
+ ref_values.each do |x, y|
336
+ if serie_values.key? x
337
+ xs << x
338
+ ys << serie_values[x] / y
339
+ end
340
+ end
341
+
342
+ """
343
+ xs=c(#{ xs * ',' })
344
+ ys=c(#{ ys * ',' })
345
+ median <- smooth.spline(xs, ys, nknots=#{nknots})
346
+ lines(median, lwd=2, col=rgb(#{color}))
347
+ """
348
+ end
349
+
350
+ # Generates scatter plot of points for plots.
351
+ #
352
+ def self.points(data, to_plot: :tpr, color: 'grey', alpha: Ppbench::alpha, symbol: 1)
353
+ points = data.map { |v| [v[:length], v[to_plot]] }
354
+ xs = "c(#{points.map { |e| e[0] } * ','})"
355
+ ys = "c(#{points.map { |e| e[1] } * ','})"
356
+
357
+ """
358
+ xs = #{xs}
359
+ ys = #{ys}
360
+ points(x=xs,y=ys, col=rgb(#{color},alpha=#{ alpha }), pch=#{ symbol })
361
+ """
362
+ end
363
+
364
+ # Generates median lines and confidence bands for plots.
365
+ #
366
+ def self.bands(data, to_plot: :tpr, n: Ppbench::precision, length: 500000, color: 'grey', confidence: 90, nknots: Ppbench::precision)
367
+
368
+ step = length / n
369
+ points = data.map { |v| [v[:length], v[to_plot]] }
370
+ values = 1.upto(n).map do |i|
371
+ [
372
+ i * step,
373
+ points.select { |p| p[0] < i * step && p[0] >= (i - 1) * step }.map { |p| p[1] }
374
+ ]
375
+ end
376
+
377
+ upper_confidence = 100 - (100 - confidence) / 2
378
+ semi_upper_confidence = 100 - (100 - confidence / 2) / 2
379
+ lower_confidence = (100 - confidence) / 2
380
+ semi_lower_confidence = (100 - confidence / 2) / 2
381
+
382
+ summary = values.map do |x,vs|
383
+
384
+ if vs.empty?
385
+ $stderr.puts precision_error(x)
386
+ exit!
387
+ end
388
+
389
+ {
390
+ :x => x,
391
+ :lower => vs.percentile(lower_confidence),
392
+ :semi_lower => vs.percentile(semi_lower_confidence),
393
+ :median => vs.median,
394
+ :semi_upper => vs.percentile(semi_upper_confidence),
395
+ :upper => vs.percentile(upper_confidence)
396
+ }
397
+ end
398
+
399
+ xs = "c(#{summary.map { |v| v[:x] } * ','})"
400
+ medians = "c(#{summary.map { |v| v[:median] } * ','})"
401
+ lowers = "c(#{summary.map { |v| v[:lower] } * ','})"
402
+ semi_lowers = "c(#{summary.map { |v| v[:semi_lower] } * ','})"
403
+ uppers = "c(#{summary.map { |v| v[:upper] } * ','})"
404
+ semi_uppers = "c(#{summary.map { |v| v[:semi_upper] } * ','})"
405
+
406
+ """
407
+ xs = #{xs}
408
+ medians = #{medians}
409
+ lowers = #{lowers}
410
+ semi_lowers = #{semi_lowers}
411
+ uppers = #{uppers}
412
+ semi_uppers = #{semi_uppers}
413
+
414
+ low <- smooth.spline(xs, lowers, nknots=#{nknots})
415
+ semi_low <- smooth.spline(xs, semi_lowers, nknots=#{nknots})
416
+ up <- smooth.spline(xs, uppers, nknots=#{nknots})
417
+ semi_up <- smooth.spline(xs, semi_uppers, nknots=#{nknots})
418
+ median <- smooth.spline(xs, medians, nknots=#{nknots})
419
+ polygon(c(low$x, rev(up$x)), c(low$y, rev(up$y)), col = rgb(#{color},alpha=0.10), border=NA)
420
+ polygon(c(semi_low$x, rev(semi_up$x)), c(semi_low$y, rev(semi_up$y)), col = rgb(#{color},alpha=0.15), border=NA)
421
+ lines(median, lwd=2, col=rgb(#{color}))
422
+ lines(low, col=rgb(#{color},alpha=0.50), lty='dashed', lwd=0.5)
423
+ lines(up, col=rgb(#{color},alpha=0.50), lty='dashed', lwd=0.5)
424
+ """
425
+
426
+
427
+ end
428
+
429
+ # Generates an R plot output script which can be used for plotting benchmark data
430
+ # as scatter plot with optional confidence bands.
431
+ #
432
+ def self.plotter(
433
+ data,
434
+ to_plot: :tpr,
435
+ machines: [],
436
+ experiments: [],
437
+ receive_window: 87380,
438
+ xaxis_max: 500000,
439
+ confidence: 90,
440
+ no_points: false,
441
+ with_bands: false,
442
+ yaxis_max: 10000000,
443
+ yaxis_steps: 10,
444
+ xaxis_steps: 10,
445
+ xaxis_title: "",
446
+ xaxis_unit: "",
447
+ xaxis_divisor: 1000,
448
+ yaxis_title: "",
449
+ yaxis_unit: "",
450
+ yaxis_divisor: 1000000,
451
+ title: "",
452
+ subtitle: "",
453
+ legend_position: "topright"
454
+ )
455
+ series_data = []
456
+ series_names = []
457
+ series_colors = R_COLORS
458
+
459
+ for exp in experiments
460
+ for machine in machines
461
+ if (data.include? exp) && (data[exp].include? machine)
462
+ series_data << data[exp][machine]
463
+ series_names << "'#{Ppbench::experiment(exp)} on #{Ppbench::machine(machine)}'"
464
+ end
465
+ end
466
+ end
467
+
468
+ colors = "c(#{series_colors.map { |c| "rgb(#{c})" } * ','})"
469
+
470
+ sym = 1;
471
+ r = "#{prepare_plot(yaxis_max, receive_window: receive_window, length: xaxis_max, title: title, xaxis_title: xaxis_title, xaxis_unit: xaxis_unit, yaxis_title: yaxis_title, yaxis_unit: yaxis_unit, subtitle: subtitle)}\n"
472
+
473
+ for serie in series_data
474
+ r += add_series(serie, to_plot: to_plot, with_bands: with_bands, no_points: no_points, color: series_colors.shift, symbol: sym, length: xaxis_max, confidence: confidence)
475
+ sym = sym + 1
476
+ end
477
+
478
+ symbols = no_points ? R_NO_SYMBOL : R_SYMBOLS
479
+
480
+ r + """
481
+ xa = seq(0, #{xaxis_max}, by=#{xaxis_max/xaxis_steps})
482
+ ya = seq(0, #{yaxis_max}, by=#{yaxis_max/yaxis_steps})
483
+ axis(1, at = xa, labels = paste(xa/#{xaxis_divisor}, '#{xaxis_unit}', sep = ' ' ))
484
+ axis(2, at = ya, labels = paste(ya/#{yaxis_divisor}, '#{yaxis_unit}', sep = ' ' ))
485
+ legend('#{legend_position}', cex=0.9, pch=#{symbols}, col=#{colors}, c(#{series_names * ',' }),box.col=rgb(1,1,1,0), bg=rgb(1,1,1,0.75))
486
+ """
487
+ end
488
+
489
+ # Generates an R plot output script which can be used for plotting comparison plots
490
+ # of benchmark data.
491
+ #
492
+ def self.comparison_plotter(
493
+ data,
494
+ yaxis_max: 1.5,
495
+ to_plot: :transfer_rate,
496
+ machines: [],
497
+ experiments: [],
498
+ receive_window: 87380,
499
+ xaxis_max: 500000,
500
+ xaxis_steps: 10,
501
+ xaxis_title: "",
502
+ xaxis_unit: "",
503
+ xaxis_divisor: 1000,
504
+ yaxis_title: "",
505
+ yaxis_unit: "%",
506
+ title: "",
507
+ subtitle: "",
508
+ legend_position: "topright"
509
+ )
510
+ series_data = []
511
+ series_names = []
512
+ series_colors = R_COLORS
513
+
514
+ ref = true
515
+ for exp in experiments
516
+ for machine in machines
517
+ reference = ref ? 'Reference: ' : ''
518
+ ref = false
519
+ if (data.include? exp) && (data[exp].include? machine)
520
+ series_data << data[exp][machine]
521
+ series_names << "'#{reference}#{Ppbench::experiment(exp)} on #{Ppbench::machine(machine)}'"
522
+ end
523
+ end
524
+ end
525
+
526
+ colors = "c(#{series_colors.map { |c| "rgb(#{c})" } * ','})"
527
+
528
+ sym = 1;
529
+ r = "#{prepare_comparisonplot(yaxis_max, receive_window: receive_window, length: xaxis_max, title: title, subtitle: subtitle, xaxis_title: xaxis_title, xaxis_unit: xaxis_unit, yaxis_title: yaxis_title, yaxis_unit: yaxis_unit)}\n"
530
+
531
+ reference = series_data.first
532
+
533
+ for serie in series_data
534
+ r += add_comparisonplot(reference, serie, to_plot: to_plot, color: series_colors.shift, symbol: sym, length: xaxis_max)
535
+ sym = sym + 1
536
+ end
537
+
538
+ r + """
539
+ xa = seq(0, #{xaxis_max}, by=#{xaxis_max/xaxis_steps})
540
+ ya = seq(0, #{yaxis_max}, by=#{0.1})
541
+ axis(1, at = xa, labels = paste(xa/#{xaxis_divisor}, '#{xaxis_unit}', sep = '' ))
542
+ axis(2, at = ya, labels = paste(ya * 100, '#{yaxis_unit}', sep = '' ))
543
+ legend('#{legend_position}', cex=0.9, pch=c(#{R_NO_SYMBOL}), col=#{colors}, c(#{series_names * ',' }),box.col=rgb(1,1,1,0), bg=rgb(1,1,1,0.75))
544
+ """
545
+ end
546
+ end
data/ppbench.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ppbench/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ppbench-locked"
8
+ spec.version = Ppbench::VERSION
9
+ spec.authors = ["Alexander Kuzmin"]
10
+ spec.email = ["alexander.ivan.kuzmin@gmail.com"]
11
+
12
+ if spec.respond_to?(:metadata)
13
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
14
+ end
15
+
16
+ spec.summary = %q{ppbench-locked - a REST ping pong benchmark}
17
+ spec.description = %q{A tool to trigger ping pong benchmark to investigate HTTP REST performances. This is the same as ppbench but we decided to lock dependency versions when we saw failures appearing out of nowhere.}
18
+ spec.homepage = "https://github.com/nkratzke/pingpong"
19
+ spec.license = "MIT"
20
+
21
+ spec.required_ruby_version = "~> 2.2"
22
+
23
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ spec.bindir = "bin"
25
+ spec.executables = ["ppbench.rb"]
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.8"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_runtime_dependency "commander", "~> 4.4"
31
+ spec.add_runtime_dependency "parallel", "= 1.10.0"
32
+ spec.add_runtime_dependency "progressbar", "= 0.21"
33
+ spec.add_runtime_dependency "descriptive_statistics", "= 2.5.1"
34
+ spec.add_runtime_dependency "terminal-table", "~> 1.7"
35
+ spec.add_runtime_dependency "httpclient", "= 2.8.2.4"
36
+
37
+ end