awfy 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +153 -10
- data/lib/awfy/cli.rb +88 -75
- data/lib/awfy/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b3e0b8df98fcef36e265d46ab9416c1b7cd529116fe57a4b9470a95d04427d0
|
4
|
+
data.tar.gz: a7d9962369da0320d5d97cfb32db82ff06b52d72425e22e4134759c8a549a140
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
5
|
+
The benchmarks are written using a simple DSL in your target project.
|
6
6
|
|
7
|
-
|
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
|
-
|
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
|
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
|
27
|
+
gem install awfy
|
21
28
|
```
|
22
29
|
|
23
30
|
## Usage
|
24
31
|
|
25
|
-
|
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/
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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:
|
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
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
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 ">
|
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]}
|
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:
|
380
|
-
headings: ["Branch", "Runtime", "Name", "IPS", "
|
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
|
-
|
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
|
432
|
+
def show_summary? = options[:summary]
|
420
433
|
end
|
421
434
|
end
|
data/lib/awfy/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2024-10-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|