awfy 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +39 -10
  3. data/lib/awfy/cli.rb +193 -61
  4. data/lib/awfy/version.rb +1 -1
  5. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b3e0b8df98fcef36e265d46ab9416c1b7cd529116fe57a4b9470a95d04427d0
4
- data.tar.gz: a7d9962369da0320d5d97cfb32db82ff06b52d72425e22e4134759c8a549a140
3
+ metadata.gz: f9e397437b1d2fddfcad8796896160185c10f1832f98468e7b828b4e7e717a9e
4
+ data.tar.gz: 055a0bb06e33dcd7b77bd1231b1adce331f8a7e2966a0bdf014bd597d12bea2f
5
5
  SHA512:
6
- metadata.gz: 89e5d7f45b6375e790ea7387af082d603d3464caa0944084970c858c526e4bccb95673a4beddb61ebfb0b3b3bb64b7b285b613870b41d1baa3ed7d114d9c9ee8
7
- data.tar.gz: beed2c0d694ffb1da281c9ac0344ca52e11cb981c2933d34f6a26ef5b73ba16c4d2e525bd40dd9a9fe68fc2a51bf10a3870253330eaa540aa05936ef7d211bb6
6
+ metadata.gz: fa51eab588e804c99b061401cc906ed58bbee83aae735839dba54eabc1a61800a99bcce7c613aeaa5b1e80c73906939cf93b4c17cf5feba54af2b3faf4e5556a
7
+ data.tar.gz: 6012ad3784a7eb769d25d47d88df7ff1bc19e0a3c5390d12c2a7f447c407c348b61c9bfd42dcef3fcb6ec7dbaf9acf2368c56afdf30cb1f91f8fc74591e54f3c
data/README.md CHANGED
@@ -120,20 +120,49 @@ Running IPS for:
120
120
  +--------+---------+----------------------------+-------------+-------------+
121
121
  | Branch | Runtime | Name | IPS | Vs baseline |
122
122
  +--------+---------+----------------------------+-------------+-------------+
123
- | perf | mri | Ruby Struct | 3.288M | 2.26 x |
124
- | perf | yjit | Ruby Struct | 3.238M | 2.22 x |
125
- | perf | yjit | MyStruct | 2.364M | 1.62 x |
126
- | main | yjit | MyStruct | 2.255M | 1.55 x |
127
- | perf | mri | (baseline) MyStruct | 1.455M | - |
123
+ | perf | mri | Ruby Struct| 3.288M | 2.26 x |
124
+ | perf | yjit | Ruby Struct| 3.238M | 2.22 x |
125
+ | perf | yjit | MyStruct| 2.364M | 1.62 x |
126
+ | main | yjit | MyStruct| 2.255M | 1.55 x |
127
+ | perf | mri | (baseline) MyStruct| 1.455M | - |
128
128
  +--------+---------+----------------------------+-------------+-------------+
129
- | main | mri | MyStruct | 1.248M | -1.1 x |
130
- | perf | yjit | Dry::Struct | 1.213M | -1.2 x |
131
- | perf | mri | Dry::Struct | 639.178k | -2.28 x |
132
- | perf | yjit | ActiveModel::Attributes | 487.398k | -2.99 x |
133
- | perf | mri | ActiveModel::Attributes | 310.554k | -4.69 x |
129
+ | main | mri | MyStruct| 1.248M | -1.1 x |
130
+ | perf | yjit | Dry::Struct| 1.213M | -1.2 x |
131
+ | perf | mri | Dry::Struct| 639.178k | -2.28 x |
132
+ | perf | yjit | ActiveModel::Attributes| 487.398k | -2.99 x |
133
+ | perf | mri | ActiveModel::Attributes| 310.554k | -4.69 x |
134
134
  +--------+---------+----------------------------+-------------+-------------+
135
135
  ```
136
136
 
137
+
138
+ ### Memory Profiling
139
+
140
+ ```bash
141
+ bundle exec awfy memory Struct "#some_method"
142
+ ```
143
+
144
+ Produces a report like:
145
+
146
+ ```
147
+ +----------------------------------------------------------------------------------------------------------------+
148
+ | Struct/.new |
149
+ +--------+---------+----------------------------+-------------------+-------------+----------------+-------------+
150
+ | Branch | Runtime | Name | Total Allocations | Vs baseline | Total Retained | Vs baseline |
151
+ +--------+---------+----------------------------+-------------------+-------------+----------------+-------------+
152
+ | perf | mri | ActiveModel::Attributes | 1.200k | 3.33 x | 640 | ∞ |
153
+ | perf | yjit | ActiveModel::Attributes | 1.200k | 3.33 x | 0 | same |
154
+ | perf | mri | Dry::Struct | 360 | 1.0 x | 160 | ∞ |
155
+ | perf | mri | (baseline) Literal::Struct | 360 | - | 0 | - |
156
+ +--------+---------+----------------------------+-------------------+-------------+----------------+-------------+
157
+ | perf | yjit | Dry::Struct | 360 | same | 0 | same |
158
+ | perf | yjit | Literal::Struct | 360 | same | 0 | same |
159
+ | perf | mri | Ruby Struct | 200 | -0.56 x | 0 | same |
160
+ | perf | mri | Ruby Data | 200 | -0.56 x | 0 | same |
161
+ | perf | yjit | Ruby Struct | 200 | -0.56 x | 0 | same |
162
+ | perf | yjit | Ruby Data | 200 | -0.56 x | 0 | same |
163
+ +--------+---------+----------------------------+-------------------+-------------+----------------+-------------+
164
+ ```
165
+
137
166
  ## CLI Options
138
167
 
139
168
  ```
data/lib/awfy/cli.rb CHANGED
@@ -24,6 +24,7 @@ module Awfy
24
24
  class_option :compare_control, type: :boolean, desc: "When comparing branches, also re-run all control blocks too", default: false
25
25
 
26
26
  class_option :summary, type: :boolean, desc: "Generate a summary of the results", default: true
27
+ class_option :quiet, type: :boolean, desc: "Silence output. Note if `summary` option is enabled the summaries will be displayed even if `quiet` enabled.", default: false
27
28
  class_option :verbose, type: :boolean, desc: "Verbose output", default: false
28
29
 
29
30
  class_option :ips_warmup, type: :numeric, default: 1, desc: "Number of seconds to warmup the benchmark"
@@ -40,7 +41,7 @@ module Awfy
40
41
  run_pref_test(group) { list_group(_1) }
41
42
  end
42
43
 
43
- desc "ips [GROUP] [REPORT] [TEST]", "Run IPS benchmarks"
44
+ desc "ips [GROUP] [REPORT] [TEST]", "Run IPS benchmarks. Can generate summary across implementations, runtimes and branches."
44
45
  def ips(group = nil, report = nil, test = nil)
45
46
  say "Running IPS for:"
46
47
  say "> #{requested_tests(group, report, test)}..."
@@ -48,7 +49,7 @@ module Awfy
48
49
  run_pref_test(group) { run_ips(_1, report, test) }
49
50
  end
50
51
 
51
- desc "memory [GROUP] [REPORT] [TEST]", "Run memory profiling"
52
+ desc "memory [GROUP] [REPORT] [TEST]", "Run memory profiling. Can generate summary across implementations, runtimes and branches."
52
53
  def memory(group = nil, report = nil, test = nil)
53
54
  say "Running memory profiling for:"
54
55
  say "> #{requested_tests(group, report, test)}..."
@@ -105,6 +106,7 @@ module Awfy
105
106
 
106
107
  def run_pref_test(group, &)
107
108
  configure_benchmark_run
109
+ prepare_output_directory
108
110
  if group
109
111
  run_group(group, &)
110
112
  else
@@ -144,12 +146,10 @@ module Awfy
144
146
  say "> #{group[:name]}...", :cyan
145
147
  end
146
148
 
147
- prepare_output_directory_for_ips
148
-
149
149
  execute_report(group, report_name) do |report, runtime|
150
150
  Benchmark.ips(time: options[:ips_time], warmup: options[:ips_warmup], quiet: show_summary? || verbose?) do |bm|
151
151
  execute_tests(report, test_name, output: false) do |test, _|
152
- test_label = "[#{runtime}] #{test[:control] ? CONTROL_MARKER : TEST_MARKER} #{test[:name]}"
152
+ test_label = generate_test_label(test, runtime)
153
153
  bm.item(test_label, &test[:block])
154
154
  end
155
155
 
@@ -165,18 +165,35 @@ module Awfy
165
165
  generate_ips_summary if options[:summary]
166
166
  end
167
167
 
168
+ def generate_test_label(test, runtime)
169
+ "[#{runtime}] #{test[:control] ? CONTROL_MARKER : TEST_MARKER} #{test[:name]}"
170
+ end
171
+
168
172
  def run_memory(group, report_name, test_name)
169
173
  if verbose?
170
174
  say "> Memory profiling for:"
171
175
  say "> #{group[:name]}...", :cyan
172
176
  end
173
177
  execute_report(group, report_name) do |report, runtime|
178
+ results = []
174
179
  execute_tests(report, test_name) do |test, _|
175
- MemoryProfiler.report do
180
+ data = MemoryProfiler.report do
176
181
  test[:block].call
177
- end.pretty_print
182
+ end
183
+ test_label = generate_test_label(test, runtime)
184
+ results << {
185
+ label: test_label,
186
+ data: data
187
+ }
188
+ data.pretty_print if verbose?
189
+ end
190
+
191
+ save_to(:memory, group, report, runtime) do |file_name|
192
+ save_memory_profile_report_to_file(file_name, results)
178
193
  end
179
194
  end
195
+
196
+ generate_memory_summary if options[:summary]
180
197
  end
181
198
 
182
199
  def run_flamegraph(group, report_name, test_name)
@@ -285,11 +302,40 @@ module Awfy
285
302
  result
286
303
  end
287
304
 
288
- def prepare_output_directory_for_ips
305
+ def prepare_output_directory
289
306
  FileUtils.mkdir_p(temp_dir) unless Dir.exist?(temp_dir)
290
307
  Dir.glob("#{temp_dir}/*.json").each { |file| File.delete(file) }
291
308
  end
292
309
 
310
+ def save_memory_profile_report_to_file(file_name, results)
311
+ data = results.map do |label_and_data|
312
+ result = label_and_data[:data]
313
+ {
314
+ label: label_and_data[:label],
315
+ total_allocated_memory: result.total_allocated_memsize,
316
+ total_retained_memory: result.total_retained_memsize,
317
+ # Individual results, arrays of objects {count: numeric, data: string}
318
+ allocated_memory_by_gem: result.allocated_memory_by_gem,
319
+ retained_memory_by_gem: result.retained_memory_by_gem,
320
+ allocated_memory_by_file: result.allocated_memory_by_file,
321
+ retained_memory_by_file: result.retained_memory_by_file,
322
+ allocated_memory_by_location: result.allocated_memory_by_location,
323
+ retained_memory_by_location: result.retained_memory_by_location,
324
+ allocated_memory_by_class: result.allocated_memory_by_class,
325
+ retained_memory_by_class: result.retained_memory_by_class,
326
+ allocated_objects_by_gem: result.allocated_objects_by_gem,
327
+ retained_objects_by_gem: result.retained_objects_by_gem,
328
+ allocated_objects_by_file: result.allocated_objects_by_file,
329
+ retained_objects_by_file: result.retained_objects_by_file,
330
+ allocated_objects_by_location: result.allocated_objects_by_location,
331
+ retained_objects_by_location: result.retained_objects_by_location,
332
+ allocated_objects_by_class: result.allocated_objects_by_class,
333
+ retained_objects_by_class: result.retained_objects_by_class
334
+ }
335
+ end
336
+ File.write(file_name, data.to_json)
337
+ end
338
+
293
339
  def save_to(type, group, report, runtime)
294
340
  current_branch = git_current_branch_name
295
341
  file_name = "#{temp_dir}/#{type}-#{runtime}-#{current_branch}-#{group[:name]}-#{report[:name]}.json".gsub(/[^A-Za-z0-9\/_\-.]/, "_")
@@ -304,7 +350,18 @@ module Awfy
304
350
  yield file_name
305
351
  end
306
352
 
307
- def load_json(file_name)
353
+ def load_results_json(type, file_name)
354
+ case type
355
+ when "ips"
356
+ load_ips_results_json(file_name)
357
+ when "memory"
358
+ load_memory_results_json(file_name)
359
+ else
360
+ raise "Unknown test type"
361
+ end
362
+ end
363
+
364
+ def load_ips_results_json(file_name)
308
365
  JSON.parse(File.read(file_name)).map do |result|
309
366
  {
310
367
  label: result["item"],
@@ -316,14 +373,121 @@ module Awfy
316
373
  end
317
374
  end
318
375
 
319
- def generate_ips_summary
320
- awfy_report_result_files = Dir.glob("#{temp_dir}/awfy-ips-*.json").map do |file_name|
376
+ def load_memory_results_json(file_name)
377
+ JSON.parse(File.read(file_name)).map { _1.transform_keys(&:to_sym) }
378
+ end
379
+
380
+ def choose_baseline_test(results)
381
+ base_branch = git_current_branch_name
382
+ baseline = results.find do |r|
383
+ r[:branch] == base_branch && r[:label].include?(TEST_MARKER) && r[:runtime] == (yjit_only? ? "yjit" : "mri") # Baseline is mri baseline unless yjit only
384
+ end
385
+ unless baseline
386
+ say_error "Could not work out which result is considered the 'baseline' (ie the `test` case)"
387
+ exit(1)
388
+ end
389
+ baseline[:is_baseline] = true
390
+ say "> Chosen baseline: #{baseline[:label]}" if verbose?
391
+ baseline
392
+ end
393
+
394
+ def generate_memory_summary
395
+ read_reports_for_summary("memory") do |report, results, baseline|
396
+ result_diffs = results.map do |result|
397
+ overlaps = result[:total_allocated_memory] == baseline[:total_allocated_memory] && result[:total_retained_memory] == baseline[:total_retained_memory]
398
+ diff_x = if baseline[:total_allocated_memory].zero? && !result[:total_allocated_memory].zero?
399
+ Float::INFINITY
400
+ elsif baseline[:total_allocated_memory].zero?
401
+ 0.0
402
+ elsif baseline[:total_allocated_memory] > result[:total_allocated_memory]
403
+ -1.0 * result[:total_allocated_memory] / baseline[:total_allocated_memory]
404
+ else
405
+ result[:total_allocated_memory].to_f / baseline[:total_allocated_memory]
406
+ end
407
+ retained_diff_x = if baseline[:total_retained_memory].zero? && !result[:total_retained_memory].zero?
408
+ Float::INFINITY
409
+ elsif baseline[:total_retained_memory].zero?
410
+ 0.0
411
+ elsif baseline[:total_retained_memory] > result[:total_retained_memory]
412
+ -1.0 * result[:total_retained_memory] / baseline[:total_retained_memory]
413
+ else
414
+ result[:total_retained_memory].to_f / baseline[:total_retained_memory]
415
+ end
416
+ result.merge(
417
+ overlaps: overlaps,
418
+ diff_times: diff_x.round(2),
419
+ retained_diff_times: retained_diff_x.round(2)
420
+ )
421
+ end
422
+
423
+ result_diffs.sort_by! { |result| -1 * result[:diff_times] }
424
+
425
+ rows = result_diffs.map do |result|
426
+ diff_message = result_diff_message(result)
427
+ retained_message = result_diff_message(result, :retained_diff_times)
428
+ test_name = result[:is_baseline] ? "(baseline) #{result[:test_name]}" : result[:test_name]
429
+ [result[:branch], result[:runtime], test_name, humanize_scale(result[:total_allocated_memory]), diff_message, humanize_scale(result[:total_retained_memory]), retained_message]
430
+ end
431
+
432
+ output_summary_table(report, rows, "Branch", "Runtime", "Name", "Total Allocations", "Vs baseline", "Total Retained", "Vs baseline")
433
+ end
434
+ end
435
+
436
+ def output_summary_table(report, rows, *headings)
437
+ group_data = report.first
438
+ table = ::Terminal::Table.new(title: requested_tests(group_data[:group], group_data[:report]), headings: headings)
439
+
440
+ rows.each do |row|
441
+ table.add_row(row)
442
+ if row[4] == "-" # FIXME: this is finding the baseline...
443
+ table.add_separator(border_type: :dot3)
444
+ end
445
+ end
446
+
447
+ (2...headings.size).each { table.align_column(_1, :right) }
448
+
449
+ if options[:quiet] && options[:summary]
450
+ puts table
451
+ else
452
+ say table
453
+ end
454
+ end
455
+
456
+ def result_diff_message(result, diff_key = :diff_times)
457
+ if result[:is_baseline]
458
+ "-"
459
+ elsif result[:overlaps] || result[diff_key].zero?
460
+ "same"
461
+ elsif result[diff_key] == Float::INFINITY
462
+ "∞"
463
+ elsif result[diff_key]
464
+ "#{result[diff_key]} x"
465
+ else
466
+ "?"
467
+ end
468
+ end
469
+
470
+ SUFFIXES = ["", "k", "M", "B", "T", "Q"].freeze
471
+
472
+ def humanize_scale(number, round_to: 0)
473
+ return 0 if number.zero?
474
+ number = number.round(round_to)
475
+ scale = (Math.log10(number) / 3).to_i
476
+ scale = 0 if scale < 0 || scale >= SUFFIXES.size
477
+ suffix = SUFFIXES[scale]
478
+ scaled_value = number.to_f / (1000**scale)
479
+ dp = (scale == 0) ? 0 : 3
480
+ "%10.#{dp}f#{suffix}" % scaled_value
481
+ end
482
+
483
+ def read_reports_for_summary(type)
484
+ awfy_report_result_files = Dir.glob("#{temp_dir}/awfy-#{type}-*.json").map do |file_name|
321
485
  JSON.parse(File.read(file_name)).map { _1.transform_keys(&:to_sym) }
322
486
  end
323
487
 
324
488
  awfy_report_result_files.each do |report|
325
489
  results = report.map do |single_run|
326
- load_json(single_run[:output_path]).map do |result|
490
+ load_results_json(type, single_run[:output_path]).map do |result|
327
491
  test_name = result[:label].match(/\[.{3,4}\] \[.\] (.*)/)[1]
328
492
  result.merge!(
329
493
  runtime: single_run[:runtime],
@@ -333,71 +497,39 @@ module Awfy
333
497
  end
334
498
  end
335
499
  results.flatten!(1)
500
+ baseline = choose_baseline_test(results)
336
501
 
337
- base_branch = git_current_branch_name
338
- baseline = results.find do |r|
339
- r[:branch] == base_branch && r[:label].include?(TEST_MARKER) && r[:runtime] == (yjit_only? ? "yjit" : "mri") # Baseline is mri baseline unless yjit only
340
- end
341
- unless baseline
342
- say_error "Could not work out which result is considered the 'baseline' (ie the `test` case)"
343
- exit(1)
344
- end
345
- baseline[:is_baseline] = true
346
- say "> Chosen baseline: #{baseline[:label]}" if verbose?
502
+ yield report, results, baseline
503
+ end
504
+ end
347
505
 
506
+ def generate_ips_summary
507
+ read_reports_for_summary("ips") do |report, results, baseline|
348
508
  result_diffs = results.map do |result|
349
- if baseline
350
- baseline_stats = baseline[:stats]
351
- result_stats = result[:stats]
352
- overlaps = result_stats.overlaps?(baseline_stats)
353
- diff_x = if baseline_stats.central_tendency > result_stats.central_tendency
354
- -1.0 * result_stats.speedup(baseline_stats).first
355
- else
356
- result_stats.slowdown(baseline_stats).first
357
- end
509
+ baseline_stats = baseline[:stats]
510
+ result_stats = result[:stats]
511
+ overlaps = result_stats.overlaps?(baseline_stats)
512
+ diff_x = if baseline_stats.central_tendency > result_stats.central_tendency
513
+ -1.0 * result_stats.speedup(baseline_stats).first
514
+ else
515
+ result_stats.slowdown(baseline_stats).first
358
516
  end
359
-
360
517
  result.merge(
361
518
  overlaps: overlaps,
362
- diff_times: diff_x
519
+ diff_times: diff_x.round(2)
363
520
  )
364
521
  end
365
522
 
366
523
  result_diffs.sort_by! { |result| -1 * result[:iter] }
367
524
 
368
525
  rows = result_diffs.map do |result|
369
- diff_message = if result[:is_baseline]
370
- "-"
371
- elsif result[:overlaps]
372
- "same-ish"
373
- elsif result[:diff_times]
374
- "#{result[:diff_times].round(2)} x"
375
- else
376
- "?"
377
- end
526
+ diff_message = result_diff_message(result)
378
527
  test_name = result[:is_baseline] ? "(baseline) #{result[:test_name]}" : result[:test_name]
379
528
 
380
- [result[:branch], result[:runtime], test_name, Benchmark::IPS::Helpers.scale(result[:stats].central_tendency.round), diff_message]
529
+ [result[:branch], result[:runtime], test_name, humanize_scale(result[:stats].central_tendency), diff_message]
381
530
  end
382
531
 
383
- group_data = report.first
384
- table = ::Terminal::Table.new(
385
- title: requested_tests(group_data[:group], group_data[:report]),
386
- headings: ["Branch", "Runtime", "Name", "IPS", "Vs baseline"]
387
- )
388
-
389
- table.align_column(2, :right)
390
- table.align_column(3, :right)
391
- table.align_column(4, :right)
392
-
393
- rows.each do |row|
394
- table.add_row(row)
395
- if row[4] == "-"
396
- table.add_separator(border_type: :dot3)
397
- end
398
- end
399
-
400
- say table
532
+ output_summary_table(report, rows, "Branch", "Runtime", "Name", "IPS", "Vs baseline")
401
533
  end
402
534
  end
403
535
 
data/lib/awfy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Awfy
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: awfy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou