awfy 0.1.0 → 0.3.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 +153 -10
  3. data/lib/awfy/cli.rb +88 -75
  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: 5bea684a36c13b8593199aa4490e13c5fbdcb35222766daa401b9c8151a71c8f
4
- data.tar.gz: 1825c25d81bd2210269d713313a8634d66f654b13e763e1f4bb33f7f62bcfe67
3
+ metadata.gz: 9b3e0b8df98fcef36e265d46ab9416c1b7cd529116fe57a4b9470a95d04427d0
4
+ data.tar.gz: a7d9962369da0320d5d97cfb32db82ff06b52d72425e22e4134759c8a549a140
5
5
  SHA512:
6
- metadata.gz: 273fda35ea1688650b64b815d7b8b37b15485214341de4e2ca7e048a3f84f89cc0cb5693cadd5bfb0ed14810eb2169f142080e82df26c60acdbc1b421e046403
7
- data.tar.gz: d00c1252bbb61fe0687806be84dcc6cdc2ce20148b893fe50f09dc82fa258ec0f6f4deb1a89aefae0efcb51c187aa3a2928db748dfa321f096b4f71b328e0465
6
+ metadata.gz: 89e5d7f45b6375e790ea7387af082d603d3464caa0944084970c858c526e4bccb95673a4beddb61ebfb0b3b3bb64b7b285b613870b41d1baa3ed7d114d9c9ee8
7
+ data.tar.gz: beed2c0d694ffb1da281c9ac0344ca52e11cb981c2933d34f6a26ef5b73ba16c4d2e525bd40dd9a9fe68fc2a51bf10a3870253330eaa540aa05936ef7d211bb6
data/README.md CHANGED
@@ -1,38 +1,181 @@
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
+ ## CLI Options
138
+
139
+ ```
140
+ bundle exec awfy -h
141
+ Commands:
142
+ awfy flamegraph GROUP REPORT TEST # Run flamegraph profiling
143
+ awfy help [COMMAND] # Describe available commands or one specific command
144
+ awfy ips [GROUP] [REPORT] [TEST] # Run IPS benchmarks
145
+ awfy list [GROUP] # List all tests in a group
146
+ awfy memory [GROUP] [REPORT] [TEST] # Run memory profiling
147
+ awfy profile [GROUP] [REPORT] [TEST] # Run CPU profiling
148
+
149
+ Options:
150
+ [--runtime=RUNTIME] # Run with and/or without YJIT enabled
151
+ # Default: both
152
+ # Possible values: both, yjit, mri
153
+ [--compare-with=COMPARE_WITH] # Name of branch to compare with results on current branch
154
+ [--compare-control], [--no-compare-control], [--skip-compare-control] # When comparing branches, also re-run all control blocks too
155
+ # Default: false
156
+ [--summary], [--no-summary], [--skip-summary] # Generate a summary of the results
157
+ # Default: true
158
+ [--verbose], [--no-verbose], [--skip-verbose] # Verbose output
159
+ # Default: false
160
+ [--ips-warmup=N] # Number of seconds to warmup the benchmark
161
+ # Default: 1
162
+ [--ips-time=N] # Number of seconds to run the benchmark
163
+ # Default: 3
164
+ [--temp-output-directory=TEMP_OUTPUT_DIRECTORY] # Directory to store temporary output files
165
+ # Default: ./benchmarks/tmp
166
+ [--setup-file-path=SETUP_FILE_PATH] # Path to the setup file
167
+ # Default: ./benchmarks/setup
168
+ [--tests-path=TESTS_PATH] # Path to the tests files
169
+ # Default: ./benchmarks/tests
170
+ ```
26
171
 
27
172
  ## Development
28
173
 
29
174
  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
175
 
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
176
  ## Contributing
34
177
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/awfy.
178
+ Bug reports and pull requests are welcome on GitHub at https://github.com/stevegeek/awfy.
36
179
 
37
180
  ## License
38
181
 
data/lib/awfy/cli.rb CHANGED
@@ -25,7 +25,6 @@ module Awfy
25
25
 
26
26
  class_option :summary, type: :boolean, desc: "Generate a summary of the results", default: true
27
27
  class_option :verbose, type: :boolean, desc: "Verbose output", default: false
28
- class_option :quiet, type: :boolean, desc: "Silence output", default: false
29
28
 
30
29
  class_option :ips_warmup, type: :numeric, default: 1, desc: "Number of seconds to warmup the benchmark"
31
30
  class_option :ips_time, type: :numeric, default: 3, desc: "Number of seconds to run the benchmark"
@@ -48,33 +47,34 @@ module Awfy
48
47
 
49
48
  run_pref_test(group) { run_ips(_1, report, test) }
50
49
  end
51
- #
52
- # desc "memory [GROUP] [REPORT] [TEST]", "Run memory profiling"
53
- # def memory(group = nil, report = nil, test = nil)
54
- # say "Running memory profiling for:"
55
- # say "> #{requested_tests(group, report, test)}..."
56
- #
57
- # run_pref_test(group) { run_memory(_1, report, test) }
58
- # end
59
- #
60
- # desc "flamegraph GROUP REPORT TEST", "Run flamegraph profiling"
61
- # def flamegraph(group, report, test)
62
- # say "Creating flamegraph for:"
63
- # say "> #{[group, report, test].join("/")}..."
64
- # configure_benchmark_run
65
- # run_group(group) { run_flamegraph(_1, report, test) }
66
- # end
67
- #
50
+
51
+ desc "memory [GROUP] [REPORT] [TEST]", "Run memory profiling"
52
+ def memory(group = nil, report = nil, test = nil)
53
+ say "Running memory profiling for:"
54
+ say "> #{requested_tests(group, report, test)}..."
55
+
56
+ run_pref_test(group) { run_memory(_1, report, test) }
57
+ end
58
+
59
+ desc "flamegraph GROUP REPORT TEST", "Run flamegraph profiling"
60
+ def flamegraph(group, report, test)
61
+ say "Creating flamegraph for:"
62
+ say "> #{[group, report, test].join("/")}..."
63
+
64
+ configure_benchmark_run
65
+ run_group(group) { run_flamegraph(_1, report, test) }
66
+ end
67
+
68
68
  # # TODO: also YJIT stats output?
69
- # desc "profile [GROUP] [REPORT] [TEST]", "Run CPU profiling"
70
- # option :iterations, type: :numeric, default: 1_000_000, desc: "Number of iterations to run the test"
71
- # def profile(group = nil, report = nil, test = nil)
72
- # say "Run profiling of:"
73
- # say "> #{requested_tests(group, report, test)}..."
74
- #
75
- # configure_benchmark_run
76
- # run_group(group) { run_profiling(_1, report, test) }
77
- # end
69
+ desc "profile [GROUP] [REPORT] [TEST]", "Run CPU profiling"
70
+ option :iterations, type: :numeric, default: 1_000_000, desc: "Number of iterations to run the test"
71
+ def profile(group = nil, report = nil, test = nil)
72
+ say "Run profiling of:"
73
+ say "> #{requested_tests(group, report, test)}..."
74
+
75
+ configure_benchmark_run
76
+ run_group(group) { run_profiling(_1, report, test) }
77
+ end
78
78
 
79
79
  private
80
80
 
@@ -147,7 +147,7 @@ module Awfy
147
147
  prepare_output_directory_for_ips
148
148
 
149
149
  execute_report(group, report_name) do |report, runtime|
150
- Benchmark.ips(time: options[:ips_time], warmup: options[:ips_warmup], quiet: quiet_steps?) do |bm|
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
152
  test_label = "[#{runtime}] #{test[:control] ? CONTROL_MARKER : TEST_MARKER} #{test[:name]}"
153
153
  bm.item(test_label, &test[:block])
@@ -158,50 +158,56 @@ module Awfy
158
158
  bm.save!(file_name)
159
159
  end
160
160
 
161
- bm.compare! if verbose?
161
+ bm.compare! if verbose? || !show_summary?
162
162
  end
163
163
  end
164
164
 
165
165
  generate_ips_summary if options[:summary]
166
166
  end
167
- #
168
- # def run_memory(group, report_name, test_name)
169
- # say "> Memory profiling for #{group[:name]}...", :cyan if verbose?
170
- # execute_report(group, report_name) do |report, runtime|
171
- # execute_tests(report, test_name) do |test, _|
172
- # MemoryProfiler.report do
173
- # test[:block].call
174
- # end.pretty_print
175
- # end
176
- # end
177
- # end
178
- #
179
- # def run_flamegraph(group, report_name, test_name)
180
- # execute_report(group, report_name) do |report, runtime|
181
- # execute_tests(report, test_name) do |test, _|
182
- # label = "report-#{group[:name]}-#{report[:name]}-#{test[:name]}".gsub(/[^A-Za-z0-9_\-]/, "_")
183
- # generate_flamegraph(label) do
184
- # test[:block].call
185
- # end
186
- # end
187
- # end
188
- # end
189
- #
190
- # def run_profiling(group, report_name, test_name)
191
- # say "> Profiling for #{group[:name]} (iterations: #{options[:iterations]})..." if verbose?
192
- # execute_report(group, report_name) do |report, runtime|
193
- # execute_tests(report, test_name) do |test, iterations|
194
- # data = StackProf.run(mode: :cpu, interval: 100) do
195
- # i = 0
196
- # while i < iterations
197
- # test[:block].call
198
- # i += 1
199
- # end
200
- # end
201
- # StackProf::Report.new(data).print_text
202
- # end
203
- # end
204
- # end
167
+
168
+ def run_memory(group, report_name, test_name)
169
+ if verbose?
170
+ say "> Memory profiling for:"
171
+ say "> #{group[:name]}...", :cyan
172
+ end
173
+ execute_report(group, report_name) do |report, runtime|
174
+ execute_tests(report, test_name) do |test, _|
175
+ MemoryProfiler.report do
176
+ test[:block].call
177
+ end.pretty_print
178
+ end
179
+ end
180
+ end
181
+
182
+ def run_flamegraph(group, report_name, test_name)
183
+ execute_report(group, report_name) do |report, runtime|
184
+ execute_tests(report, test_name) do |test, _|
185
+ label = "report-#{group[:name]}-#{report[:name]}-#{test[:name]}".gsub(/[^A-Za-z0-9_\-]/, "_")
186
+ generate_flamegraph(label) do
187
+ test[:block].call
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ def run_profiling(group, report_name, test_name)
194
+ if verbose?
195
+ say "> Profiling for:"
196
+ say "> #{group[:name]} (iterations: #{options[:iterations]})...", :cyan
197
+ end
198
+ execute_report(group, report_name) do |report, runtime|
199
+ execute_tests(report, test_name) do |test, iterations|
200
+ data = StackProf.run(mode: :cpu, interval: 100) do
201
+ i = 0
202
+ while i < iterations
203
+ test[:block].call
204
+ i += 1
205
+ end
206
+ end
207
+ StackProf::Report.new(data).print_text
208
+ end
209
+ end
210
+ end
205
211
 
206
212
  def execute_report(group, report_name, &)
207
213
  runtime = options[:runtime]
@@ -242,7 +248,7 @@ module Awfy
242
248
 
243
249
  say if verbose?
244
250
  say "> --------------------------" if verbose?
245
- say "> Report (#{runtime} - branch '#{git_current_branch_name}'): #{report[:name]}"
251
+ say "> [#{runtime} - branch '#{git_current_branch_name}'] #{group[:name]} / #{report[:name]}", :magenta
246
252
  say "> --------------------------" if verbose?
247
253
  say if verbose?
248
254
  yield run_report, runtime
@@ -369,22 +375,28 @@ module Awfy
369
375
  else
370
376
  "?"
371
377
  end
372
- test_name = result[:is_baseline] ? "#{result[:test_name]} (baseline)" : result[:test_name]
378
+ test_name = result[:is_baseline] ? "(baseline) #{result[:test_name]}" : result[:test_name]
373
379
 
374
- [result[:branch], result[:runtime], test_name, result[:stats].central_tendency.round, diff_message]
380
+ [result[:branch], result[:runtime], test_name, Benchmark::IPS::Helpers.scale(result[:stats].central_tendency.round), diff_message]
375
381
  end
376
382
 
377
383
  group_data = report.first
378
384
  table = ::Terminal::Table.new(
379
- title: "Summary for #{requested_tests(group_data[:group], group_data[:report])}",
380
- headings: ["Branch", "Runtime", "Name", "IPS", "Diff v baseline (times)"],
381
- rows: rows
385
+ title: requested_tests(group_data[:group], group_data[:report]),
386
+ headings: ["Branch", "Runtime", "Name", "IPS", "Vs baseline"]
382
387
  )
383
388
 
384
389
  table.align_column(2, :right)
385
390
  table.align_column(3, :right)
386
391
  table.align_column(4, :right)
387
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
+
388
400
  say table
389
401
  end
390
402
  end
@@ -403,7 +415,8 @@ module Awfy
403
415
  ensure
404
416
  say "Switching back to branch '#{previous_branch}'" if verbose?
405
417
  git_client.checkout(previous_branch)
406
- git_client.lib.stash_apply(0)
418
+ # Git client does not have a pop method so send our own command
419
+ git_client.lib.send(:command, "stash", "pop")
407
420
  end
408
421
 
409
422
  def git_current_branch_name = git_client.current_branch
@@ -416,6 +429,6 @@ module Awfy
416
429
 
417
430
  def verbose? = options[:verbose]
418
431
 
419
- def quiet_steps? = options[:quiet] || options[:summary]
432
+ def show_summary? = options[:summary]
420
433
  end
421
434
  end
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.1.0"
4
+ VERSION = "0.3.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.1.0
4
+ version: 0.3.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