awfy 0.3.0 → 0.4.0

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.
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