ppbench 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ppbench.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Nane Kratzke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,39 @@
1
+ # Ppbench
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ppbench`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'ppbench'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install ppbench
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ 1. Fork it ( https://github.com/[my-github-username]/ppbench/fork )
36
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
37
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
38
+ 4. Push to the branch (`git push origin my-new-feature`)
39
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ppbench"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,996 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'rubygems'
5
+
6
+ require 'ppbench'
7
+ require 'commander/import'
8
+ require 'terminal-table'
9
+ require 'json'
10
+
11
+ # Description constants used by the help command.
12
+ #
13
+ MACHINES_DESCRIPTION = 'Consider only specific machines (e.g. m3.large,m3.xlarge); comma separated list.'
14
+ EXPERIMENTS_DESCRIPTION = 'Consider only specific experiments (e.g. bare,docker,weave); comma separated list.'
15
+ RECWINDOW_DESCRIPTION = 'Standard Receive Window. Defaults to 87380 byte (Window is not plotted if set to 0).'
16
+ YAXIS_MAX_DESCRIPTION = 'Maximum Y value on the Y axis (defaults to biggest value found).'
17
+ YAXIS_STEPS_DESCRIPTION = 'How many ticks shall be plotted on yaxis (defaults to 10).'
18
+ XAXIS_MAX_DESCRIPTION = 'Maximum X value on the X axis (defaults to biggest message size found).'
19
+ XAXIS_STEPS_DESCRIPTION = 'How many ticks shall be plotted on xaxis (defaults to 10).'
20
+ PRECISION_DESCRIPTION = 'Amount of points used per series for plotting medians, comparisons and confidence intervals.'
21
+ CONFIDENCE_DESCRIPTION = 'Percent value for confidence bands. Defaults to 90%.'
22
+ WITHBANDS_DESCRIPTION = 'Plots confidence bands (confidence bands are _not_ plotted by default).'
23
+ NOPOINTS_DESCRIPTION = 'Show no points (points are plotted by default).'
24
+ NAMING_DESCRIPTION = 'Use user defined names via an JSON file.'
25
+ PDF_DESCRIPTION = 'Adds additional commands to an R script, so that it can be used to generate a PDF file.'
26
+ PDF_WIDTH_DESCRIPTION = 'Width of plot in inch (defaults to 7 inch, only useful with PDF output).'
27
+ PDF_HEIGHT_DESCRIPTION = 'Height of plot in inch (defaults to 7 inch, only useful with PDF output).'
28
+
29
+ # Plotting constants used for plotting.
30
+ #
31
+ RECWINDOW_DEFAULT = 87380
32
+ CONFIDENCE_DEFAULT = 90
33
+ AXIS_STEP_DEFAULT = 10
34
+ COMPARISON_MAX_DEFAULT = 2.0
35
+
36
+ # Constants used for PDF generation.
37
+ #
38
+ PDF_HEIGHT_WIDTH_DEFAULT = 7
39
+
40
+ program :name, 'ppbench'
41
+ program :version, "#{Ppbench::VERSION}"
42
+ program :description, 'Ping pong benchmark'
43
+ program :help, 'Author', 'Nane Kratzke <nane.kratzke@fh-luebeck.de>'
44
+
45
+ global_option '--precision POINTS', Integer, PRECISION_DESCRIPTION
46
+ global_option '--naming FILE', String, NAMING_DESCRIPTION
47
+
48
+ default_command :help
49
+
50
+ # Validates and processes global options like
51
+ # - precision (used for comparison lines, median lines and confidence band plotting)
52
+ # - naming (used for user defined naming of machine and experiment tags)
53
+ #
54
+ def validate_global_options(args, options)
55
+ options.default :precision => 500
56
+ options.default :naming => ''
57
+
58
+ if options.precision < 20
59
+ $stderr.puts("Error in --precision flag: Precision must be >= 20 points.\n")
60
+ exit!
61
+ end
62
+
63
+ if options.precision < 0
64
+ $stderr.puts("Error in --precision flag: Precision must be >= 1 point.\n")
65
+ exit!
66
+ end
67
+
68
+ Ppbench::precision = options.precision
69
+
70
+ if !options.naming.empty? && !File.exist?(options.naming)
71
+ $stderr.puts("Error in --naming flag: File '#{options.naming}' does not exist.")
72
+ exit!
73
+ end
74
+
75
+ Ppbench::naming = {} if options.naming.empty?
76
+
77
+ unless options.naming.empty?
78
+ begin
79
+ file = File.read(options.naming)
80
+ Ppbench::naming = JSON.parse(file)
81
+ rescue Exception => ex
82
+ $stderr.puts("Error in naming file '#{options.naming}'. Does not seem to be a valid JSON file.")
83
+ exit!
84
+ end
85
+ end
86
+ end
87
+
88
+ # Validates command line flags of the run command.
89
+ #
90
+ def validate_run_options(args, options)
91
+
92
+ if (options.machine.empty?)
93
+ $stderr.puts("You have to tag your benchmark data with the --machine flag.\n")
94
+ exit!
95
+ end
96
+
97
+ if (options.experiment.empty?)
98
+ $stderr.puts("You have to tag your benchmark data with the --experiment flag.\n")
99
+ exit!
100
+ end
101
+
102
+ if options.coverage < 0 || options.coverage > 1.0
103
+ $stderr.puts("Error in --coverage flag: Coverage must be in [0..1.0]\n")
104
+ exit!
105
+ end
106
+
107
+ if options.repetitions < 1
108
+ $stderr.puts("Error in --repetitions flag: Repetitions must be >= 1\n")
109
+ exit!
110
+ end
111
+
112
+ if options.concurrency < 1
113
+ $stderr.puts("Error in --concurrency flag: Concurrency must be >= 1\n")
114
+ exit!
115
+ end
116
+
117
+ if options.timeout < 1
118
+ $stderr.puts("Error in --timeout flag: Timeout must be >= 1 seconds\n")
119
+ exit!
120
+ end
121
+
122
+ if args.empty?
123
+ $stderr.puts("You have to specify a log file.\n")
124
+ exit!
125
+ end
126
+
127
+ if $stderr.args.length > 1
128
+ print("You should only specify one log file. You specified #{args.length} logfiles.\n")
129
+ exit!
130
+ end
131
+
132
+ if File.exist?(args[0])
133
+ $stderr.puts("Logfile #{args[0]} already exists. You do not want to overwrite collected benchmark data.\n")
134
+ exit!
135
+ end
136
+
137
+ end
138
+
139
+ # Validates command line flags of the xyz-comparison-plot commands.
140
+ #
141
+ def validate_comparison_options(args, options)
142
+
143
+ if args.empty?
144
+ $stderr.puts("You have to provide benchmark files (*.csv) to analyze.")
145
+ exit!
146
+ end
147
+
148
+ if options.recwindow < 0
149
+ $stderr.puts("Error in --recwindow flag: TCP standard receive window must be >= 0 bytes.\n")
150
+ exit!
151
+ end
152
+
153
+ if options.yaxis_max < 0
154
+ $stderr.puts("Error in --yaxis_max flag: Maximum value on yaxis must be >= 0.\n")
155
+ end
156
+
157
+ if options.xaxis_max < 0
158
+ $stderr.puts("Error in --xaxis_max flag: Maximum value on xaxis must be >= 0.\n")
159
+ exit!
160
+ end
161
+
162
+ if options.xaxis_steps <= 0
163
+ $stderr.puts("Error in --xaxis_steps flag: You must provide a positive step > 0.\n")
164
+ exit!
165
+ end
166
+
167
+ end
168
+
169
+ # Validates command line flags of the xyz-plot commands.
170
+ #
171
+ def validate_plot_options(args, options)
172
+
173
+ if args.empty?
174
+ $stderr.puts("You have to provide benchmark files (*.csv) to analyze.")
175
+ exit!
176
+ end
177
+
178
+ if options.recwindow < 0
179
+ $stderr.puts("Error in --recwindow flag: TCP standard receive window must be >= 0 bytes\n")
180
+ exit!
181
+ end
182
+
183
+ if options.confidence < 0 || options.confidence > 100
184
+ $stderr.puts("Error in --confidence flag: Confidence interval must be between 0 and 100 %.\n")
185
+ exit!
186
+ end
187
+
188
+ if options.yaxis_max < 0
189
+ $stderr.puts("Error in --yaxis_max flag: Maximum value on yaxis must be >= 0.\n")
190
+ end
191
+
192
+ if options.yaxis_steps <= 0
193
+ $stderr.puts("Error in --yaxis_steps flag: You must provide a positive step > 0.\n")
194
+ exit!
195
+ end
196
+
197
+ if options.xaxis_max < 0
198
+ $stderr.puts("Error in --xaxis_max flag: Maximum value on xaxis must be >= 0.\n")
199
+ exit!
200
+ end
201
+
202
+ if options.xaxis_steps <= 0
203
+ $stderr.puts("Error in --xaxis_steps flag: You must provide a positive step > 0.\n")
204
+ exit!
205
+ end
206
+
207
+ if options.nopoints && !options.withbands
208
+ $stderr.puts("Error in --nopoints flag. You must use --withbands if applying --nopoints. Otherwise nothing would be plotted.\n")
209
+ exit!
210
+ end
211
+
212
+ end
213
+
214
+ # Validates command line options dealing with PDF generation.
215
+ #
216
+ def validate_pdf_options(args, options)
217
+ if options.height < 1
218
+ $stderr.puts("Error in --height flag. You must provide a positive height >= 1 in inch.\n")
219
+ exit!
220
+ end
221
+
222
+ if options.width < 1
223
+ $stderr.puts("Error in --width flag. You must provide a positive width >= 1 in inch.\n")
224
+ exit!
225
+ end
226
+ end
227
+
228
+ # Validates command line options for the naming-template command.
229
+ #
230
+ def validate_naming_options(args, options)
231
+ if args.empty?
232
+ $stderr.puts("Error due to missing files to analyze. You must provide at least one log file (csv).\n")
233
+ exit!
234
+ end
235
+
236
+ if !options.update.empty? && !File.exist?(options.update)
237
+ $stderr.puts("Error: File #{options.update} does not exist.\n")
238
+ exit!
239
+ end
240
+ end
241
+
242
+ # Implements the run command.
243
+ #
244
+ def run(args, options)
245
+
246
+ logfile = args[0]
247
+
248
+ Ppbench::run_bench(
249
+ options.host,
250
+ logfile,
251
+ machine_tag: options.machine,
252
+ experiment_tag: options.experiment,
253
+ coverage: options.coverage,
254
+ min: options.min,
255
+ max: options.max,
256
+ concurrency: options.concurrency,
257
+ repetitions: options.repetitions,
258
+ timeout: options.timeout
259
+ )
260
+ end
261
+
262
+ # Embeds a R script (content) into some PDF generating
263
+ # R statements.
264
+ #
265
+ def pdfout(content, file: 'output.pdf', width: 7, height: 7)
266
+ """
267
+ pdf('#{file}', width=#{width}, height=#{height})
268
+ #{content}
269
+ dev.off()
270
+ """
271
+ end
272
+
273
+ # Implements the transfer-plot command.
274
+ #
275
+ def transfer_plot(args, options)
276
+
277
+ experiments = options.experiments.split(',')
278
+ machines = options.machines.split(',')
279
+
280
+ data = Ppbench::load_data(args)
281
+ filtered_data = Ppbench::filter(
282
+ data,
283
+ experiments: experiments,
284
+ machines: machines
285
+ )
286
+ aggregated_data = Ppbench::aggregate(filtered_data)
287
+
288
+ max_x = Ppbench::maximum(aggregated_data, of: :length)
289
+ max_y = Ppbench::maximum(aggregated_data, of: :transfer_rate)
290
+
291
+ rplot = Ppbench::plotter(
292
+ aggregated_data,
293
+ to_plot: :transfer_rate,
294
+ machines: machines,
295
+ experiments: experiments,
296
+ receive_window: options.recwindow,
297
+ xaxis_max: options.xaxis_max == 0 ? max_x : options.xaxis_max,
298
+ confidence: options.confidence,
299
+ no_points: options.nopoints,
300
+ with_bands: options.withbands,
301
+ yaxis_max: options.yaxis_max == 0 ? max_y : options.yaxis_max,
302
+ yaxis_steps: options.yaxis_steps,
303
+ xaxis_steps: options.xaxis_steps,
304
+ title: "Data Transfer Rates",
305
+ subtitle: "bigger is better",
306
+ xaxis_title: "Message Size",
307
+ xaxis_unit: "kB",
308
+ yaxis_title: "Transfer Rate",
309
+ yaxis_unit: "MB/sec"
310
+ )
311
+
312
+ pdf = pdfout(rplot, file: options.pdf, width: options.width, height: options.height)
313
+ print("#{pdf}") unless options.pdf.empty?
314
+ print("#{rplot}") if options.pdf.empty?
315
+ end
316
+
317
+ # Implements the transfer-comparison-plot command.
318
+ #
319
+ def transfer_comparison_plot(args, options)
320
+
321
+ experiments = options.experiments.split(',')
322
+ machines = options.machines.split(',')
323
+
324
+ data = Ppbench::load_data(args)
325
+ filtered_data = Ppbench::filter(
326
+ data,
327
+ experiments: experiments,
328
+ machines: machines
329
+ )
330
+ aggregated_data = Ppbench::aggregate(filtered_data)
331
+
332
+ max_x = Ppbench::maximum(aggregated_data, of: :length)
333
+
334
+ rplot = Ppbench::comparison_plotter(
335
+ aggregated_data,
336
+ yaxis_max: options.yaxis_max,
337
+ to_plot: :transfer_rate,
338
+ machines: machines,
339
+ experiments: experiments,
340
+ receive_window: options.recwindow,
341
+ xaxis_max: options.xaxis_max == 0 ? max_x : options.xaxis_max,
342
+ xaxis_steps: options.xaxis_steps,
343
+ title: "Data transfer in relative comparison",
344
+ subtitle: "bigger is better",
345
+ xaxis_title: "Message Size",
346
+ xaxis_unit: "kB",
347
+ xaxis_divisor: 1000,
348
+ yaxis_title: "Ratio"
349
+ )
350
+
351
+ pdf = pdfout(rplot, file: options.pdf, width: options.width, height: options.height)
352
+ print("#{pdf}") unless options.pdf.empty?
353
+ print("#{rplot}") if options.pdf.empty?
354
+
355
+ end
356
+
357
+ # Implements the request-plot command.
358
+ #
359
+ def request_plot(args, options)
360
+
361
+ experiments = options.experiments.split(',')
362
+ machines = options.machines.split(',')
363
+
364
+ data = Ppbench::load_data(args)
365
+ filtered_data = Ppbench::filter(
366
+ data,
367
+ experiments: experiments,
368
+ machines: machines
369
+ )
370
+ aggregated_data = Ppbench::aggregate(filtered_data)
371
+
372
+ max_x = Ppbench::maximum(aggregated_data, of: :length)
373
+ max_y = Ppbench::maximum(aggregated_data, of: :rps)
374
+
375
+ rplot = Ppbench::plotter(
376
+ aggregated_data,
377
+ to_plot: :rps,
378
+ machines: machines,
379
+ experiments: experiments,
380
+ receive_window: options.recwindow,
381
+ xaxis_max: options.xaxis_max == 0 ? max_x : options.xaxis_max,
382
+ confidence: options.confidence,
383
+ no_points: options.nopoints,
384
+ with_bands: options.withbands,
385
+ yaxis_max: options.yaxis_max == 0 ? max_y : options.yaxis_max,
386
+ yaxis_steps: options.yaxis_steps,
387
+ xaxis_steps: options.xaxis_steps,
388
+ title: "Requests per seconds",
389
+ subtitle: "bigger is better",
390
+ xaxis_title: "Message Size",
391
+ xaxis_unit: "kB",
392
+ yaxis_title: "Requests per seconds",
393
+ yaxis_unit: "Req/sec",
394
+ yaxis_divisor: 1
395
+ )
396
+
397
+ pdf = pdfout(rplot, file: options.pdf, width: options.width, height: options.height)
398
+ print("#{pdf}") unless options.pdf.empty?
399
+ print("#{rplot}") if options.pdf.empty?
400
+ end
401
+
402
+ # Implements the request-comparison-plot command
403
+ #
404
+ def request_comparison_plot(args, options)
405
+
406
+ experiments = options.experiments.split(',')
407
+ machines = options.machines.split(',')
408
+
409
+ data = Ppbench::load_data(args)
410
+ filtered_data = Ppbench::filter(
411
+ data,
412
+ experiments: experiments,
413
+ machines: machines
414
+ )
415
+ aggregated_data = Ppbench::aggregate(filtered_data)
416
+
417
+ max_x = Ppbench::maximum(aggregated_data, of: :length)
418
+
419
+ rplot = Ppbench::comparison_plotter(
420
+ aggregated_data,
421
+ yaxis_max: options.yaxis_max,
422
+ to_plot: :rps,
423
+ machines: machines,
424
+ experiments: experiments,
425
+ receive_window: options.recwindow,
426
+ xaxis_max: options.xaxis_max == 0 ? max_x : options.xaxis_max,
427
+ xaxis_steps: options.xaxis_steps,
428
+ title: "Requests per second in relative comparison",
429
+ subtitle: "bigger is better",
430
+ xaxis_title: "Message Size",
431
+ xaxis_unit: "kB",
432
+ xaxis_divisor: 1000,
433
+ yaxis_title: "Ratio",
434
+ )
435
+
436
+ pdf = pdfout(rplot, file: options.pdf, width: options.width, height: options.height)
437
+ print("#{pdf}") unless options.pdf.empty?
438
+ print("#{rplot}") if options.pdf.empty?
439
+ end
440
+
441
+ # Implements the latency-plot command.
442
+ #
443
+ def latency_plot(args, options)
444
+
445
+ experiments = options.experiments.split(',')
446
+ machines = options.machines.split(',')
447
+
448
+ data = Ppbench::load_data(args)
449
+ filtered_data = Ppbench::filter(
450
+ data,
451
+ experiments: experiments,
452
+ machines: machines
453
+ )
454
+ aggregated_data = Ppbench::aggregate(filtered_data)
455
+
456
+ max_y = Ppbench::maximum(aggregated_data, of: :tpr)
457
+ max_x = Ppbench::maximum(aggregated_data, of: :length)
458
+
459
+ rplot = Ppbench::plotter(
460
+ aggregated_data,
461
+ to_plot: :tpr,
462
+ machines: machines,
463
+ experiments: experiments,
464
+ receive_window: options.recwindow,
465
+ xaxis_max: options.xaxis_max == 0 ? max_x : options.xaxis_max,
466
+ confidence: options.confidence,
467
+ no_points: options.nopoints,
468
+ with_bands: options.withbands,
469
+ yaxis_max: options.yaxis_max == 0 ? max_y : options.yaxis_max,
470
+ yaxis_steps: options.yaxis_steps,
471
+ xaxis_steps: options.xaxis_steps,
472
+ title: "Round-trip latency",
473
+ subtitle: "smaller is better",
474
+ xaxis_title: "Message Size",
475
+ xaxis_unit: "kB",
476
+ yaxis_title: "Latency",
477
+ yaxis_unit: "ms",
478
+ yaxis_divisor: 1
479
+ )
480
+
481
+ pdf = pdfout(rplot, file: options.pdf, width: options.width, height: options.height)
482
+ print("#{pdf}") unless options.pdf.empty?
483
+ print("#{rplot}") if options.pdf.empty?
484
+ end
485
+
486
+ # Implements the latency-comparison-plot command
487
+ #
488
+ def latency_comparison_plot(args, options)
489
+
490
+ experiments = options.experiments.split(',')
491
+ machines = options.machines.split(',')
492
+
493
+ data = Ppbench::load_data(args)
494
+ filtered_data = Ppbench::filter(
495
+ data,
496
+ experiments: experiments,
497
+ machines: machines
498
+ )
499
+ aggregated_data = Ppbench::aggregate(filtered_data)
500
+
501
+ max_x = Ppbench::maximum(aggregated_data, of: :length)
502
+
503
+ rplot = Ppbench::comparison_plotter(
504
+ aggregated_data,
505
+ yaxis_max: options.yaxis_max,
506
+ to_plot: :tpr,
507
+ machines: machines,
508
+ experiments: experiments,
509
+ receive_window: options.recwindow,
510
+ xaxis_max: options.xaxis_max == 0 ? max_x : options.xaxis_max,
511
+ xaxis_steps: options.xaxis_steps,
512
+ title: "Round-trip latency in relative comparison",
513
+ subtitle: "smaller is better",
514
+ xaxis_title: "Message Size",
515
+ xaxis_unit: "kB",
516
+ xaxis_divisor: 1000,
517
+ yaxis_title: "Ratio",
518
+ )
519
+
520
+ pdf = pdfout(rplot, file: options.pdf, width: options.width, height: options.height)
521
+ print("#{pdf}") unless options.pdf.empty?
522
+ print("#{rplot}") if options.pdf.empty?
523
+ end
524
+
525
+ # Implements the summary command.
526
+ #
527
+ def summary(args, options)
528
+
529
+ experiments = options.experiments.split(',')
530
+ machines = options.machines.split(',')
531
+
532
+ data = Ppbench::load_data(args)
533
+ filtered_data = Ppbench::filter(
534
+ data,
535
+ experiments: experiments,
536
+ machines: machines
537
+ )
538
+ aggregated_data = Ppbench::aggregate(filtered_data)
539
+
540
+ rows = []
541
+ aggregated_data.each do |experiment, machines|
542
+ machines.each do |machine, data|
543
+ mtr = data.map { |e| e[:transfer_rate] }.median / 1000 # median transfer rate
544
+ tpr = data.map { |e| e[:tpr] }.median # median round trip latency
545
+ rps = 1000 / tpr # median request per second
546
+
547
+ rows << [experiment, machine, data.count, "%.2f" % mtr, "%.2f" % rps, "%.2f" % tpr]
548
+ end
549
+ rows << :separator
550
+ end
551
+ rows.pop
552
+
553
+ print("We have data for: \n")
554
+ table = Terminal::Table.new(:headings => ['Experiment', 'Machine', 'Samples', 'Transfer (kB/s)', "Requests/sec", "Latency (ms)"], :rows => rows)
555
+ table.align_column(2, :right)
556
+ table.align_column(3, :right)
557
+ table.align_column(4, :right)
558
+ table.align_column(5, :right)
559
+ print("#{table}\n")
560
+ end
561
+
562
+ # Implements the naming command.
563
+ #
564
+ def naming(args, options)
565
+ experiments = options.experiments.split(',')
566
+ machines = options.machines.split(',')
567
+
568
+ data = Ppbench::load_data(args)
569
+ filtered_data = Ppbench::filter(
570
+ data,
571
+ experiments: experiments,
572
+ machines: machines
573
+ )
574
+
575
+ json_to_update = {}
576
+ existing_machine_names = []
577
+ existing_experiment_names = []
578
+ unless options.update.empty?
579
+ begin
580
+ file = File.read(options.update)
581
+ json_to_update = JSON.parse(file)
582
+ rescue Exception => ex
583
+ $stderr.puts("Could not process #{options.update}. It does not seem to be a valid JSON file.")
584
+ exit!
585
+ end
586
+ end
587
+
588
+ existing_machine_names = json_to_update['machines'].keys if json_to_update['machines']
589
+ existing_experiment_names = json_to_update['experiments'].keys if json_to_update['experiments']
590
+
591
+ machine_names = filtered_data.map { |entry| entry[:machine] }.uniq.sort - existing_machine_names
592
+ experiment_names = filtered_data.map { |entry| entry[:experiment] }.uniq.sort - existing_experiment_names
593
+
594
+ machines_map = (
595
+ machine_names.map { |e| [e, "#{e} (TODO: Provide a better name)"] } +
596
+ existing_machine_names.map { |e| [e, json_to_update['machines'][e]] }
597
+ ).to_h
598
+
599
+ experiments_map = (
600
+ experiment_names.map { |e| [e, "#{e} (TODO: Provide a better name)"] } +
601
+ existing_experiment_names.map { |e| [e, json_to_update['experiments'][e]] }
602
+ ).to_h
603
+
604
+ json = { "machines": machines_map, "experiments": experiments_map }
605
+ print ("#{JSON.pretty_generate(json)}")
606
+ end
607
+
608
+ # Implements the citation command.
609
+ #
610
+ def citation(args, options)
611
+ bibtex =
612
+ """
613
+ @misc{Kra2015,
614
+ title = {A distributed HTTP-based and REST-like ping-pong system for test and benchmarking purposes.},
615
+ author = {{Nane Kratzke}},
616
+ organization = {L\\\"ubeck University of Applied Sciences},
617
+ address = {L\\\"ubeck, Germany},
618
+ year = {2015},
619
+ howpublished = {\\url{https://github.com/nkratzke/pingpong}}
620
+ }
621
+ """
622
+
623
+ return bibtex if options.bibtex
624
+
625
+ """
626
+ To cite ppbench in publications use:
627
+
628
+ Kratzke, Nane (2015). A distributed HTTP-based and REST-like ping-pong system for test and benchmarking purposes.
629
+ Lübeck University of Applied Sciences, Lübeck, Germany. URL https://github.com/nkratzke/pingpong.
630
+
631
+ A BibTeX entry for LaTeX users is: #{bibtex}
632
+
633
+ """
634
+ end
635
+
636
+ command :run do |c|
637
+ c.syntax = 'ppbench run [options] log.csv'
638
+ c.description = 'Runs a ping pong benchmark.'
639
+ c.example 'Run a benchmark and tags the results as to be collected on a m3.2xlarge instance running a docker experiment',
640
+ 'ppbench run --host http://1.2.3.4:8080 --machine m3.2xlarge --experiment docker log.csv'
641
+
642
+ c.option '--host STRING', String, 'Host'
643
+ c.option '--machine STRING', String, 'A tag to categorize the machine (defaults to empty String)'
644
+ c.option '--experiment STRING', String, 'A tag to categorize the experiment (defaults to empty String)'
645
+ c.option '--min INT', Integer, 'Minimum message size [bytes] (defaults to 1)'
646
+ c.option '--max INT', Integer, 'Maximum message size [bytes] (defaults to 500.000)'
647
+ c.option '--coverage FLOAT', Float, 'Amount of requests to send (defaults to 5% == 0.05, must be between 0.0 and 1.0)'
648
+ c.option '--repetitions INT', Integer, 'Repetitions for each data point to collect (defaults to 1, must be >= 1)'
649
+ c.option '--concurrency INT', Integer, 'Requests to be send at the same time in parallel (defaults to 1, must be >= 1)'
650
+ c.option '--timeout SECONDS', Integer, 'Timeout in seconds (defaults to 60 seconds, must be >= 1)'
651
+
652
+ c.action do |args, options|
653
+
654
+ options.default :min => 1, :max => 500000
655
+ options.default :machine => ''
656
+ options.default :experiment => ''
657
+ options.default :coverage => 0.05
658
+ options.default :repetitions => 1
659
+ options.default :concurrency => 1
660
+ options.default :timeout => 60
661
+
662
+ validate_global_options(args, options)
663
+ validate_run_options(args, options)
664
+ run(args, options)
665
+ print("Finished\n")
666
+ end
667
+ end
668
+
669
+ command 'transfer-plot' do |c|
670
+ c.syntax = 'ppbench transfer-plot [options] *.csv'
671
+ c.summary = 'Plots data transfer rates in an absolute way.'
672
+ c.description = 'Generates a R script to plot data transfer rates in an absolute way.'
673
+
674
+ c.option '--machines LIST', String, MACHINES_DESCRIPTION
675
+ c.option '--experiments LIST', String, EXPERIMENTS_DESCRIPTION
676
+ c.option '--recwindow BYTES', Integer, RECWINDOW_DESCRIPTION
677
+
678
+ c.option '--yaxis_max FLOAT', Float, YAXIS_MAX_DESCRIPTION
679
+ c.option '--yaxis_steps TICKS', Integer, YAXIS_STEPS_DESCRIPTION
680
+ c.option '--xaxis_max BYTES', Integer, YAXIS_MAX_DESCRIPTION
681
+ c.option '--xaxis_steps TICKS', Integer, XAXIS_STEPS_DESCRIPTION
682
+
683
+ c.option '--confidence PERCENT' , Integer, CONFIDENCE_DESCRIPTION
684
+ c.option '--withbands', WITHBANDS_DESCRIPTION
685
+ c.option '--nopoints', NOPOINTS_DESCRIPTION
686
+
687
+ c.option '--pdf FILE', String, PDF_DESCRIPTION
688
+ c.option '--width INCH', Integer, PDF_WIDTH_DESCRIPTION
689
+ c.option '--height INCH', Integer, PDF_HEIGHT_DESCRIPTION
690
+
691
+ c.action do |args, options|
692
+ options.default :machines => ''
693
+ options.default :experiments => ''
694
+ options.default :recwindow => RECWINDOW_DEFAULT
695
+ options.default :confidence => CONFIDENCE_DEFAULT
696
+
697
+ options.default :yaxis_max => 0
698
+ options.default :yaxis_steps => AXIS_STEP_DEFAULT
699
+ options.default :xaxis_max => 0
700
+ options.default :xaxis_steps => AXIS_STEP_DEFAULT
701
+
702
+ options.default :pdf => ''
703
+ options.default :width => PDF_HEIGHT_WIDTH_DEFAULT
704
+ options.default :height => PDF_HEIGHT_WIDTH_DEFAULT
705
+
706
+ validate_global_options(args, options)
707
+ validate_plot_options(args, options)
708
+ validate_pdf_options(args,options)
709
+ transfer_plot(args, options)
710
+ end
711
+ end
712
+
713
+
714
+ command 'transfer-comparison-plot' do |c|
715
+ c.syntax = 'ppbench transfer-comparison-plot [options] *.csv'
716
+ c.summary = 'Compares data transfer rates in a relative way.'
717
+ c.description = 'Generates a R script to compare data transfer rates in a relative way.'
718
+
719
+ c.option '--machines LIST', String, MACHINES_DESCRIPTION
720
+ c.option '--experiments LIST', String, EXPERIMENTS_DESCRIPTION
721
+ c.option '--recwindow BYTES', Integer, RECWINDOW_DESCRIPTION
722
+
723
+ c.option '--yaxis_max BYTES', Float, YAXIS_MAX_DESCRIPTION
724
+ c.option '--yaxis_steps STEPS', Integer, YAXIS_STEPS_DESCRIPTION
725
+ c.option '--xaxis_max BYTES', Integer, XAXIS_MAX_DESCRIPTION
726
+ c.option '--xaxis_steps STEPS', Integer, XAXIS_STEPS_DESCRIPTION
727
+
728
+ c.option '--pdf FILE', String, PDF_DESCRIPTION
729
+ c.option '--width INTEGER', Integer, PDF_WIDTH_DESCRIPTION
730
+ c.option '--height INTEGER', Integer, PDF_HEIGHT_DESCRIPTION
731
+
732
+ c.action do |args, options|
733
+
734
+ options.default :machines => ''
735
+ options.default :experiments => ''
736
+ options.default :recwindow => RECWINDOW_DEFAULT
737
+
738
+ options.default :yaxis_max => COMPARISON_MAX_DEFAULT
739
+ options.default :yaxis_steps => AXIS_STEP_DEFAULT
740
+ options.default :xaxis_max => 0
741
+ options.default :xaxis_steps => AXIS_STEP_DEFAULT
742
+
743
+ options.default :pdf => ''
744
+ options.default :width => PDF_HEIGHT_WIDTH_DEFAULT
745
+ options.default :height => PDF_HEIGHT_WIDTH_DEFAULT
746
+
747
+ validate_global_options(args, options)
748
+ validate_comparison_options(args, options)
749
+ validate_pdf_options(args,options)
750
+ transfer_comparison_plot(args, options)
751
+ end
752
+ end
753
+
754
+ command 'request-plot' do |c|
755
+ c.syntax = 'ppbench request-plot [options] *.csv'
756
+ c.summary = 'Plots requests per second in an absolute way.'
757
+ c.description = 'Generates a R script to plot requests per second in an absolute way.'
758
+
759
+ c.option '--machines LIST', String, MACHINES_DESCRIPTION
760
+ c.option '--experiments LIST', String, EXPERIMENTS_DESCRIPTION
761
+ c.option '--recwindow BYTES', Integer, RECWINDOW_DESCRIPTION
762
+
763
+ c.option '--yaxis_max REQS', Float, YAXIS_MAX_DESCRIPTION
764
+ c.option '--yaxis_steps STEPS', Integer, YAXIS_STEPS_DESCRIPTION
765
+ c.option '--xaxis_max BYTES', Integer, XAXIS_MAX_DESCRIPTION
766
+ c.option '--xsteps STEPS', Integer, YAXIS_STEPS_DESCRIPTION
767
+
768
+ c.option '--confidence PERCENT' , Integer, CONFIDENCE_DESCRIPTION
769
+ c.option '--withbands', WITHBANDS_DESCRIPTION
770
+ c.option '--nopoints', NOPOINTS_DESCRIPTION
771
+
772
+ c.option '--pdf FILE', String, PDF_DESCRIPTION
773
+ c.option '--width INCH', Integer, PDF_WIDTH_DESCRIPTION
774
+ c.option '--height INCH', Integer, PDF_HEIGHT_DESCRIPTION
775
+
776
+ c.action do |args, options|
777
+ options.default :machines => ''
778
+ options.default :experiments => ''
779
+ options.default :recwindow => RECWINDOW_DEFAULT
780
+ options.default :confidence => CONFIDENCE_DEFAULT
781
+
782
+ options.default :yaxis_max => 0
783
+ options.default :yaxis_steps => AXIS_STEP_DEFAULT
784
+ options.default :xaxis_max => 0
785
+ options.default :xaxis_steps => AXIS_STEP_DEFAULT
786
+
787
+ options.default :pdf => ''
788
+ options.default :width => PDF_HEIGHT_WIDTH_DEFAULT
789
+ options.default :height => PDF_HEIGHT_WIDTH_DEFAULT
790
+
791
+ validate_global_options(args, options)
792
+ validate_plot_options(args, options)
793
+ validate_pdf_options(args, options)
794
+ request_plot(args, options)
795
+ end
796
+ end
797
+
798
+ command 'request-comparison-plot' do |c|
799
+ c.syntax = 'ppbench request-comparison-plot [options] *.csv'
800
+ c.summary = 'Compares requests per second in a relative way.'
801
+ c.description = 'Generates a R script to compare requests per second in a relative way.'
802
+
803
+ c.option '--machines STRING', String, MACHINES_DESCRIPTION
804
+ c.option '--experiments STRING', String, EXPERIMENTS_DESCRIPTION
805
+ c.option '--recwindow INTEGER', Integer, RECWINDOW_DESCRIPTION
806
+
807
+ c.option '--yaxis_max FLOAT', Float, YAXIS_MAX_DESCRIPTION
808
+
809
+ c.option '--xaxis_max BYTES', Integer, XAXIS_MAX_DESCRIPTION
810
+ c.option '--xaxis_steps INTEGER', Integer, YAXIS_STEPS_DESCRIPTION
811
+
812
+ c.option '--pdf FILE', String, 'Saves output to a PDF file'
813
+ c.option '--width INTEGER', Integer, 'Width of plot in inch (defaults to 7 inch, only useful with PDF output)'
814
+ c.option '--height INTEGER', Integer, 'Height of plot in inch (defaults to 7 inch, only useful with PDF output)'
815
+
816
+ c.action do |args, options|
817
+
818
+ options.default :machines => ''
819
+ options.default :experiments => ''
820
+ options.default :recwindow => RECWINDOW_DEFAULT
821
+
822
+ options.default :yaxis_max => COMPARISON_MAX_DEFAULT
823
+ options.default :xaxis_max => 0
824
+ options.default :xaxis_steps => AXIS_STEP_DEFAULT
825
+
826
+ options.default :pdf => ''
827
+ options.default :width => PDF_HEIGHT_WIDTH_DEFAULT
828
+ options.default :height => PDF_HEIGHT_WIDTH_DEFAULT
829
+
830
+ validate_global_options(args, options)
831
+ validate_comparison_options(args, options)
832
+ validate_pdf_options(args, options)
833
+ request_comparison_plot(args, options)
834
+ end
835
+ end
836
+
837
+ command 'latency-plot' do |c|
838
+ c.syntax = 'ppbench latency-plot [options] *.csv'
839
+ c.summary = 'Plots round-trip latencies in an absolute way.'
840
+ c.description = 'Generates a R script to plot round-trip latencies in an absolute way.'
841
+
842
+ c.example 'Generates a latency plot for data collected on machine m3.xlarge for java, dart and go implementations of the ping-pong system.',
843
+ 'ppbench latency-plot --machines m3.xlarge --experiments bare-java,bare-go,bare-dart *.csv > bare-comparison.R'
844
+
845
+ c.example 'Generates a latency plot for data collected on machine m3.xlarge for java, dart implementations of the ping-pong system.',
846
+ 'ppbench latency-comparison-plot --machines m3.xlarge --experiments bare-java,bare-go --pdf compare.pdf --width 9, --height 6 *.csv > bare-comparison.R'
847
+
848
+ c.option '--machines LIST', String, MACHINES_DESCRIPTION
849
+ c.option '--experiments LIST', String, EXPERIMENTS_DESCRIPTION
850
+ c.option '--recwindow BYTES', Integer, RECWINDOW_DESCRIPTION
851
+
852
+ c.option '--yaxis_max MS', Integer, YAXIS_MAX_DESCRIPTION
853
+ c.option '--yaxis_steps TICKS', Integer, YAXIS_STEPS_DESCRIPTION
854
+ c.option '--xaxis_max BYTES', Integer, XAXIS_MAX_DESCRIPTION
855
+ c.option '--xaxis_steps TICKS', Integer, XAXIS_STEPS_DESCRIPTION
856
+
857
+ c.option '--confidence PERCENT' , Integer, CONFIDENCE_DESCRIPTION
858
+ c.option '--withbands', WITHBANDS_DESCRIPTION
859
+ c.option '--nopoints', NOPOINTS_DESCRIPTION
860
+
861
+ c.option '--pdf FILE', String, PDF_DESCRIPTION
862
+ c.option '--width INCH', Integer, PDF_WIDTH_DESCRIPTION
863
+ c.option '--height INCH', Integer, PDF_HEIGHT_DESCRIPTION
864
+
865
+ c.action do |args, options|
866
+
867
+ options.default :machines => ''
868
+ options.default :experiments => ''
869
+ options.default :recwindow => RECWINDOW_DEFAULT
870
+
871
+ options.default :confidence => CONFIDENCE_DEFAULT
872
+
873
+ options.default :yaxis_max => 0
874
+ options.default :yaxis_steps => AXIS_STEP_DEFAULT
875
+ options.default :xaxis_max => 0
876
+ options.default :xaxis_steps => AXIS_STEP_DEFAULT
877
+
878
+ options.default :pdf => ''
879
+ options.default :width => PDF_HEIGHT_WIDTH_DEFAULT
880
+ options.default :height => PDF_HEIGHT_WIDTH_DEFAULT
881
+
882
+ validate_global_options(args, options)
883
+ validate_plot_options(args, options)
884
+ validate_pdf_options(args, options)
885
+ latency_plot(args, options)
886
+ end
887
+ end
888
+
889
+ command 'latency-comparison-plot' do |c|
890
+ c.syntax = 'ppbench latency-comparison-plot [options] *.csv'
891
+ c.summary = 'Compares round-trip latencies in a relative way.'
892
+ c.description = 'Generates a R script to compare round-trip latencies in a relative way.'
893
+
894
+ c.example 'Generates a latency comparison plot for data collected on machine m3.xlarge for java, dart and go implementations of the ping-pong system.',
895
+ 'ppbench latency-comparison-plot --machines m3.xlarge --experiments bare-java,bare-go,bare-dart *.csv > bare-comparison.R'
896
+
897
+ c.example 'Generates a latency comparison plot as PDF using Rscript',
898
+ 'ppbench latency-comparison-plot -m m3.xlarge -e bare-java,bare-go -p compare.pdf *.csv | Rscript - '
899
+
900
+ c.option '--machines LIST', String, MACHINES_DESCRIPTION
901
+ c.option '--experiments LIST', String, EXPERIMENTS_DESCRIPTION
902
+ c.option '--recwindow BYTES', Integer, RECWINDOW_DESCRIPTION
903
+
904
+ c.option '--yaxis_max MS', Float, 'Maximum Y Value (must be greater than 1.0, defaults to 2.0)'
905
+ c.option '--xaxis_max BYTES', Integer, XAXIS_MAX_DESCRIPTION
906
+ c.option '--xaxis_steps TICKS', Integer, YAXIS_STEPS_DESCRIPTION
907
+
908
+ c.option '--pdf FILE', String, PDF_DESCRIPTION
909
+ c.option '--width INCH', Integer, PDF_WIDTH_DESCRIPTION
910
+ c.option '--height INCH', Integer, PDF_HEIGHT_DESCRIPTION
911
+
912
+ c.action do |args, options|
913
+
914
+ options.default :machines => ''
915
+ options.default :experiments => ''
916
+ options.default :recwindow => RECWINDOW_DEFAULT
917
+
918
+ options.default :yaxis_max => COMPARISON_MAX_DEFAULT
919
+ options.default :xaxis_max => 0
920
+ options.default :xaxis_steps => AXIS_STEP_DEFAULT
921
+
922
+ options.default :pdf => ''
923
+ options.default :width => PDF_HEIGHT_WIDTH_DEFAULT
924
+ options.default :height => PDF_HEIGHT_WIDTH_DEFAULT
925
+
926
+ validate_global_options(args, options)
927
+ validate_comparison_options(args, options)
928
+ validate_pdf_options(args, options)
929
+ latency_comparison_plot(args, options)
930
+ end
931
+ end
932
+
933
+ command :summary do |c|
934
+ c.syntax = 'ppbench summary [options] *.csv'
935
+ c.summary = 'Summarizes benchmark data.'
936
+ c.description = 'Summarizes benchmark data. Useful to inspect data for completeness or to query machine and experiment tags.'
937
+
938
+ c.example 'Lists a summary of all benchmark data.',
939
+ 'ppbench summary *.csv'
940
+ c.example 'Lists a summary of all benchmark data tagged to be run on machines m3.2xlarge, m3.xlarge',
941
+ 'ppbench summary --machines m3.2xlarge,m3.xlarge *.csv'
942
+ c.example 'Lists a summary of all benchmark data tagged to be run as docker-java or bare-dart experiments',
943
+ 'ppbench summary --experiments bare-dart,docker-java *.csv'
944
+
945
+ c.option '--machines LIST', String, MACHINES_DESCRIPTION
946
+ c.option '--experiments LIST', String, EXPERIMENTS_DESCRIPTION
947
+
948
+ c.action do |args, options|
949
+ options.default :machines => ''
950
+ options.default :experiments => ''
951
+
952
+ summary(args, options)
953
+ end
954
+ end
955
+
956
+ command 'naming-template' do |c|
957
+
958
+ c.syntax = 'ppbench naming-template [options] *.csv'
959
+ c.summary = 'Generates a JSON file for naming.'
960
+ c.description = 'Generates a JSON file from benchmark data for naming. This file can be used with the --naming flag.'
961
+
962
+ c.example 'Generates a naming template from all benchmark data.',
963
+ 'ppbench naming-template *.csv > naming.json'
964
+ c.example 'Appends all missing names to an existing naming-template',
965
+ 'ppbench naming-template --update naming.json > naming-update.json'
966
+
967
+ c.option '--machines LIST', String, MACHINES_DESCRIPTION
968
+ c.option '--experiments LIST', String, EXPERIMENTS_DESCRIPTION
969
+ c.option '--update FILE', 'Naming file to update with additional data.'
970
+
971
+ c.action do |args, options|
972
+ options.default :machines => ''
973
+ options.default :experiments => ''
974
+ options.default :update => ''
975
+
976
+ validate_naming_options(args, options)
977
+ naming(args, options)
978
+ end
979
+
980
+ end
981
+
982
+ command :citation do |c|
983
+ c.syntax = 'ppbench citation [options]'
984
+ c.summary = 'Citation information about ppbench.'
985
+ c.description = 'Provides information how to cite ppbench in publications.'
986
+
987
+ c.example 'Get general information how to cite ppbench in publications',
988
+ 'ppbench citation'
989
+ c.example 'Append a bibtex entry for ppbench to your references.bib (LaTex users).',
990
+ 'ppbench citation --bibtex >> references.bib'
991
+ c.option '--bibtex', 'Get bibtex entry (for Latex users)'
992
+
993
+ c.action do |args, options|
994
+ print "#{citation(args, options)}\n"
995
+ end
996
+ end