awfy 0.2.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 +182 -10
  3. data/lib/awfy/cli.rb +195 -57
  4. data/lib/awfy/version.rb +1 -1
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f90af1a6ce12d7b811d3fb6353f91d46e1637607b7890d3ed0837fbeda7fdd96
4
- data.tar.gz: e2a17a61c3ecf4d5795fc636407b49fa1aeaf150308f67215acf8d648d8f5df5
3
+ metadata.gz: f9e397437b1d2fddfcad8796896160185c10f1832f98468e7b828b4e7e717a9e
4
+ data.tar.gz: 055a0bb06e33dcd7b77bd1231b1adce331f8a7e2966a0bdf014bd597d12bea2f
5
5
  SHA512:
6
- metadata.gz: 22291837342e69a9da6c55e7a4a6869a80b2dfa47d67e2ad3847a870335999560109de557f73b58d1b73d564fb1f9f016654fcc542ecada0238b16276a74ca23
7
- data.tar.gz: 4c8dfa85d87b246512f7a478bfdf844c4af7361e1b6d87a418bf1d96aaa260a592194f3c7114f79cdfb38afc3a473da871f52f93c2f20811739bdbe05ba2bf77
6
+ metadata.gz: fa51eab588e804c99b061401cc906ed58bbee83aae735839dba54eabc1a61800a99bcce7c613aeaa5b1e80c73906939cf93b4c17cf5feba54af2b3faf4e5556a
7
+ data.tar.gz: 6012ad3784a7eb769d25d47d88df7ff1bc19e0a3c5390d12c2a7f447c407c348b61c9bfd42dcef3fcb6ec7dbaf9acf2368c56afdf30cb1f91f8fc74591e54f3c
data/README.md CHANGED
@@ -1,38 +1,210 @@
1
1
  # Awfy (Are We Fast Yet)
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ CLI tool to help run suites of benchmarks and compare results between control implementations, across branches and with or without YJIT.
4
4
 
5
- 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/awfy`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ The benchmarks are written using a simple DSL in your target project.
6
6
 
7
- ## Installation
7
+ Supports running:
8
+
9
+ - IPS benchmarks (with [benchmark-ips](https://rubygems.org/gems/benchmark-ips))
10
+ - Memory profiling (with [memory_profiler](https://rubygems.org/gems/memory_profiler))
11
+ - CPU profiling (with [stackprof](https://rubygems.org/gems/stackprof))
12
+ - Flamegraph profiling (with [singed](https://rubygems.org/gems/singed))
8
13
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
14
+ Awfy can also create summary reports of the results which can be useful for comparing the performance of different implementations **(currently only supported for IPS benchmarks)**.
15
+
16
+ ## Installation
10
17
 
11
18
  Install the gem and add to the application's Gemfile by executing:
12
19
 
13
20
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
+ bundle add awfy
15
22
  ```
16
23
 
17
24
  If bundler is not being used to manage dependencies, install the gem by executing:
18
25
 
19
26
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
27
+ gem install awfy
21
28
  ```
22
29
 
23
30
  ## Usage
24
31
 
25
- TODO: Write usage instructions here
32
+ Imagine we have a custom implementation of a Struct class called `MyStruct`. We want to compare the performance of our implementation with the built-in `Struct` class
33
+ and other similar implementations.
34
+
35
+ First, we need to create a setup file in the `benchmarks/setup.rb` directory. For example:
36
+
37
+ ```ruby
38
+ # setup.rb
39
+
40
+ require "dry-struct"
41
+ require "active_model"
42
+
43
+ class DryStruct < Dry::Struct
44
+ attribute :name, Types::String
45
+ attribute :age, Types::Integer
46
+ end
47
+
48
+ class ActiveModelAttributes
49
+ include ActiveModel::API
50
+ include ActiveModel::Attributes
51
+
52
+ attribute :name, :string
53
+ attribute :age, :integer
54
+ end
55
+
56
+ # ... etc
57
+ ```
58
+
59
+ Then we write benchmarks in files in the `benchmarks/tests` directory. For example:
60
+
61
+ ```ruby
62
+ # benchmarks/tests/struct.rb
63
+
64
+ # A group is a collection of related reports
65
+ Awfy.group "Struct" do
66
+ # A report is a collection of tests related to one method or feature we want to benchmark
67
+ report "#some_method" do
68
+ # We do not want to the benchmark to include the creation of the object
69
+ my_struct = MyStruct.new(name: "John", age: 30)
70
+ ruby_struct = Struct.new(:name, :age).new("John", 30)
71
+ dry_struct = DryStruct.new(name: "John", age: 30)
72
+ active_model_attributes = ActiveModelAttributes.new(name: "John", age: 30)
73
+
74
+ # "control" blocks are used to compare the performance to other implementations
75
+ control "Ruby Struct" do
76
+ ruby_struct.some_method
77
+ end
78
+
79
+ control "Dry::Struct" do
80
+ dry_struct.some_method
81
+ end
82
+
83
+ control "ActiveModel::Attributes" do
84
+ active_model_attributes.some_method
85
+ end
86
+
87
+ # This is our implementation under test
88
+ test "MyStruct" do
89
+ my_struct.some_method
90
+ end
91
+ end
92
+
93
+ end
94
+ ```
95
+
96
+ ### IPS Benchmarks & Summary Reports
97
+
98
+ Say you are working on performance improvements in a branch called `perf`.
99
+
100
+ ```bash
101
+ git checkout perf
102
+
103
+ # ... make some changes ... then run the benchmarks
104
+
105
+ bundle exec awfy ips Struct "#some_method" --compare-with=main --runtime=both
106
+ ```
107
+
108
+ Note the comparison here is with the "baseline" which is the "test" block running on MRI without YJIT enabled, on the
109
+ current branch.
110
+
111
+ ```
112
+ Running IPS for:
113
+ > Struct/#some_method...
114
+ > [mri - branch 'perf'] Struct / #some_method
115
+ > [mri - branch 'main'] Struct / #some_method
116
+ > [yjit - branch 'perf'] Struct / #some_method
117
+ > [yjit - branch 'main'] Struct / #some_method
118
+ +---------------------------------------------------------------------------+
119
+ | Struct/#some_method |
120
+ +--------+---------+----------------------------+-------------+-------------+
121
+ | Branch | Runtime | Name | IPS | Vs baseline |
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 | - |
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 |
134
+ +--------+---------+----------------------------+-------------+-------------+
135
+ ```
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
+
166
+ ## CLI Options
167
+
168
+ ```
169
+ bundle exec awfy -h
170
+ Commands:
171
+ awfy flamegraph GROUP REPORT TEST # Run flamegraph profiling
172
+ awfy help [COMMAND] # Describe available commands or one specific command
173
+ awfy ips [GROUP] [REPORT] [TEST] # Run IPS benchmarks
174
+ awfy list [GROUP] # List all tests in a group
175
+ awfy memory [GROUP] [REPORT] [TEST] # Run memory profiling
176
+ awfy profile [GROUP] [REPORT] [TEST] # Run CPU profiling
177
+
178
+ Options:
179
+ [--runtime=RUNTIME] # Run with and/or without YJIT enabled
180
+ # Default: both
181
+ # Possible values: both, yjit, mri
182
+ [--compare-with=COMPARE_WITH] # Name of branch to compare with results on current branch
183
+ [--compare-control], [--no-compare-control], [--skip-compare-control] # When comparing branches, also re-run all control blocks too
184
+ # Default: false
185
+ [--summary], [--no-summary], [--skip-summary] # Generate a summary of the results
186
+ # Default: true
187
+ [--verbose], [--no-verbose], [--skip-verbose] # Verbose output
188
+ # Default: false
189
+ [--ips-warmup=N] # Number of seconds to warmup the benchmark
190
+ # Default: 1
191
+ [--ips-time=N] # Number of seconds to run the benchmark
192
+ # Default: 3
193
+ [--temp-output-directory=TEMP_OUTPUT_DIRECTORY] # Directory to store temporary output files
194
+ # Default: ./benchmarks/tmp
195
+ [--setup-file-path=SETUP_FILE_PATH] # Path to the setup file
196
+ # Default: ./benchmarks/setup
197
+ [--tests-path=TESTS_PATH] # Path to the tests files
198
+ # Default: ./benchmarks/tests
199
+ ```
26
200
 
27
201
  ## Development
28
202
 
29
203
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
204
 
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`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
-
33
205
  ## Contributing
34
206
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/awfy.
207
+ Bug reports and pull requests are welcome on GitHub at https://github.com/stevegeek/awfy.
36
208
 
37
209
  ## License
38
210
 
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)
@@ -248,7 +265,7 @@ module Awfy
248
265
 
249
266
  say if verbose?
250
267
  say "> --------------------------" if verbose?
251
- say "> Report (#{runtime} - branch '#{git_current_branch_name}'): #{report[:name]}", :magenta
268
+ say "> [#{runtime} - branch '#{git_current_branch_name}'] #{group[:name]} / #{report[:name]}", :magenta
252
269
  say "> --------------------------" if verbose?
253
270
  say if verbose?
254
271
  yield run_report, runtime
@@ -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,65 +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
378
- test_name = result[:is_baseline] ? "#{result[:test_name]} (baseline)" : result[:test_name]
526
+ diff_message = result_diff_message(result)
527
+ test_name = result[:is_baseline] ? "(baseline) #{result[:test_name]}" : result[:test_name]
379
528
 
380
- [result[:branch], result[:runtime], test_name, 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: "Summary for #{requested_tests(group_data[:group], group_data[:report])}",
386
- headings: ["Branch", "Runtime", "Name", "IPS", "Diff v baseline (times)"],
387
- rows: rows
388
- )
389
-
390
- table.align_column(2, :right)
391
- table.align_column(3, :right)
392
- table.align_column(4, :right)
393
-
394
- say table
532
+ output_summary_table(report, rows, "Branch", "Runtime", "Name", "IPS", "Vs baseline")
395
533
  end
396
534
  end
397
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.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: awfy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-28 00:00:00.000000000 Z
11
+ date: 2024-10-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor