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.
- checksums.yaml +4 -4
- data/README.md +39 -10
- data/lib/awfy/cli.rb +193 -61
- data/lib/awfy/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9e397437b1d2fddfcad8796896160185c10f1832f98468e7b828b4e7e717a9e
|
4
|
+
data.tar.gz: 055a0bb06e33dcd7b77bd1231b1adce331f8a7e2966a0bdf014bd597d12bea2f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 |
|
124
|
-
| perf | yjit |
|
125
|
-
| perf | yjit |
|
126
|
-
| main | yjit |
|
127
|
-
| perf | mri |
|
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 |
|
130
|
-
| perf | yjit |
|
131
|
-
| perf | mri |
|
132
|
-
| perf | yjit |
|
133
|
-
| perf | mri |
|
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 =
|
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
|
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
|
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
|
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
|
320
|
-
|
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
|
-
|
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
|
-
|
338
|
-
|
339
|
-
|
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
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
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 =
|
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,
|
529
|
+
[result[:branch], result[:runtime], test_name, humanize_scale(result[:stats].central_tendency), diff_message]
|
381
530
|
end
|
382
531
|
|
383
|
-
|
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