test-prof 1.5.2 → 1.6.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/CHANGELOG.md +13 -0
- data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +1 -1
- data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +1 -1
- data/lib/test_prof/memory_prof/printer.rb +31 -0
- data/lib/test_prof/memory_prof/rspec.rb +2 -0
- data/lib/test_prof/memory_prof/tracker.rb +11 -1
- data/lib/test_prof/memory_prof.rb +8 -3
- data/lib/test_prof/rspec_dissect/collector.rb +58 -0
- data/lib/test_prof/rspec_dissect/rspec.rb +13 -21
- data/lib/test_prof/rspec_dissect.rb +45 -51
- data/lib/test_prof/rspec_stamp/parser/prism.rb +75 -0
- data/lib/test_prof/rspec_stamp/parser/ripper.rb +128 -0
- data/lib/test_prof/rspec_stamp/parser.rb +8 -120
- data/lib/test_prof/rspec_stamp.rb +1 -0
- data/lib/test_prof/tps_prof/profiler.rb +52 -15
- data/lib/test_prof/tps_prof/reporter/text.rb +27 -7
- data/lib/test_prof/tps_prof/rspec.rb +25 -2
- data/lib/test_prof/tps_prof.rb +69 -4
- data/lib/test_prof/version.rb +1 -1
- metadata +5 -5
- data/lib/test_prof/rspec_dissect/collectors/base.rb +0 -70
- data/lib/test_prof/rspec_dissect/collectors/before.rb +0 -19
- data/lib/test_prof/rspec_dissect/collectors/let.rb +0 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b843db2a99bf53e8c380e748bb73340c573e1de8082f76519530eb0ddf32cb95
|
|
4
|
+
data.tar.gz: 94048d78eaf4857b0a869fad4d551701b0501074cb97ff9ac9ad13a5b69eee03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5718228b4b63106230f730ef7ee2e1ed53de8457c266f82859a2824b7f9967ba30d0d26d2ee9e4fb1f6363e04eb6a790560e98d529d7e630d93f7dd9dd2dc990
|
|
7
|
+
data.tar.gz: b6df0f87115a3cfd159b1a132459a7958c204374e9b76a8f0c7282158fe6fe6a11a208a5e60f111d98f89c212d215c8cf6db1087c071257a92a7377f999cae0c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## master (unreleased)
|
|
4
4
|
|
|
5
|
+
## 1.6.0 (2026-03-18)
|
|
6
|
+
|
|
7
|
+
- Add TPS profiler. ([@palkan][])
|
|
8
|
+
|
|
9
|
+
- Add GC profiling to MemoryProf. ([@palkan][])
|
|
10
|
+
|
|
11
|
+
- Upgrade to Sidekiq 8. ([@palkan][])
|
|
12
|
+
|
|
13
|
+
- RSpecStamp now uses Prism if available. ([@kddnewton][])
|
|
14
|
+
|
|
15
|
+
- Upgrade RSpecDissect to show total setup time and let breakdowns. ([@palkan][])
|
|
16
|
+
|
|
5
17
|
## 1.5.2 (2026-02-03)
|
|
6
18
|
|
|
7
19
|
- Avoid using `Gem.loaded_specs` methods in RuboCop plugin version check. ([@Rylan12][])
|
|
@@ -489,3 +501,4 @@ See [changelog](https://github.com/test-prof/test-prof/blob/v0.8.0/CHANGELOG.md)
|
|
|
489
501
|
[@devinburnette]: https://github.com/devinburnette
|
|
490
502
|
[@elasticspoon]: https://github.com/elasticspoon
|
|
491
503
|
[@Rylan12]: https://github.com/Rylan12
|
|
504
|
+
[@kddnewton]: https://github.com/kddnewton
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "test_prof/ext/float_duration"
|
|
3
4
|
require "test_prof/memory_prof/printer/number_to_human"
|
|
4
5
|
require "test_prof/ext/string_truncate"
|
|
5
6
|
|
|
@@ -92,5 +93,35 @@ module TestProf
|
|
|
92
93
|
number_to_human(item[:memory])
|
|
93
94
|
end
|
|
94
95
|
end
|
|
96
|
+
|
|
97
|
+
class GCPrinter < Printer
|
|
98
|
+
using StringTruncate
|
|
99
|
+
using FloatDuration
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def mode
|
|
104
|
+
"GC time"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def print_total
|
|
108
|
+
"Total GC time: #{(GC.total_time.to_f / 1_000_000_000).duration}\n\n"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def print_items(items)
|
|
112
|
+
messages =
|
|
113
|
+
items.map do |item|
|
|
114
|
+
<<~ITEM
|
|
115
|
+
#{item[:name].truncate(30)} (#{item[:location]}) – #{gc_time(item)}
|
|
116
|
+
ITEM
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
messages.join
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def gc_time(item)
|
|
123
|
+
(item[:memory].to_f / 1_000_000_000).duration
|
|
124
|
+
end
|
|
125
|
+
end
|
|
95
126
|
end
|
|
96
127
|
end
|
|
@@ -31,10 +31,12 @@ module TestProf
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def example_group_started(notification)
|
|
34
|
+
return unless notification.group.top_level?
|
|
34
35
|
tracker.group_started(notification.group, group(notification))
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def example_group_finished(notification)
|
|
39
|
+
return unless notification.group.top_level?
|
|
38
40
|
tracker.group_finished(notification.group)
|
|
39
41
|
end
|
|
40
42
|
|
|
@@ -55,7 +55,7 @@ module TestProf
|
|
|
55
55
|
node = list.remove_node(id, track)
|
|
56
56
|
return unless node
|
|
57
57
|
|
|
58
|
-
groups << {**node.item, memory: node.
|
|
58
|
+
groups << {**node.item, memory: node.total_memory}
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
@@ -84,5 +84,15 @@ module TestProf
|
|
|
84
84
|
!!@rss_tool
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
|
+
|
|
88
|
+
class GCTracker < Tracker
|
|
89
|
+
def track
|
|
90
|
+
::GC.total_time
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def supported?
|
|
94
|
+
::GC.respond_to?(:total_time)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
87
97
|
end
|
|
88
98
|
end
|
|
@@ -11,6 +11,7 @@ module TestProf
|
|
|
11
11
|
#
|
|
12
12
|
# TEST_MEM_PROF='rss' rspec ...
|
|
13
13
|
# TEST_MEM_PROF='alloc' rspec ...
|
|
14
|
+
# TEST_MEM_PROF='gc' rspec ...
|
|
14
15
|
#
|
|
15
16
|
# By default MemoryProf shows the top 5 examples and groups (for RSpec) but you can
|
|
16
17
|
# set how many items to display with `TEST_MEM_PROF_COUNT`:
|
|
@@ -25,6 +26,8 @@ module TestProf
|
|
|
25
26
|
module MemoryProf
|
|
26
27
|
# MemoryProf configuration
|
|
27
28
|
class Configuration
|
|
29
|
+
MODES = %w[alloc rss gc].freeze
|
|
30
|
+
|
|
28
31
|
attr_reader :mode, :top_count
|
|
29
32
|
|
|
30
33
|
def initialize
|
|
@@ -33,7 +36,7 @@ module TestProf
|
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def mode=(value)
|
|
36
|
-
@mode = (value
|
|
39
|
+
@mode = MODES.include?(value) ? value.to_sym : :rss
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
def top_count=(value)
|
|
@@ -45,12 +48,14 @@ module TestProf
|
|
|
45
48
|
class << self
|
|
46
49
|
TRACKERS = {
|
|
47
50
|
alloc: AllocTracker,
|
|
48
|
-
rss: RssTracker
|
|
51
|
+
rss: RssTracker,
|
|
52
|
+
gc: GCTracker
|
|
49
53
|
}.freeze
|
|
50
54
|
|
|
51
55
|
PRINTERS = {
|
|
52
56
|
alloc: AllocPrinter,
|
|
53
|
-
rss: RssPrinter
|
|
57
|
+
rss: RssPrinter,
|
|
58
|
+
gc: GCPrinter
|
|
54
59
|
}.freeze
|
|
55
60
|
|
|
56
61
|
def config
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_prof/utils/sized_ordered_set"
|
|
4
|
+
require "test_prof/ext/float_duration"
|
|
5
|
+
require "test_prof/ext/string_truncate"
|
|
6
|
+
|
|
7
|
+
module TestProf # :nodoc: all
|
|
8
|
+
using FloatDuration
|
|
9
|
+
using StringTruncate
|
|
10
|
+
|
|
11
|
+
module RSpecDissect
|
|
12
|
+
class Collector
|
|
13
|
+
attr_reader :results, :name, :top_count
|
|
14
|
+
|
|
15
|
+
def initialize(top_count:)
|
|
16
|
+
@top_count = top_count
|
|
17
|
+
@results = Utils::SizedOrderedSet.new(
|
|
18
|
+
top_count, sort_by: :total_setup
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def <<(data)
|
|
23
|
+
results << data
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def print_result_header
|
|
27
|
+
<<~MSG
|
|
28
|
+
|
|
29
|
+
Top #{top_count} slowest suites by setup time:
|
|
30
|
+
|
|
31
|
+
MSG
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def print_group_result(group)
|
|
35
|
+
"#{group[:desc].truncate} (#{group[:loc]}) – \e[1m#{group[:total_setup].duration}\e[22m " \
|
|
36
|
+
"of #{group[:total].duration} / #{group[:count]} " \
|
|
37
|
+
"(before: #{(group[:total_setup] - group[:total_lazy_let]).duration}, " \
|
|
38
|
+
"before let: #{group[:total_before_let].duration}, " \
|
|
39
|
+
"lazy let: #{group[:total_lazy_let].duration})"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def print_results
|
|
43
|
+
msgs = [print_result_header]
|
|
44
|
+
|
|
45
|
+
results.each do |group|
|
|
46
|
+
msgs << print_group_result(group)
|
|
47
|
+
msgs << "\n" if group[:top_lets].any?
|
|
48
|
+
group[:top_lets].each do |let|
|
|
49
|
+
msgs << " ↳ #{let[:name]} – #{let[:duration].duration} (#{let[:size]})\n"
|
|
50
|
+
end
|
|
51
|
+
msgs << "\n"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
msgs.join
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "test_prof/rspec_dissect/collector"
|
|
4
|
+
|
|
3
5
|
require "test_prof/ext/float_duration"
|
|
4
6
|
|
|
5
7
|
module TestProf
|
|
@@ -15,19 +17,12 @@ module TestProf
|
|
|
15
17
|
].freeze
|
|
16
18
|
|
|
17
19
|
def initialize
|
|
18
|
-
@
|
|
19
|
-
|
|
20
|
-
if RSpecDissect.config.let?
|
|
21
|
-
collectors << Collectors::Let.new(top_count: RSpecDissect.config.top_count)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
if RSpecDissect.config.before?
|
|
25
|
-
collectors << Collectors::Before.new(top_count: RSpecDissect.config.top_count)
|
|
26
|
-
end
|
|
20
|
+
@collector = Collector.new(top_count: top_count)
|
|
27
21
|
|
|
28
22
|
@examples_count = 0
|
|
29
23
|
@examples_time = 0.0
|
|
30
24
|
@total_examples_time = 0.0
|
|
25
|
+
@total_setup_time = 0.0
|
|
31
26
|
end
|
|
32
27
|
|
|
33
28
|
def example_finished(notification)
|
|
@@ -44,9 +39,11 @@ module TestProf
|
|
|
44
39
|
data[:desc] = notification.group.top_level_description
|
|
45
40
|
data[:loc] = notification.group.metadata[:location]
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
collectors.each { |c| c << data }
|
|
42
|
+
RSpecDissect.populate_from_spans!(data)
|
|
49
43
|
|
|
44
|
+
collector << data
|
|
45
|
+
|
|
46
|
+
@total_setup_time += data[:total_setup]
|
|
50
47
|
@total_examples_time += @examples_time
|
|
51
48
|
@examples_count = 0
|
|
52
49
|
@examples_time = 0.0
|
|
@@ -62,17 +59,12 @@ module TestProf
|
|
|
62
59
|
RSpecDissect report
|
|
63
60
|
|
|
64
61
|
Total time: #{@total_examples_time.duration}
|
|
62
|
+
Total setup time: #{@total_setup_time.duration}
|
|
65
63
|
MSG
|
|
66
64
|
|
|
67
|
-
collectors.each do |c|
|
|
68
|
-
msgs << c.total_time_message
|
|
69
|
-
end
|
|
70
|
-
|
|
71
65
|
msgs << "\n"
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
msgs << c.print_results
|
|
75
|
-
end
|
|
67
|
+
msgs << collector.print_results
|
|
76
68
|
|
|
77
69
|
log :info, msgs.join
|
|
78
70
|
|
|
@@ -84,9 +76,9 @@ module TestProf
|
|
|
84
76
|
|
|
85
77
|
examples = Hash.new { |h, k| h[k] = [] }
|
|
86
78
|
|
|
87
|
-
|
|
79
|
+
results = collector.results.to_a
|
|
88
80
|
|
|
89
|
-
|
|
81
|
+
results
|
|
90
82
|
.map { |obj| obj[:loc] }.each do |location|
|
|
91
83
|
file, line = location.split(":")
|
|
92
84
|
examples[file] << line.to_i
|
|
@@ -114,7 +106,7 @@ module TestProf
|
|
|
114
106
|
|
|
115
107
|
private
|
|
116
108
|
|
|
117
|
-
attr_reader :
|
|
109
|
+
attr_reader :collector
|
|
118
110
|
|
|
119
111
|
def top_count
|
|
120
112
|
RSpecDissect.config.top_count
|
|
@@ -7,6 +7,9 @@ module TestProf
|
|
|
7
7
|
# RSpecDissect tracks how much time do you spend in `before` hooks
|
|
8
8
|
# and memoization helpers (i.e. `let`) in your tests.
|
|
9
9
|
module RSpecDissect
|
|
10
|
+
class Span < Struct.new(:id, :parent_id, :type, :duration, :meta)
|
|
11
|
+
end
|
|
12
|
+
|
|
10
13
|
module ExampleInstrumentation # :nodoc:
|
|
11
14
|
def run_before_example(*)
|
|
12
15
|
RSpecDissect.track(:before) { super }
|
|
@@ -15,12 +18,15 @@ module TestProf
|
|
|
15
18
|
|
|
16
19
|
module MemoizedInstrumentation # :nodoc:
|
|
17
20
|
def fetch_or_store(id, *)
|
|
21
|
+
return super if id == :subject
|
|
22
|
+
return @memoized[id] if @memoized[id]
|
|
23
|
+
|
|
18
24
|
res = nil
|
|
19
25
|
Thread.current[:_rspec_dissect_let_depth] ||= 0
|
|
20
26
|
Thread.current[:_rspec_dissect_let_depth] += 1
|
|
21
27
|
begin
|
|
22
28
|
res = if Thread.current[:_rspec_dissect_let_depth] == 1
|
|
23
|
-
RSpecDissect.track(:let, id) { super }
|
|
29
|
+
RSpecDissect.track(:let, name: id) { super }
|
|
24
30
|
else
|
|
25
31
|
super
|
|
26
32
|
end
|
|
@@ -33,45 +39,25 @@ module TestProf
|
|
|
33
39
|
|
|
34
40
|
# RSpecDisect configuration
|
|
35
41
|
class Configuration
|
|
36
|
-
MODES = %w[all let before].freeze
|
|
37
|
-
|
|
38
42
|
attr_accessor :top_count, :let_stats_enabled,
|
|
39
43
|
:let_top_count
|
|
40
44
|
|
|
41
45
|
alias_method :let_stats_enabled?, :let_stats_enabled
|
|
42
46
|
|
|
43
|
-
attr_reader :mode
|
|
44
|
-
|
|
45
47
|
def initialize
|
|
46
48
|
@let_stats_enabled = true
|
|
47
49
|
@let_top_count = (ENV["RD_PROF_LET_TOP"] || 3).to_i
|
|
48
50
|
@top_count = (ENV["RD_PROF_TOP"] || 5).to_i
|
|
49
51
|
@stamp = ENV["RD_PROF_STAMP"]
|
|
50
|
-
@mode = (ENV["RD_PROF"] == "1") ? "all" : ENV["RD_PROF"]
|
|
51
|
-
|
|
52
|
-
unless MODES.include?(mode)
|
|
53
|
-
raise "Unknown RSpecDissect mode: #{mode};" \
|
|
54
|
-
"available modes: #{MODES.join(", ")}"
|
|
55
|
-
end
|
|
56
52
|
|
|
57
53
|
RSpecStamp.config.tags = @stamp if stamp?
|
|
58
54
|
end
|
|
59
55
|
|
|
60
|
-
def let?
|
|
61
|
-
mode == "all" || mode == "let"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def before?
|
|
65
|
-
mode == "all" || mode == "before"
|
|
66
|
-
end
|
|
67
|
-
|
|
68
56
|
def stamp?
|
|
69
57
|
!@stamp.nil?
|
|
70
58
|
end
|
|
71
59
|
end
|
|
72
60
|
|
|
73
|
-
METRICS = %w[before let].freeze
|
|
74
|
-
|
|
75
61
|
class << self
|
|
76
62
|
include Logging
|
|
77
63
|
|
|
@@ -89,56 +75,64 @@ module TestProf
|
|
|
89
75
|
RSpec::Core::MemoizedHelpers::ThreadsafeMemoized.prepend(MemoizedInstrumentation)
|
|
90
76
|
RSpec::Core::MemoizedHelpers::NonThreadSafeMemoized.prepend(MemoizedInstrumentation)
|
|
91
77
|
|
|
92
|
-
@data = {}
|
|
93
|
-
|
|
94
|
-
METRICS.each do |type|
|
|
95
|
-
@data["total_#{type}"] = 0.0
|
|
96
|
-
end
|
|
97
|
-
|
|
98
78
|
reset!
|
|
99
79
|
|
|
100
80
|
log :info, "RSpecDissect enabled"
|
|
101
81
|
end
|
|
102
82
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
delta = (TestProf.now - start)
|
|
107
|
-
type = type.to_s
|
|
108
|
-
@data[type][:time] += delta
|
|
109
|
-
@data[type][:meta] << meta unless meta.nil?
|
|
110
|
-
@data["total_#{type}"] += delta
|
|
111
|
-
res
|
|
83
|
+
def nextid
|
|
84
|
+
@last_id += 1
|
|
85
|
+
@last_id.to_s
|
|
112
86
|
end
|
|
113
87
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
@data[type.to_s] = {time: 0.0, meta: []}
|
|
117
|
-
end
|
|
88
|
+
def current_span
|
|
89
|
+
Thread.current[:_rspec_dissect_spans_stack].last
|
|
118
90
|
end
|
|
119
91
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
defined?(::RSpec::Core::MemoizedHelpers::ThreadsafeMemoized)
|
|
92
|
+
def span_stack
|
|
93
|
+
Thread.current[:_rspec_dissect_spans_stack]
|
|
123
94
|
end
|
|
124
95
|
|
|
125
|
-
def
|
|
126
|
-
|
|
96
|
+
def track(type, id: nextid, **meta)
|
|
97
|
+
span = Span.new(id, current_span&.id, type, 0.0, meta)
|
|
98
|
+
span_stack << span
|
|
99
|
+
|
|
100
|
+
begin
|
|
101
|
+
start = TestProf.now
|
|
102
|
+
res = yield
|
|
103
|
+
delta = (TestProf.now - start)
|
|
104
|
+
span.duration = delta
|
|
105
|
+
@spans << span
|
|
106
|
+
res
|
|
107
|
+
ensure
|
|
108
|
+
span_stack.pop
|
|
109
|
+
end
|
|
127
110
|
end
|
|
128
111
|
|
|
129
|
-
def
|
|
130
|
-
|
|
112
|
+
def populate_from_spans!(data)
|
|
113
|
+
data[:total_setup] = @spans.select { !_1.parent_id }.sum(&:duration)
|
|
114
|
+
data[:total_before_let] = @spans.select { _1.type == :let && _1.parent_id }.sum(&:duration).to_f
|
|
115
|
+
data[:total_lazy_let] = @spans.select { _1.type == :let && !_1.parent_id }.sum(&:duration).to_f
|
|
116
|
+
|
|
117
|
+
data[:top_lets] = @spans.select { _1.type == :let }
|
|
118
|
+
.group_by { _1.meta[:name] }
|
|
119
|
+
.transform_values! do |spans|
|
|
120
|
+
{name: spans.first.meta[:name], duration: spans.sum(&:duration), size: spans.size}
|
|
121
|
+
end
|
|
122
|
+
.values
|
|
123
|
+
.sort_by { -_1[:duration] }
|
|
124
|
+
.take(RSpecDissect.config.let_top_count)
|
|
131
125
|
end
|
|
132
126
|
|
|
133
|
-
def
|
|
134
|
-
@
|
|
127
|
+
def reset!
|
|
128
|
+
@last_id = 1
|
|
129
|
+
@spans = []
|
|
130
|
+
Thread.current[:_rspec_dissect_spans_stack] = []
|
|
135
131
|
end
|
|
136
132
|
end
|
|
137
133
|
end
|
|
138
134
|
end
|
|
139
135
|
|
|
140
|
-
require "test_prof/rspec_dissect/collectors/let"
|
|
141
|
-
require "test_prof/rspec_dissect/collectors/before"
|
|
142
136
|
require "test_prof/rspec_dissect/rspec"
|
|
143
137
|
|
|
144
138
|
TestProf.activate("RD_PROF") do
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module TestProf
|
|
6
|
+
module RSpecStamp
|
|
7
|
+
module Parser
|
|
8
|
+
class Prism
|
|
9
|
+
def parse(code)
|
|
10
|
+
result = ::Prism.parse(code)
|
|
11
|
+
return unless result.success?
|
|
12
|
+
|
|
13
|
+
node = result.value.statements.body.first
|
|
14
|
+
return unless node.is_a?(::Prism::CallNode)
|
|
15
|
+
|
|
16
|
+
res = Result.new
|
|
17
|
+
res.fname =
|
|
18
|
+
if node.receiver
|
|
19
|
+
"#{node.receiver.full_name}.#{node.name}"
|
|
20
|
+
else
|
|
21
|
+
node.name.name
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
args = node.arguments&.arguments
|
|
25
|
+
return res if args.nil?
|
|
26
|
+
|
|
27
|
+
rest =
|
|
28
|
+
case (first = args.first).type
|
|
29
|
+
when :string_node
|
|
30
|
+
res.desc = first.content
|
|
31
|
+
args[1..]
|
|
32
|
+
when :constant_read_node, :constant_path_node
|
|
33
|
+
res.desc_const = first.full_name
|
|
34
|
+
args[1..]
|
|
35
|
+
else
|
|
36
|
+
args
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
rest.each do |arg|
|
|
40
|
+
case arg.type
|
|
41
|
+
when :symbol_node
|
|
42
|
+
res.add_tag(arg.value.to_sym)
|
|
43
|
+
when :keyword_hash_node
|
|
44
|
+
arg.elements.each do |assoc|
|
|
45
|
+
res.add_htag(
|
|
46
|
+
assoc.key.value.to_sym,
|
|
47
|
+
parse_htag_value(assoc.value)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
res
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def parse_htag_value(node)
|
|
59
|
+
case node.type
|
|
60
|
+
when :true_node
|
|
61
|
+
true
|
|
62
|
+
when :false_node
|
|
63
|
+
false
|
|
64
|
+
when :integer_node, :float_node
|
|
65
|
+
node.value
|
|
66
|
+
when :string_node
|
|
67
|
+
node.unescaped
|
|
68
|
+
when :symbol_node
|
|
69
|
+
node.value.to_sym
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ripper"
|
|
4
|
+
|
|
5
|
+
module TestProf
|
|
6
|
+
module RSpecStamp
|
|
7
|
+
module Parser
|
|
8
|
+
class Ripper
|
|
9
|
+
def parse(code)
|
|
10
|
+
sexp = ::Ripper.sexp(code)
|
|
11
|
+
return unless sexp
|
|
12
|
+
|
|
13
|
+
# sexp has the following format:
|
|
14
|
+
# [:program,
|
|
15
|
+
# [
|
|
16
|
+
# [
|
|
17
|
+
# :command,
|
|
18
|
+
# [:@ident, "it", [1, 0]],
|
|
19
|
+
# [:args_add_block, [ ... ]]
|
|
20
|
+
# ]
|
|
21
|
+
# ]
|
|
22
|
+
# ]
|
|
23
|
+
#
|
|
24
|
+
# or
|
|
25
|
+
#
|
|
26
|
+
# [:program,
|
|
27
|
+
# [
|
|
28
|
+
# [
|
|
29
|
+
# :vcall,
|
|
30
|
+
# [:@ident, "it", [1, 0]]
|
|
31
|
+
# ]
|
|
32
|
+
# ]
|
|
33
|
+
# ]
|
|
34
|
+
res = Result.new
|
|
35
|
+
|
|
36
|
+
fcall = sexp[1][0][1]
|
|
37
|
+
args_block = sexp[1][0][2]
|
|
38
|
+
|
|
39
|
+
if fcall.first == :fcall
|
|
40
|
+
fcall = fcall[1]
|
|
41
|
+
elsif fcall.first == :var_ref
|
|
42
|
+
res.fname = [parse_const(fcall), sexp[1][0][3][1]].join(".")
|
|
43
|
+
args_block = sexp[1][0][4]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
res.fname ||= fcall[1]
|
|
47
|
+
|
|
48
|
+
return res if args_block.nil?
|
|
49
|
+
|
|
50
|
+
args_block = args_block[1] if args_block.first == :arg_paren
|
|
51
|
+
|
|
52
|
+
args = args_block[1]
|
|
53
|
+
|
|
54
|
+
if args.first.first == :string_literal
|
|
55
|
+
res.desc = parse_literal(args.shift)
|
|
56
|
+
elsif args.first.first == :var_ref || args.first.first == :const_path_ref
|
|
57
|
+
res.desc_const = parse_const(args.shift)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
parse_arg(res, args.shift) until args.empty?
|
|
61
|
+
|
|
62
|
+
res
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def parse_arg(res, arg)
|
|
68
|
+
if arg.first == :symbol_literal
|
|
69
|
+
res.add_tag parse_literal(arg)
|
|
70
|
+
elsif arg.first == :bare_assoc_hash
|
|
71
|
+
parse_hash(res, arg[1])
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_hash(res, hash_arg)
|
|
76
|
+
hash_arg.each do |(_, label, val)|
|
|
77
|
+
res.add_htag label[1][0..-2].to_sym, parse_value(val)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Expr of the form:
|
|
82
|
+
# bool - [:var_ref, [:@kw, "true", [1, 24]]]
|
|
83
|
+
# string - [:string_literal, [:string_content, [...]]]
|
|
84
|
+
# int - [:@int, "3", [1, 52]]]]
|
|
85
|
+
def parse_value(expr)
|
|
86
|
+
case expr.first
|
|
87
|
+
when :var_ref
|
|
88
|
+
expr[1][1] == "true"
|
|
89
|
+
when :@int
|
|
90
|
+
expr[1].to_i
|
|
91
|
+
when :@float
|
|
92
|
+
expr[1].to_f
|
|
93
|
+
else
|
|
94
|
+
parse_literal(expr)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Expr of the form:
|
|
99
|
+
# [:string_literal, [:string_content, [:@tstring_content, "is", [1, 4]]]]
|
|
100
|
+
def parse_literal(expr)
|
|
101
|
+
val = expr[1][1][1]
|
|
102
|
+
val = val.to_sym if expr[0] == :symbol_literal ||
|
|
103
|
+
expr[0] == :assoc_new
|
|
104
|
+
val
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Expr of the form:
|
|
108
|
+
# [:var_ref, [:@const, "User", [1, 9]]]
|
|
109
|
+
#
|
|
110
|
+
# or
|
|
111
|
+
#
|
|
112
|
+
# [:const_path_ref, [:const_path_ref, [:var_ref,
|
|
113
|
+
# [:@const, "User", [1, 17]]],
|
|
114
|
+
# [:@const, "Guest", [1, 23]]],
|
|
115
|
+
# [:@const, "Collection", [1, 30]]
|
|
116
|
+
def parse_const(expr)
|
|
117
|
+
if expr.first == :var_ref
|
|
118
|
+
expr[1][1]
|
|
119
|
+
elsif expr.first == :@const
|
|
120
|
+
expr[1]
|
|
121
|
+
elsif expr.first == :const_path_ref
|
|
122
|
+
expr[1..].map(&method(:parse_const)).join("::")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ripper"
|
|
4
|
-
|
|
5
3
|
module TestProf
|
|
6
4
|
module RSpecStamp
|
|
7
5
|
# Parse examples headers
|
|
@@ -27,126 +25,16 @@ module TestProf
|
|
|
27
25
|
end
|
|
28
26
|
end
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# [
|
|
38
|
-
# [
|
|
39
|
-
# :command,
|
|
40
|
-
# [:@ident, "it", [1, 0]],
|
|
41
|
-
# [:args_add_block, [ ... ]]
|
|
42
|
-
# ]
|
|
43
|
-
# ]
|
|
44
|
-
# ]
|
|
45
|
-
#
|
|
46
|
-
# or
|
|
47
|
-
#
|
|
48
|
-
# [:program,
|
|
49
|
-
# [
|
|
50
|
-
# [
|
|
51
|
-
# :vcall,
|
|
52
|
-
# [:@ident, "it", [1, 0]]
|
|
53
|
-
# ]
|
|
54
|
-
# ]
|
|
55
|
-
# ]
|
|
56
|
-
res = Result.new
|
|
57
|
-
|
|
58
|
-
fcall = sexp[1][0][1]
|
|
59
|
-
args_block = sexp[1][0][2]
|
|
60
|
-
|
|
61
|
-
if fcall.first == :fcall
|
|
62
|
-
fcall = fcall[1]
|
|
63
|
-
elsif fcall.first == :var_ref
|
|
64
|
-
res.fname = [parse_const(fcall), sexp[1][0][3][1]].join(".")
|
|
65
|
-
args_block = sexp[1][0][4]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
res.fname ||= fcall[1]
|
|
69
|
-
|
|
70
|
-
return res if args_block.nil?
|
|
71
|
-
|
|
72
|
-
args_block = args_block[1] if args_block.first == :arg_paren
|
|
73
|
-
|
|
74
|
-
args = args_block[1]
|
|
75
|
-
|
|
76
|
-
if args.first.first == :string_literal
|
|
77
|
-
res.desc = parse_literal(args.shift)
|
|
78
|
-
elsif args.first.first == :var_ref || args.first.first == :const_path_ref
|
|
79
|
-
res.desc_const = parse_const(args.shift)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
parse_arg(res, args.shift) until args.empty?
|
|
83
|
-
|
|
84
|
-
res
|
|
28
|
+
instance =
|
|
29
|
+
begin
|
|
30
|
+
require_relative "parser/prism"
|
|
31
|
+
self::Prism.new
|
|
32
|
+
rescue LoadError
|
|
33
|
+
require_relative "parser/ripper"
|
|
34
|
+
self::Ripper.new
|
|
85
35
|
end
|
|
86
|
-
# rubocop: enable Metrics/CyclomaticComplexity
|
|
87
|
-
# rubocop: enable Metrics/PerceivedComplexity
|
|
88
36
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def parse_arg(res, arg)
|
|
92
|
-
if arg.first == :symbol_literal
|
|
93
|
-
res.add_tag parse_literal(arg)
|
|
94
|
-
elsif arg.first == :bare_assoc_hash
|
|
95
|
-
parse_hash(res, arg[1])
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def parse_hash(res, hash_arg)
|
|
100
|
-
hash_arg.each do |(_, label, val)|
|
|
101
|
-
res.add_htag label[1][0..-2].to_sym, parse_value(val)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Expr of the form:
|
|
106
|
-
# bool - [:var_ref, [:@kw, "true", [1, 24]]]
|
|
107
|
-
# string - [:string_literal, [:string_content, [...]]]
|
|
108
|
-
# int - [:@int, "3", [1, 52]]]]
|
|
109
|
-
def parse_value(expr)
|
|
110
|
-
case expr.first
|
|
111
|
-
when :var_ref
|
|
112
|
-
expr[1][1] == "true"
|
|
113
|
-
when :@int
|
|
114
|
-
expr[1].to_i
|
|
115
|
-
when :@float
|
|
116
|
-
expr[1].to_f
|
|
117
|
-
else
|
|
118
|
-
parse_literal(expr)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Expr of the form:
|
|
123
|
-
# [:string_literal, [:string_content, [:@tstring_content, "is", [1, 4]]]]
|
|
124
|
-
def parse_literal(expr)
|
|
125
|
-
val = expr[1][1][1]
|
|
126
|
-
val = val.to_sym if expr[0] == :symbol_literal ||
|
|
127
|
-
expr[0] == :assoc_new
|
|
128
|
-
val
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Expr of the form:
|
|
132
|
-
# [:var_ref, [:@const, "User", [1, 9]]]
|
|
133
|
-
#
|
|
134
|
-
# or
|
|
135
|
-
#
|
|
136
|
-
# [:const_path_ref, [:const_path_ref, [:var_ref,
|
|
137
|
-
# [:@const, "User", [1, 17]]],
|
|
138
|
-
# [:@const, "Guest", [1, 23]]],
|
|
139
|
-
# [:@const, "Collection", [1, 30]]
|
|
140
|
-
def parse_const(expr)
|
|
141
|
-
if expr.first == :var_ref
|
|
142
|
-
expr[1][1]
|
|
143
|
-
elsif expr.first == :@const
|
|
144
|
-
expr[1]
|
|
145
|
-
elsif expr.first == :const_path_ref
|
|
146
|
-
expr[1..].map(&method(:parse_const)).join("::")
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
37
|
+
define_singleton_method(:parse) { |code| instance.parse(code) }
|
|
150
38
|
end
|
|
151
39
|
end
|
|
152
40
|
end
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "test_prof/utils/sized_ordered_set"
|
|
4
|
+
require "forwardable"
|
|
4
5
|
|
|
5
6
|
module TestProf
|
|
6
7
|
module TPSProf
|
|
7
8
|
class Profiler
|
|
8
|
-
|
|
9
|
+
extend Forwardable
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
attr_reader :top_count, :groups, :total_count, :total_time,
|
|
12
|
+
:config
|
|
13
|
+
|
|
14
|
+
def_delegators :@config, :min_examples_count, :min_group_time, :min_target_tps,
|
|
15
|
+
:mode, :max_examples_count, :max_group_time, :min_tps
|
|
16
|
+
|
|
17
|
+
def initialize(top_count, config)
|
|
18
|
+
@config = config
|
|
19
|
+
# In strict mode, we use the sorted set to keep track of offenders
|
|
20
|
+
# to show in the end
|
|
21
|
+
@top_count = (config.mode == :strict) ? 100 : top_count
|
|
13
22
|
@total_count = 0
|
|
14
23
|
@total_time = 0.0
|
|
15
|
-
@groups = Utils::SizedOrderedSet.new(top_count, sort_by: :
|
|
24
|
+
@groups = Utils::SizedOrderedSet.new(top_count, sort_by: :penalty)
|
|
16
25
|
end
|
|
17
26
|
|
|
18
27
|
def group_started(id)
|
|
@@ -22,20 +31,48 @@ module TestProf
|
|
|
22
31
|
@group_started_at = TestProf.now
|
|
23
32
|
end
|
|
24
33
|
|
|
25
|
-
def group_finished(
|
|
26
|
-
return unless @examples_count >=
|
|
34
|
+
def group_finished(group)
|
|
35
|
+
return unless @examples_count >= min_examples_count
|
|
36
|
+
|
|
37
|
+
total_time = TestProf.now - @group_started_at
|
|
38
|
+
shared_setup_time = total_time - @examples_time
|
|
39
|
+
|
|
40
|
+
return unless total_time >= min_group_time
|
|
27
41
|
|
|
28
|
-
|
|
29
|
-
group_time = (TestProf.now - @group_started_at) - @examples_time
|
|
30
|
-
run_time = @examples_time + group_time
|
|
42
|
+
tps = (@examples_count / total_time).round(2)
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
return unless tps < min_target_tps
|
|
45
|
+
|
|
46
|
+
# How much time did we waste compared to the target TPS
|
|
47
|
+
penalty = @examples_count * ((1.0 / tps) - (1.0 / min_target_tps))
|
|
48
|
+
|
|
49
|
+
item = {
|
|
50
|
+
id: group,
|
|
51
|
+
total_time: total_time,
|
|
52
|
+
shared_setup_time: shared_setup_time,
|
|
36
53
|
count: @examples_count,
|
|
37
|
-
tps:
|
|
54
|
+
tps: tps,
|
|
55
|
+
penalty: penalty
|
|
38
56
|
}
|
|
57
|
+
|
|
58
|
+
if mode == :strict
|
|
59
|
+
location = group.metadata[:location]
|
|
60
|
+
|
|
61
|
+
if TPSProf.handle_group_strictly(
|
|
62
|
+
GroupInfo.new(
|
|
63
|
+
group: group,
|
|
64
|
+
location: location,
|
|
65
|
+
examples_count: @examples_count,
|
|
66
|
+
total_time: total_time,
|
|
67
|
+
tps: tps,
|
|
68
|
+
penalty: penalty
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
groups << item
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
groups << item
|
|
75
|
+
end
|
|
39
76
|
end
|
|
40
77
|
|
|
41
78
|
def example_started(id)
|
|
@@ -23,25 +23,45 @@ module TestProf
|
|
|
23
23
|
<<~MSG
|
|
24
24
|
Total TPS (tests per second): #{total_tps}
|
|
25
25
|
|
|
26
|
-
Top #{profiler.top_count} slowest suites by TPS (tests per second) (min examples per group: #{profiler.threshold}):
|
|
27
|
-
|
|
28
26
|
MSG
|
|
29
27
|
|
|
28
|
+
if profiler.mode == :strict
|
|
29
|
+
return if groups.empty?
|
|
30
|
+
|
|
31
|
+
msgs << if groups.size < profiler.top_count
|
|
32
|
+
<<~MSG
|
|
33
|
+
Suites violating TPS limits:
|
|
34
|
+
|
|
35
|
+
MSG
|
|
36
|
+
else
|
|
37
|
+
<<~MSG
|
|
38
|
+
Top #{profiler.top_count} suites violating TPS limits:
|
|
39
|
+
|
|
40
|
+
MSG
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
msgs <<
|
|
44
|
+
<<~MSG
|
|
45
|
+
Top #{profiler.top_count} slowest suites by TPS (tests per second):
|
|
46
|
+
|
|
47
|
+
MSG
|
|
48
|
+
end
|
|
49
|
+
|
|
30
50
|
groups.each do |group|
|
|
31
51
|
description = group[:id].top_level_description
|
|
32
52
|
location = group[:id].metadata[:location]
|
|
33
|
-
time = group[:
|
|
34
|
-
|
|
53
|
+
time = group[:total_time]
|
|
54
|
+
setup_time = group[:shared_setup_time]
|
|
35
55
|
count = group[:count]
|
|
36
|
-
tps =
|
|
56
|
+
tps = group[:tps]
|
|
37
57
|
|
|
38
58
|
msgs <<
|
|
39
59
|
<<~GROUP
|
|
40
|
-
#{description.truncate} (#{location}) – #{tps} TPS (#{time.duration} / #{count}
|
|
60
|
+
#{description.truncate} (#{location}) – #{tps} TPS (#{time.duration} / #{count}, shared setup time: #{setup_time.duration})
|
|
41
61
|
GROUP
|
|
42
62
|
end
|
|
43
63
|
|
|
44
|
-
log :info, msgs.join
|
|
64
|
+
log((profiler.mode == :strict) ? :error : :info, msgs.join)
|
|
45
65
|
end
|
|
46
66
|
end
|
|
47
67
|
end
|
|
@@ -15,27 +15,50 @@ module TestProf
|
|
|
15
15
|
attr_reader :reporter, :profiler
|
|
16
16
|
|
|
17
17
|
def initialize
|
|
18
|
-
@profiler = Profiler.new(TPSProf.config.top_count,
|
|
18
|
+
@profiler = Profiler.new(TPSProf.config.top_count, TPSProf.config)
|
|
19
19
|
@reporter = TPSProf.config.reporter
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
conf = TPSProf.config
|
|
22
|
+
if conf.mode == :strict
|
|
23
|
+
config_parts = []
|
|
24
|
+
|
|
25
|
+
if conf.custom_strict_handler
|
|
26
|
+
config_parts << "custom handler"
|
|
27
|
+
else
|
|
28
|
+
config_parts << "max examples: #{conf.max_examples_count}" if conf.max_examples_count
|
|
29
|
+
config_parts << "max group time: #{conf.max_group_time}" if conf.max_group_time
|
|
30
|
+
config_parts << "min tps: #{conf.min_tps}" if conf.min_tps
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
log :info, "TPSProf strict enabled (#{config_parts.join(", ")})"
|
|
34
|
+
else
|
|
35
|
+
log :info, "TPSProf enabled (top-#{TPSProf.config.top_count})"
|
|
36
|
+
end
|
|
22
37
|
end
|
|
23
38
|
|
|
24
39
|
def example_group_started(notification)
|
|
25
40
|
return unless notification.group.top_level?
|
|
41
|
+
return if notification.group.metadata[:tps_prof] == :ignore
|
|
42
|
+
|
|
26
43
|
profiler.group_started notification.group
|
|
27
44
|
end
|
|
28
45
|
|
|
29
46
|
def example_group_finished(notification)
|
|
30
47
|
return unless notification.group.top_level?
|
|
48
|
+
return if notification.group.metadata[:tps_prof] == :ignore
|
|
49
|
+
|
|
31
50
|
profiler.group_finished notification.group
|
|
32
51
|
end
|
|
33
52
|
|
|
34
53
|
def example_started(notification)
|
|
54
|
+
return if notification.example.metadata[:tps_prof] == :ignore
|
|
55
|
+
|
|
35
56
|
profiler.example_started notification.example
|
|
36
57
|
end
|
|
37
58
|
|
|
38
59
|
def example_finished(notification)
|
|
60
|
+
return if notification.example.metadata[:tps_prof] == :ignore
|
|
61
|
+
|
|
39
62
|
profiler.example_finished notification.example
|
|
40
63
|
end
|
|
41
64
|
|
data/lib/test_prof/tps_prof.rb
CHANGED
|
@@ -8,25 +8,81 @@ module TestProf
|
|
|
8
8
|
#
|
|
9
9
|
# Example:
|
|
10
10
|
#
|
|
11
|
+
# # Show top-10 groups with the worst TPS
|
|
11
12
|
# TPS_PROF=10 rspec ...
|
|
12
13
|
#
|
|
14
|
+
# # Report (as errors) groups with lower TPS
|
|
15
|
+
# TPS_PROF_MIN_TPS=10 TPS_PROF=strict rspec ...
|
|
13
16
|
module TPSProf
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
|
|
19
|
+
class GroupInfo < Struct.new(:group, :location, :examples_count, :total_time, :tps, :penalty, keyword_init: true)
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
class Configuration
|
|
15
|
-
attr_accessor :top_count, :
|
|
23
|
+
attr_accessor :top_count, :reporter, :mode
|
|
24
|
+
|
|
25
|
+
# Thresholds
|
|
26
|
+
attr_accessor(
|
|
27
|
+
:min_examples_count, # Ignore groups with fewer examples
|
|
28
|
+
:min_group_time, # Ignore groups with less total time (in seconds)
|
|
29
|
+
:min_target_tps, # Ignore groups with higher TPS
|
|
30
|
+
:max_examples_count, # Report groups with more examples in strict mode
|
|
31
|
+
:max_group_time, # Report groups with more total time in strict mode
|
|
32
|
+
:min_tps # Report groups with lower TPS in strict mode
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
attr_reader :custom_strict_handler
|
|
16
36
|
|
|
17
37
|
def initialize
|
|
18
|
-
@
|
|
19
|
-
@top_count =
|
|
20
|
-
|
|
38
|
+
@mode = (ENV["TPS_PROF_MODE"] || ((ENV["TPS_PROF"] == "strict") ? :strict : :profile)).to_sym
|
|
39
|
+
@top_count = ENV["TPS_PROF_COUNT"]&.to_i || ((ENV["TPS_PROF"].to_i > 1) ? ENV["TPS_PROF"].to_i : nil) || 10
|
|
40
|
+
|
|
41
|
+
@min_examples_count = ENV.fetch("TPS_PROF_MIN_EXAMPLES", 10).to_i
|
|
42
|
+
@min_group_time = ENV.fetch("TPS_PROF_MIN_TIME", 5).to_i
|
|
43
|
+
@min_target_tps = ENV.fetch("TPS_PROF_TARGET_TPS", 30).to_i
|
|
44
|
+
|
|
45
|
+
@max_examples_count = ENV["TPS_PROF_MAX_EXAMPLES"]&.to_i
|
|
46
|
+
@max_group_time = ENV["TPS_PROF_MAX_TIME"]&.to_i
|
|
47
|
+
@min_tps = ENV["TPS_PROF_MIN_TPS"]&.to_i
|
|
48
|
+
|
|
21
49
|
@reporter = resolve_reporter(ENV["TPS_PROF_FORMAT"])
|
|
22
50
|
end
|
|
23
51
|
|
|
52
|
+
def strict_handler
|
|
53
|
+
@strict_handler ||= method(:default_strict_handler)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def strict_handler=(val)
|
|
57
|
+
@strict_handler = val
|
|
58
|
+
@custom_strict_handler = true
|
|
59
|
+
end
|
|
60
|
+
|
|
24
61
|
private
|
|
25
62
|
|
|
26
63
|
def resolve_reporter(format)
|
|
27
64
|
# TODO: support other formats
|
|
28
65
|
TPSProf::Reporter::Text.new
|
|
29
66
|
end
|
|
67
|
+
|
|
68
|
+
def default_strict_handler(group_info)
|
|
69
|
+
error_msg = nil
|
|
70
|
+
location = group_info.location
|
|
71
|
+
|
|
72
|
+
if max_examples_count && group_info.examples_count > max_examples_count
|
|
73
|
+
error_msg ||= "Group #{location} has too many examples: #{group_info.examples_count} > #{max_examples_count}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if max_group_time && group_info.total_time > max_group_time
|
|
77
|
+
error_msg ||= "Group #{location} has too long total time: #{group_info.total_time} > #{max_group_time}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if min_tps && group_info.tps < min_tps
|
|
81
|
+
error_msg ||= "Group #{location} has too low TPS: #{group_info.tps} < #{min_tps}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
raise error_msg if error_msg
|
|
85
|
+
end
|
|
30
86
|
end
|
|
31
87
|
|
|
32
88
|
class << self
|
|
@@ -37,6 +93,15 @@ module TestProf
|
|
|
37
93
|
def configure
|
|
38
94
|
yield config
|
|
39
95
|
end
|
|
96
|
+
|
|
97
|
+
def handle_group_strictly(group_info)
|
|
98
|
+
reporter = ::RSpec.configuration.reporter
|
|
99
|
+
config.strict_handler.call(group_info)
|
|
100
|
+
false
|
|
101
|
+
rescue => err
|
|
102
|
+
reporter.notify_non_example_exception(Error.new(err.message), "")
|
|
103
|
+
true
|
|
104
|
+
end
|
|
40
105
|
end
|
|
41
106
|
end
|
|
42
107
|
end
|
data/lib/test_prof/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: test-prof
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vladimir Dementyev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-03-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -201,12 +201,12 @@ files:
|
|
|
201
201
|
- lib/test_prof/recipes/rspec/let_it_be.rb
|
|
202
202
|
- lib/test_prof/recipes/rspec/sample.rb
|
|
203
203
|
- lib/test_prof/rspec_dissect.rb
|
|
204
|
-
- lib/test_prof/rspec_dissect/
|
|
205
|
-
- lib/test_prof/rspec_dissect/collectors/before.rb
|
|
206
|
-
- lib/test_prof/rspec_dissect/collectors/let.rb
|
|
204
|
+
- lib/test_prof/rspec_dissect/collector.rb
|
|
207
205
|
- lib/test_prof/rspec_dissect/rspec.rb
|
|
208
206
|
- lib/test_prof/rspec_stamp.rb
|
|
209
207
|
- lib/test_prof/rspec_stamp/parser.rb
|
|
208
|
+
- lib/test_prof/rspec_stamp/parser/prism.rb
|
|
209
|
+
- lib/test_prof/rspec_stamp/parser/ripper.rb
|
|
210
210
|
- lib/test_prof/rspec_stamp/rspec.rb
|
|
211
211
|
- lib/test_prof/rubocop.rb
|
|
212
212
|
- lib/test_prof/ruby_prof.rb
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "test_prof/utils/sized_ordered_set"
|
|
4
|
-
require "test_prof/ext/float_duration"
|
|
5
|
-
require "test_prof/ext/string_truncate"
|
|
6
|
-
|
|
7
|
-
module TestProf # :nodoc: all
|
|
8
|
-
using FloatDuration
|
|
9
|
-
using StringTruncate
|
|
10
|
-
|
|
11
|
-
module RSpecDissect
|
|
12
|
-
module Collectors
|
|
13
|
-
class Base
|
|
14
|
-
attr_reader :results, :name, :top_count
|
|
15
|
-
|
|
16
|
-
def initialize(name:, top_count:)
|
|
17
|
-
@name = name
|
|
18
|
-
@top_count = top_count
|
|
19
|
-
@results = Utils::SizedOrderedSet.new(
|
|
20
|
-
top_count, sort_by: name
|
|
21
|
-
)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def populate!(data)
|
|
25
|
-
data[name] = RSpecDissect.time_for(name)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def <<(data)
|
|
29
|
-
results << data
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def total_time
|
|
33
|
-
RSpecDissect.total_time_for(name)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def total_time_message
|
|
37
|
-
"\nTotal `#{print_name}` time: #{total_time.duration}"
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def print_name
|
|
41
|
-
name
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def print_result_header
|
|
45
|
-
<<~MSG
|
|
46
|
-
|
|
47
|
-
Top #{top_count} slowest suites (by `#{print_name}` time):
|
|
48
|
-
|
|
49
|
-
MSG
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def print_group_result(group)
|
|
53
|
-
<<~GROUP
|
|
54
|
-
#{group[:desc].truncate} (#{group[:loc]}) – #{group[name].duration} of #{group[:total].duration} (#{group[:count]})
|
|
55
|
-
GROUP
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def print_results
|
|
59
|
-
msgs = [print_result_header]
|
|
60
|
-
|
|
61
|
-
results.each do |group|
|
|
62
|
-
msgs << print_group_result(group)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
msgs.join
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "test_prof/rspec_dissect/collectors/base"
|
|
4
|
-
|
|
5
|
-
module TestProf
|
|
6
|
-
module RSpecDissect
|
|
7
|
-
module Collectors # :nodoc: all
|
|
8
|
-
class Before < Base
|
|
9
|
-
def initialize(params)
|
|
10
|
-
super(name: :before, **params)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def print_name
|
|
14
|
-
"before(:each)"
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "test_prof/rspec_dissect/collectors/base"
|
|
4
|
-
|
|
5
|
-
module TestProf
|
|
6
|
-
module RSpecDissect
|
|
7
|
-
module Collectors # :nodoc: all
|
|
8
|
-
class Let < Base
|
|
9
|
-
def initialize(params)
|
|
10
|
-
super(name: :let, **params)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def populate!(data)
|
|
14
|
-
super
|
|
15
|
-
data[:let_calls] = RSpecDissect.meta_for(name)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def print_results
|
|
19
|
-
return unless RSpecDissect.memoization_available?
|
|
20
|
-
super
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def print_group_result(group)
|
|
24
|
-
return super unless RSpecDissect.config.let_stats_enabled?
|
|
25
|
-
msgs = [super]
|
|
26
|
-
group[:let_calls]
|
|
27
|
-
.group_by(&:itself)
|
|
28
|
-
.map { |id, calls| [id, -calls.size] }
|
|
29
|
-
.sort_by(&:last)
|
|
30
|
-
.take(RSpecDissect.config.let_top_count)
|
|
31
|
-
.each do |(id, size)|
|
|
32
|
-
msgs << " ↳ #{id} – #{-size}\n"
|
|
33
|
-
end
|
|
34
|
-
msgs.join
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|