test-prof 0.4.9 → 0.5.0.pre1
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 +63 -20
- data/README.md +10 -57
- data/assets/tagprof.demo.html +447 -0
- data/assets/tagprof.template.html +447 -0
- data/lib/minitest/event_prof_formatter.rb +18 -16
- data/lib/test_prof.rb +2 -1
- data/lib/test_prof/any_fixture.rb +80 -4
- data/lib/test_prof/any_fixture/dsl.rb +18 -0
- data/lib/test_prof/event_prof.rb +10 -108
- data/lib/test_prof/event_prof/custom_events.rb +30 -0
- data/lib/test_prof/event_prof/custom_events/factory_create.rb +1 -1
- 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/event_prof/minitest.rb +6 -0
- data/lib/test_prof/event_prof/profiler.rb +129 -0
- data/lib/test_prof/event_prof/rspec.rb +20 -11
- data/lib/test_prof/factory_all_stub.rb +32 -0
- data/lib/test_prof/factory_all_stub/factory_bot_patch.rb +13 -0
- data/lib/test_prof/factory_doctor/rspec.rb +3 -2
- data/lib/test_prof/factory_prof/printers/flamegraph.rb +9 -13
- data/lib/test_prof/recipes/active_record_shared_connection.rb +55 -0
- data/lib/test_prof/recipes/logging.rb +37 -0
- data/lib/test_prof/recipes/rspec/any_fixture.rb +4 -1
- data/lib/test_prof/recipes/rspec/factory_all_stub.rb +10 -0
- data/lib/test_prof/rspec_dissect/rspec.rb +4 -2
- data/lib/test_prof/rspec_stamp/rspec.rb +3 -2
- data/lib/test_prof/tag_prof.rb +4 -0
- data/lib/test_prof/tag_prof/printers/html.rb +24 -0
- data/lib/test_prof/tag_prof/printers/simple.rb +82 -0
- data/lib/test_prof/tag_prof/result.rb +38 -0
- data/lib/test_prof/tag_prof/rspec.rb +43 -40
- data/lib/test_prof/utils/html_builder.rb +21 -0
- data/lib/test_prof/version.rb +1 -1
- metadata +20 -24
- data/assets/logo.svg +0 -1
- data/assets/testprof.png +0 -0
- data/guides/.rubocop.yml +0 -1
- data/guides/any_fixture.md +0 -114
- data/guides/before_all.md +0 -98
- data/guides/event_prof.md +0 -177
- data/guides/factory_default.md +0 -111
- data/guides/factory_doctor.md +0 -119
- data/guides/factory_prof.md +0 -86
- data/guides/let_it_be.md +0 -97
- data/guides/rspec_dissect.md +0 -60
- data/guides/rspec_stamp.md +0 -52
- data/guides/rubocop.md +0 -48
- data/guides/ruby_prof.md +0 -63
- data/guides/stack_prof.md +0 -47
- data/guides/tag_prof.md +0 -51
- data/guides/tests_sampling.md +0 -24
data/lib/test_prof.rb
CHANGED
@@ -90,7 +90,7 @@ module TestProf
|
|
90
90
|
private
|
91
91
|
|
92
92
|
def activate!(env_var, val)
|
93
|
-
yield if ENV[env_var] && (val.nil? || ENV[env_var]
|
93
|
+
yield if ENV[env_var] && (val.nil? || val === ENV[env_var])
|
94
94
|
end
|
95
95
|
|
96
96
|
def with_timestamps(path)
|
@@ -132,3 +132,4 @@ require "test_prof/factory_prof"
|
|
132
132
|
require "test_prof/rspec_stamp"
|
133
133
|
require "test_prof/tag_prof"
|
134
134
|
require "test_prof/rspec_dissect"
|
135
|
+
require "test_prof/factory_all_stub"
|
@@ -1,27 +1,53 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "test_prof/ext/float_duration"
|
4
|
+
require "test_prof/ext/string_strip_heredoc"
|
5
|
+
|
3
6
|
module TestProf
|
4
7
|
# Make DB fixtures from blocks.
|
5
8
|
module AnyFixture
|
6
9
|
INSERT_RXP = /^INSERT INTO ([\S]+)/
|
7
10
|
|
8
|
-
|
9
|
-
|
11
|
+
using FloatDuration
|
12
|
+
using StringStripHeredoc
|
10
13
|
|
11
|
-
|
14
|
+
class Cache # :nodoc:
|
15
|
+
attr_reader :store, :stats
|
12
16
|
|
13
17
|
def initialize
|
14
18
|
@store = {}
|
19
|
+
@stats = {}
|
15
20
|
end
|
16
21
|
|
17
22
|
def fetch(key)
|
18
|
-
|
23
|
+
if store.key?(key)
|
24
|
+
stats[key][:hit] += 1
|
25
|
+
return store[key]
|
26
|
+
end
|
27
|
+
|
19
28
|
return unless block_given?
|
29
|
+
|
30
|
+
ts = TestProf.now
|
20
31
|
store[key] = yield
|
32
|
+
stats[key] = { time: TestProf.now - ts, hit: 0 }
|
33
|
+
store[key]
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear
|
37
|
+
store.clear
|
38
|
+
stats.clear
|
21
39
|
end
|
22
40
|
end
|
23
41
|
|
24
42
|
class << self
|
43
|
+
include Logging
|
44
|
+
|
45
|
+
attr_accessor :reporting_enabled
|
46
|
+
|
47
|
+
def reporting_enabled?
|
48
|
+
reporting_enabled == true
|
49
|
+
end
|
50
|
+
|
25
51
|
# Register a block of code as a fixture,
|
26
52
|
# returns the result of the block execution
|
27
53
|
def register(id)
|
@@ -53,6 +79,54 @@ module TestProf
|
|
53
79
|
tables_cache[matches[1]] = true if matches
|
54
80
|
end
|
55
81
|
|
82
|
+
def report_stats
|
83
|
+
msgs = []
|
84
|
+
|
85
|
+
msgs <<
|
86
|
+
<<-MSG.strip_heredoc
|
87
|
+
AnyFixture usage stats:
|
88
|
+
MSG
|
89
|
+
|
90
|
+
first_column = cache.stats.keys.map(&:size).max + 2
|
91
|
+
|
92
|
+
msgs << format(
|
93
|
+
"%#{first_column}s %12s %9s %12s",
|
94
|
+
'key', 'build time', 'hit count', 'saved time'
|
95
|
+
)
|
96
|
+
|
97
|
+
msgs << ""
|
98
|
+
|
99
|
+
total_spent = 0.0
|
100
|
+
total_saved = 0.0
|
101
|
+
total_miss = 0.0
|
102
|
+
|
103
|
+
cache.stats.to_a.sort_by { |(_, v)| -v[:hit] }.each do |(key, stats)|
|
104
|
+
total_spent += stats[:time]
|
105
|
+
|
106
|
+
saved = stats[:time] * stats[:hit]
|
107
|
+
|
108
|
+
total_saved += saved
|
109
|
+
|
110
|
+
total_miss += stats[:time] if stats[:hit].zero?
|
111
|
+
|
112
|
+
msgs << format(
|
113
|
+
"%#{first_column}s %12s %9d %12s",
|
114
|
+
key, stats[:time].duration, stats[:hit],
|
115
|
+
saved.duration
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
msgs <<
|
120
|
+
<<-MSG.strip_heredoc
|
121
|
+
|
122
|
+
Total time spent: #{total_spent.duration}
|
123
|
+
Total time saved: #{total_saved.duration}
|
124
|
+
Total time wasted: #{total_miss.duration}
|
125
|
+
MSG
|
126
|
+
|
127
|
+
log :info, msgs.join("\n")
|
128
|
+
end
|
129
|
+
|
56
130
|
private
|
57
131
|
|
58
132
|
def cache
|
@@ -63,5 +137,7 @@ module TestProf
|
|
63
137
|
@tables_cache ||= {}
|
64
138
|
end
|
65
139
|
end
|
140
|
+
|
141
|
+
self.reporting_enabled = ENV['ANYFIXTURE_REPORT'] == '1'
|
66
142
|
end
|
67
143
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module AnyFixture
|
5
|
+
# Adds "global" `fixture` method (through refinement)
|
6
|
+
module DSL
|
7
|
+
module Ext # :nodoc:
|
8
|
+
def fixture(id, &block)
|
9
|
+
::TestProf::AnyFixture.register(:"#{id}", &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
refine Kernel do
|
14
|
+
include Ext
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/test_prof/event_prof.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "test_prof/rspec_stamp"
|
4
|
+
require "test_prof/event_prof/profiler"
|
4
5
|
require "test_prof/event_prof/instrumentations/active_support"
|
5
6
|
require "test_prof/utils/sized_ordered_set"
|
6
7
|
|
@@ -72,119 +73,20 @@ module TestProf
|
|
72
73
|
yield config
|
73
74
|
end
|
74
75
|
|
75
|
-
# Returns new configured instance of
|
76
|
-
def build
|
77
|
-
|
78
|
-
event:
|
79
|
-
instrumenter: config.resolve_instrumenter
|
76
|
+
# Returns new configured instance of profilers group
|
77
|
+
def build(event = config.event)
|
78
|
+
ProfilersGroup.new(
|
79
|
+
event: event,
|
80
|
+
instrumenter: config.resolve_instrumenter,
|
81
|
+
rank_by: config.rank_by,
|
82
|
+
top_count: config.top_count,
|
83
|
+
per_example: config.per_example?
|
80
84
|
)
|
81
85
|
end
|
82
86
|
end
|
83
|
-
|
84
|
-
class Profiler # :nodoc:
|
85
|
-
include TestProf::Logging
|
86
|
-
|
87
|
-
attr_reader :event, :total_count, :total_time
|
88
|
-
|
89
|
-
def initialize(event:, instrumenter:)
|
90
|
-
@event = event
|
91
|
-
|
92
|
-
log :info, "EventProf enabled (#{@event})"
|
93
|
-
|
94
|
-
instrumenter.subscribe(event) { |time| track(time) }
|
95
|
-
|
96
|
-
@groups = Utils::SizedOrderedSet.new(
|
97
|
-
top_count, sort_by: rank_by
|
98
|
-
)
|
99
|
-
|
100
|
-
@examples = Utils::SizedOrderedSet.new(
|
101
|
-
top_count, sort_by: rank_by
|
102
|
-
)
|
103
|
-
|
104
|
-
@total_count = 0
|
105
|
-
@total_time = 0.0
|
106
|
-
end
|
107
|
-
|
108
|
-
def track(time)
|
109
|
-
return if @current_group.nil?
|
110
|
-
@total_time += time
|
111
|
-
@total_count += 1
|
112
|
-
|
113
|
-
@time += time
|
114
|
-
@count += 1
|
115
|
-
|
116
|
-
return if @current_example.nil?
|
117
|
-
|
118
|
-
@example_time += time
|
119
|
-
@example_count += 1
|
120
|
-
end
|
121
|
-
|
122
|
-
def group_started(id)
|
123
|
-
reset_group!
|
124
|
-
@current_group = id
|
125
|
-
end
|
126
|
-
|
127
|
-
def group_finished(id)
|
128
|
-
data = { id: id, time: @time, count: @count, examples: @total_examples }
|
129
|
-
|
130
|
-
@groups << data unless data[rank_by].zero?
|
131
|
-
|
132
|
-
@current_group = nil
|
133
|
-
end
|
134
|
-
|
135
|
-
def example_started(id)
|
136
|
-
return unless config.per_example?
|
137
|
-
reset_example!
|
138
|
-
@current_example = id
|
139
|
-
end
|
140
|
-
|
141
|
-
def example_finished(id)
|
142
|
-
@total_examples += 1
|
143
|
-
return unless config.per_example?
|
144
|
-
|
145
|
-
data = { id: id, time: @example_time, count: @example_count }
|
146
|
-
@examples << data unless data[rank_by].zero?
|
147
|
-
@current_example = nil
|
148
|
-
end
|
149
|
-
|
150
|
-
def results
|
151
|
-
{
|
152
|
-
groups: @groups.to_a
|
153
|
-
}.tap do |data|
|
154
|
-
next unless config.per_example?
|
155
|
-
|
156
|
-
data[:examples] = @examples.to_a
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
def rank_by
|
161
|
-
EventProf.config.rank_by
|
162
|
-
end
|
163
|
-
|
164
|
-
def top_count
|
165
|
-
EventProf.config.top_count
|
166
|
-
end
|
167
|
-
|
168
|
-
private
|
169
|
-
|
170
|
-
def config
|
171
|
-
EventProf.config
|
172
|
-
end
|
173
|
-
|
174
|
-
def reset_group!
|
175
|
-
@time = 0.0
|
176
|
-
@count = 0
|
177
|
-
@total_examples = 0
|
178
|
-
end
|
179
|
-
|
180
|
-
def reset_example!
|
181
|
-
@example_count = 0
|
182
|
-
@example_time = 0.0
|
183
|
-
end
|
184
|
-
end
|
185
87
|
end
|
186
88
|
end
|
187
89
|
|
90
|
+
require "test_prof/event_prof/custom_events"
|
188
91
|
require "test_prof/event_prof/rspec" if defined?(RSpec::Core)
|
189
92
|
require "test_prof/event_prof/minitest" if defined?(Minitest)
|
190
|
-
require "test_prof/event_prof/custom_events"
|
@@ -1,5 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
module TestProf
|
4
|
+
module EventProf
|
5
|
+
# Registers and activates custom events (which require patches).
|
6
|
+
module CustomEvents
|
7
|
+
class << self
|
8
|
+
def register(event)
|
9
|
+
raise ArgumentError, "Block is required!" unless block_given?
|
10
|
+
registrations[event] = Proc.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def activate_all(events)
|
14
|
+
events = events.split(",")
|
15
|
+
events.each { |event| try_activate(event) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def try_activate(event)
|
19
|
+
return unless registrations.key?(event)
|
20
|
+
registrations.delete(event).call
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def registrations
|
26
|
+
@registrations ||= {}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
3
33
|
require "test_prof/event_prof/custom_events/factory_create"
|
4
34
|
require "test_prof/event_prof/custom_events/sidekiq_inline"
|
5
35
|
require "test_prof/event_prof/custom_events/sidekiq_jobs"
|
@@ -44,7 +44,7 @@ module TestProf::EventProf::CustomEvents
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
TestProf.
|
47
|
+
TestProf::EventProf::CustomEvents.register("factory.create") do
|
48
48
|
if defined? TestProf::FactoryBot
|
49
49
|
TestProf::EventProf::CustomEvents::FactoryCreate.setup!
|
50
50
|
else
|
@@ -9,6 +9,9 @@ module Minitest
|
|
9
9
|
def initialize(io = $stdout, options = {})
|
10
10
|
super
|
11
11
|
@profiler = configure_profiler(options)
|
12
|
+
|
13
|
+
log :info, "EventProf enabled (#{@profiler.events.join(', ')})"
|
14
|
+
|
12
15
|
@formatter = EventProfFormatter.new(@profiler)
|
13
16
|
@current_group = nil
|
14
17
|
@current_example = nil
|
@@ -64,7 +67,10 @@ module Minitest
|
|
64
67
|
config.rank_by = options[:rank_by] if options[:rank_by]
|
65
68
|
config.top_count = options[:top_count] if options[:top_count]
|
66
69
|
config.per_example = options[:per_example] if options[:per_example]
|
70
|
+
|
71
|
+
::TestProf::EventProf::CustomEvents.activate_all config.event
|
67
72
|
end
|
73
|
+
|
68
74
|
::TestProf::EventProf.build
|
69
75
|
end
|
70
76
|
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module EventProf
|
5
|
+
class Profiler # :nodoc:
|
6
|
+
attr_reader :event, :total_count, :total_time, :rank_by, :top_count, :per_example,
|
7
|
+
:time, :count, :example_time, :example_count
|
8
|
+
|
9
|
+
alias per_example? per_example
|
10
|
+
|
11
|
+
def initialize(event:, instrumenter:, rank_by: :time, top_count: 5, per_example: false)
|
12
|
+
@event = event
|
13
|
+
@rank_by = rank_by
|
14
|
+
@top_count = top_count
|
15
|
+
@per_example = per_example
|
16
|
+
|
17
|
+
instrumenter.subscribe(event) { |time| track(time) }
|
18
|
+
|
19
|
+
@groups = Utils::SizedOrderedSet.new(
|
20
|
+
top_count, sort_by: rank_by
|
21
|
+
)
|
22
|
+
|
23
|
+
@examples = Utils::SizedOrderedSet.new(
|
24
|
+
top_count, sort_by: rank_by
|
25
|
+
)
|
26
|
+
|
27
|
+
@total_count = 0
|
28
|
+
@total_time = 0.0
|
29
|
+
end
|
30
|
+
|
31
|
+
def track(time)
|
32
|
+
return if @current_group.nil?
|
33
|
+
@total_time += time
|
34
|
+
@total_count += 1
|
35
|
+
|
36
|
+
@time += time
|
37
|
+
@count += 1
|
38
|
+
|
39
|
+
return if @current_example.nil?
|
40
|
+
|
41
|
+
@example_time += time
|
42
|
+
@example_count += 1
|
43
|
+
end
|
44
|
+
|
45
|
+
def group_started(id)
|
46
|
+
reset_group!
|
47
|
+
@current_group = id
|
48
|
+
end
|
49
|
+
|
50
|
+
def group_finished(id)
|
51
|
+
data = { id: id, time: @time, count: @count, examples: @total_examples }
|
52
|
+
|
53
|
+
@groups << data unless data[rank_by].zero?
|
54
|
+
|
55
|
+
@current_group = nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def example_started(id)
|
59
|
+
return unless per_example?
|
60
|
+
reset_example!
|
61
|
+
@current_example = id
|
62
|
+
end
|
63
|
+
|
64
|
+
def example_finished(id)
|
65
|
+
@total_examples += 1
|
66
|
+
return unless per_example?
|
67
|
+
|
68
|
+
data = { id: id, time: @example_time, count: @example_count }
|
69
|
+
@examples << data unless data[rank_by].zero?
|
70
|
+
@current_example = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def results
|
74
|
+
{
|
75
|
+
groups: @groups.to_a
|
76
|
+
}.tap do |data|
|
77
|
+
next unless per_example?
|
78
|
+
|
79
|
+
data[:examples] = @examples.to_a
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def reset_group!
|
86
|
+
@time = 0.0
|
87
|
+
@count = 0
|
88
|
+
@total_examples = 0
|
89
|
+
end
|
90
|
+
|
91
|
+
def reset_example!
|
92
|
+
@example_count = 0
|
93
|
+
@example_time = 0.0
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Multiple profilers wrapper
|
98
|
+
class ProfilersGroup
|
99
|
+
attr_reader :profilers
|
100
|
+
|
101
|
+
def initialize(event:, **options)
|
102
|
+
events = event.split(",")
|
103
|
+
@profilers = events.map do |ev|
|
104
|
+
Profiler.new(event: ev, **options)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def each
|
109
|
+
if block_given?
|
110
|
+
@profilers.each(&Proc.new)
|
111
|
+
else
|
112
|
+
@profilers.each
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def events
|
117
|
+
@profilers.map(&:event)
|
118
|
+
end
|
119
|
+
|
120
|
+
%i[group_started group_finished example_started example_finished].each do |name|
|
121
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
122
|
+
def #{name}(id)
|
123
|
+
@profilers.each { |profiler| profiler.#{name}(id) }
|
124
|
+
end
|
125
|
+
CODE
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|