test-prof 0.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +103 -0
- data/assets/flamegraph.demo.html +173 -0
- data/assets/flamegraph.template.html +196 -0
- data/assets/src/d3-tip.js +352 -0
- data/assets/src/d3-tip.min.js +1 -0
- data/assets/src/d3.flameGraph.css +92 -0
- data/assets/src/d3.flameGraph.js +459 -0
- data/assets/src/d3.flameGraph.min.css +1 -0
- data/assets/src/d3.flameGraph.min.js +1 -0
- data/assets/src/d3.v4.min.js +8 -0
- data/guides/any_fixture.md +60 -0
- data/guides/before_all.md +98 -0
- data/guides/event_prof.md +97 -0
- data/guides/factory_doctor.md +64 -0
- data/guides/factory_prof.md +85 -0
- data/guides/rspec_stamp.md +53 -0
- data/guides/rubocop.md +48 -0
- data/guides/ruby_prof.md +61 -0
- data/guides/stack_prof.md +43 -0
- data/lib/test-prof.rb +3 -0
- data/lib/test_prof/any_fixture.rb +67 -0
- data/lib/test_prof/cops/rspec/aggregate_failures.rb +140 -0
- data/lib/test_prof/event_prof/custom_events/factory_create.rb +51 -0
- data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +48 -0
- data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +38 -0
- data/lib/test_prof/event_prof/custom_events.rb +5 -0
- data/lib/test_prof/event_prof/instrumentations/active_support.rb +16 -0
- data/lib/test_prof/event_prof/minitest.rb +3 -0
- data/lib/test_prof/event_prof/rspec.rb +94 -0
- data/lib/test_prof/event_prof.rb +177 -0
- data/lib/test_prof/ext/float_duration.rb +14 -0
- data/lib/test_prof/factory_doctor/factory_girl_patch.rb +12 -0
- data/lib/test_prof/factory_doctor/minitest.rb +3 -0
- data/lib/test_prof/factory_doctor/rspec.rb +96 -0
- data/lib/test_prof/factory_doctor.rb +133 -0
- data/lib/test_prof/factory_prof/factory_girl_patch.rb +12 -0
- data/lib/test_prof/factory_prof/printers/flamegraph.rb +71 -0
- data/lib/test_prof/factory_prof/printers/simple.rb +28 -0
- data/lib/test_prof/factory_prof.rb +140 -0
- data/lib/test_prof/logging.rb +25 -0
- data/lib/test_prof/recipes/rspec/any_fixture.rb +21 -0
- data/lib/test_prof/recipes/rspec/before_all.rb +23 -0
- data/lib/test_prof/rspec_stamp/parser.rb +103 -0
- data/lib/test_prof/rspec_stamp/rspec.rb +91 -0
- data/lib/test_prof/rspec_stamp.rb +135 -0
- data/lib/test_prof/rubocop.rb +3 -0
- data/lib/test_prof/ruby_prof/rspec.rb +13 -0
- data/lib/test_prof/ruby_prof.rb +194 -0
- data/lib/test_prof/stack_prof/rspec.rb +13 -0
- data/lib/test_prof/stack_prof.rb +120 -0
- data/lib/test_prof/version.rb +5 -0
- data/lib/test_prof.rb +108 -0
- metadata +227 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf::EventProf::CustomEvents
|
4
|
+
module SidekiqJobs # :nodoc: all
|
5
|
+
module ClientPatch
|
6
|
+
def raw_push(*)
|
7
|
+
return super unless Sidekiq::Testing.inline?
|
8
|
+
SidekiqJobs.track { super }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def setup!
|
14
|
+
Sidekiq::Client.prepend ClientPatch
|
15
|
+
end
|
16
|
+
|
17
|
+
def track
|
18
|
+
ActiveSupport::Notifications.instrument(
|
19
|
+
'sidekiq.jobs'
|
20
|
+
) { yield }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if TestProf.require(
|
27
|
+
'sidekiq/testing',
|
28
|
+
<<~MSG
|
29
|
+
Failed to load Sidekiq.
|
30
|
+
|
31
|
+
Make sure that "sidekiq" gem is in your Gemfile.
|
32
|
+
MSG
|
33
|
+
)
|
34
|
+
TestProf::EventProf::CustomEvents::SidekiqJobs.setup!
|
35
|
+
TestProf::EventProf.configure do |config|
|
36
|
+
config.rank_by = :count
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/event_prof/custom_events/factory_create" if ENV['EVENT_PROF'] == "factory.create"
|
4
|
+
require "test_prof/event_prof/custom_events/sidekiq_inline" if ENV['EVENT_PROF'] == "sidekiq.inline"
|
5
|
+
require "test_prof/event_prof/custom_events/sidekiq_jobs" if ENV['EVENT_PROF'] == "sidekiq.jobs"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf::EventProf
|
4
|
+
module Instrumentations
|
5
|
+
# Wrapper over ActiveSupport::Notifications
|
6
|
+
module ActiveSupport
|
7
|
+
def self.subscribe(event)
|
8
|
+
raise ArgumentError, 'Block is required!' unless block_given?
|
9
|
+
|
10
|
+
::ActiveSupport::Notifications.subscribe(event) do |_event, start, finish, *_args|
|
11
|
+
yield (finish - start)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/ext/float_duration"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module EventProf
|
7
|
+
class RSpecListener # :nodoc:
|
8
|
+
include Logging
|
9
|
+
using FloatDuration
|
10
|
+
|
11
|
+
NOTIFICATIONS = %i[
|
12
|
+
example_group_started
|
13
|
+
example_group_finished
|
14
|
+
example_started
|
15
|
+
example_finished
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@profiler = EventProf.build
|
20
|
+
end
|
21
|
+
|
22
|
+
def example_group_started(notification)
|
23
|
+
return unless notification.group.top_level?
|
24
|
+
@profiler.group_started notification.group
|
25
|
+
end
|
26
|
+
|
27
|
+
def example_group_finished(notification)
|
28
|
+
return unless notification.group.top_level?
|
29
|
+
@profiler.group_finished notification.group
|
30
|
+
end
|
31
|
+
|
32
|
+
def example_started(notification)
|
33
|
+
@profiler.example_started notification.example
|
34
|
+
end
|
35
|
+
|
36
|
+
def example_finished(notification)
|
37
|
+
@profiler.example_finished notification.example
|
38
|
+
end
|
39
|
+
|
40
|
+
def print
|
41
|
+
result = @profiler.results
|
42
|
+
|
43
|
+
msgs = []
|
44
|
+
|
45
|
+
msgs <<
|
46
|
+
<<~MSG
|
47
|
+
EventProf results for #{@profiler.event}
|
48
|
+
|
49
|
+
Total time: #{@profiler.total_time.duration}
|
50
|
+
Total events: #{@profiler.total_count}
|
51
|
+
|
52
|
+
Top #{@profiler.top_count} slowest suites (by #{@profiler.rank_by}):
|
53
|
+
|
54
|
+
MSG
|
55
|
+
|
56
|
+
result[:groups].each do |group|
|
57
|
+
description = group[:id].top_level_description
|
58
|
+
location = group[:id].metadata[:location]
|
59
|
+
|
60
|
+
msgs <<
|
61
|
+
<<~GROUP
|
62
|
+
#{description} (#{location}) – #{group[:time].duration} (#{group[:count]} / #{group[:examples]})
|
63
|
+
GROUP
|
64
|
+
end
|
65
|
+
|
66
|
+
if result[:examples]
|
67
|
+
msgs << "\nTop #{@profiler.top_count} slowest tests (by #{@profiler.rank_by}):\n\n"
|
68
|
+
|
69
|
+
result[:examples].each do |example|
|
70
|
+
description = example[:id].description
|
71
|
+
location = example[:id].metadata[:location]
|
72
|
+
msgs <<
|
73
|
+
<<~GROUP
|
74
|
+
#{description} (#{location}) – #{example[:time].duration} (#{example[:count]})
|
75
|
+
GROUP
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
log :info, msgs.join
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Register EventProf listener
|
86
|
+
TestProf.activate('EVENT_PROF') do
|
87
|
+
RSpec.configure do |config|
|
88
|
+
listener = TestProf::EventProf::RSpecListener.new
|
89
|
+
|
90
|
+
config.reporter.register_listener(listener, *TestProf::EventProf::RSpecListener::NOTIFICATIONS)
|
91
|
+
|
92
|
+
config.after(:suite) { listener.print }
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/event_prof/instrumentations/active_support"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
# EventProf profiles your tests and suites against custom events,
|
7
|
+
# such as ActiveSupport::Notifacations.
|
8
|
+
#
|
9
|
+
# It works very similar to `rspec --profile` but can track arbitrary events.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# # Collect SQL queries stats for every suite and example
|
14
|
+
# EVENT_PROF='sql.active_record' rspec ...
|
15
|
+
#
|
16
|
+
# By default it collects information only about top-level groups (aka suites),
|
17
|
+
# but you can also profile individual examples. Just set the configuration option:
|
18
|
+
#
|
19
|
+
# TestProf::EventProf.configure do |config|
|
20
|
+
# config.per_example = true
|
21
|
+
# end
|
22
|
+
module EventProf
|
23
|
+
# EventProf configuration
|
24
|
+
class Configuration
|
25
|
+
# Map of supported instrumenters
|
26
|
+
INSTRUMENTERS = {
|
27
|
+
active_support: 'ActiveSupport'
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
attr_accessor :instrumenter, :top_count, :per_example,
|
31
|
+
:rank_by, :event
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@event = ENV['EVENT_PROF']
|
35
|
+
@instrumenter = :active_support
|
36
|
+
@top_count = (ENV['EVENT_PROF_TOP'] || 5).to_i
|
37
|
+
@per_example = false
|
38
|
+
@rank_by = (ENV['EVENT_PROF_RANK'] || :time).to_sym
|
39
|
+
end
|
40
|
+
|
41
|
+
def per_example?
|
42
|
+
per_example == true
|
43
|
+
end
|
44
|
+
|
45
|
+
def resolve_instrumenter
|
46
|
+
return instrumenter if instrumenter.is_a?(Module)
|
47
|
+
|
48
|
+
raise ArgumentError, "Unknown instrumenter: #{instrumenter}" unless
|
49
|
+
INSTRUMENTERS.key?(instrumenter)
|
50
|
+
|
51
|
+
Instrumentations.const_get(INSTRUMENTERS[instrumenter])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
def config
|
57
|
+
@config ||= Configuration.new
|
58
|
+
end
|
59
|
+
|
60
|
+
def configure
|
61
|
+
yield config
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns new configured instance of profiler
|
65
|
+
def build
|
66
|
+
Profiler.new(
|
67
|
+
event: config.event,
|
68
|
+
instrumenter: config.resolve_instrumenter
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Profiler # :nodoc:
|
74
|
+
include TestProf::Logging
|
75
|
+
|
76
|
+
attr_reader :event, :top_count, :rank_by, :total_count, :total_time
|
77
|
+
|
78
|
+
def initialize(event:, instrumenter:)
|
79
|
+
@event = event
|
80
|
+
|
81
|
+
log :info, "EventProf enabled (#{@event})"
|
82
|
+
|
83
|
+
instrumenter.subscribe(event) { |time| track(time) }
|
84
|
+
|
85
|
+
@groups = Hash.new { |h, k| h[k] = { id: k } }
|
86
|
+
@examples = Hash.new { |h, k| h[k] = { id: k } }
|
87
|
+
|
88
|
+
@total_count = 0
|
89
|
+
@total_time = 0.0
|
90
|
+
|
91
|
+
reset!
|
92
|
+
end
|
93
|
+
|
94
|
+
def track(time)
|
95
|
+
return if @current_group.nil?
|
96
|
+
@total_time += time
|
97
|
+
@total_count += 1
|
98
|
+
|
99
|
+
@time += time
|
100
|
+
@count += 1
|
101
|
+
|
102
|
+
@example_time += time if config.per_example?
|
103
|
+
@example_count += 1 if config.per_example?
|
104
|
+
end
|
105
|
+
|
106
|
+
def group_started(id)
|
107
|
+
reset!
|
108
|
+
@current_group = id
|
109
|
+
end
|
110
|
+
|
111
|
+
def group_finished(id)
|
112
|
+
@groups[id][:time] = @time
|
113
|
+
@groups[id][:count] = @count
|
114
|
+
@groups[id][:examples] = @total_examples
|
115
|
+
@current_group = nil
|
116
|
+
end
|
117
|
+
|
118
|
+
def example_started(_id)
|
119
|
+
reset_example!
|
120
|
+
end
|
121
|
+
|
122
|
+
def example_finished(id)
|
123
|
+
@total_examples += 1
|
124
|
+
return unless config.per_example?
|
125
|
+
|
126
|
+
@examples[id][:time] = @example_time
|
127
|
+
@examples[id][:count] = @example_count
|
128
|
+
end
|
129
|
+
|
130
|
+
def results
|
131
|
+
{
|
132
|
+
groups: fetch_top(@groups.values)
|
133
|
+
}.tap do |data|
|
134
|
+
next unless config.per_example?
|
135
|
+
|
136
|
+
data[:examples] = fetch_top(@examples.values)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def rank_by
|
141
|
+
EventProf.config.rank_by
|
142
|
+
end
|
143
|
+
|
144
|
+
def top_count
|
145
|
+
EventProf.config.top_count
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def fetch_top(arr)
|
151
|
+
arr.reject { |el| el[rank_by].zero? }
|
152
|
+
.sort_by { |el| -el[rank_by] }
|
153
|
+
.take(top_count)
|
154
|
+
end
|
155
|
+
|
156
|
+
def config
|
157
|
+
EventProf.config
|
158
|
+
end
|
159
|
+
|
160
|
+
def reset!
|
161
|
+
@time = 0.0
|
162
|
+
@count = 0
|
163
|
+
@total_examples = 0
|
164
|
+
reset_example!
|
165
|
+
end
|
166
|
+
|
167
|
+
def reset_example!
|
168
|
+
@example_count = 0
|
169
|
+
@example_time = 0.0
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
require "test_prof/event_prof/rspec" if defined?(RSpec)
|
176
|
+
require "test_prof/event_prof/minitest" if defined?(Minitest::Reporters)
|
177
|
+
require "test_prof/event_prof/custom_events"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add #duration method to floats
|
4
|
+
module TestProf
|
5
|
+
# Extend Float with #duration method
|
6
|
+
module FloatDuration
|
7
|
+
refine Float do
|
8
|
+
def duration
|
9
|
+
t = self
|
10
|
+
format("%02d:%02d.%03d", t / 60, t % 60, t.modulo(1) * 1000)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module FactoryDoctor
|
5
|
+
# Wrap #run method with FactoryDoctor tracking
|
6
|
+
module FactoryGirlPatch
|
7
|
+
def run(strategy = @strategy)
|
8
|
+
FactoryDoctor.within_factory(strategy) { super }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/ext/float_duration"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module FactoryDoctor
|
7
|
+
class RSpecListener # :nodoc:
|
8
|
+
include Logging
|
9
|
+
using FloatDuration
|
10
|
+
|
11
|
+
NOTIFICATIONS = %i[
|
12
|
+
example_started
|
13
|
+
example_finished
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@count = 0
|
18
|
+
@time = 0.0
|
19
|
+
@example_groups = Hash.new { |h, k| h[k] = [] }
|
20
|
+
end
|
21
|
+
|
22
|
+
def example_started(_notification)
|
23
|
+
FactoryDoctor.start
|
24
|
+
end
|
25
|
+
|
26
|
+
def example_finished(notification)
|
27
|
+
FactoryDoctor.stop
|
28
|
+
return if notification.example.pending?
|
29
|
+
|
30
|
+
result = FactoryDoctor.result
|
31
|
+
|
32
|
+
return unless result.bad?
|
33
|
+
|
34
|
+
group = notification.example.example_group.parent_groups.last
|
35
|
+
notification.example.metadata.merge!(
|
36
|
+
factories: result.count,
|
37
|
+
time: result.time
|
38
|
+
)
|
39
|
+
@example_groups[group] << notification.example
|
40
|
+
@count += 1
|
41
|
+
@time += result.time
|
42
|
+
end
|
43
|
+
|
44
|
+
def print
|
45
|
+
return if @example_groups.empty?
|
46
|
+
|
47
|
+
msgs = []
|
48
|
+
|
49
|
+
msgs <<
|
50
|
+
<<~MSG
|
51
|
+
FactoryDoctor report
|
52
|
+
|
53
|
+
Total (potentially) bad examples: #{@count}
|
54
|
+
Total wasted time: #{@time.duration}
|
55
|
+
|
56
|
+
MSG
|
57
|
+
|
58
|
+
@example_groups.each do |group, examples|
|
59
|
+
msgs << "#{group.description} (#{group.metadata[:location]})\n"
|
60
|
+
examples.each do |ex|
|
61
|
+
msgs << " #{ex.description} (#{ex.metadata[:location]}) "\
|
62
|
+
"– #{pluralize_records(ex.metadata[:factories])} created, "\
|
63
|
+
"#{ex.metadata[:time].duration}\n"
|
64
|
+
end
|
65
|
+
msgs << "\n"
|
66
|
+
end
|
67
|
+
|
68
|
+
log :info, msgs.join
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def pluralize_records(count)
|
74
|
+
return "1 record" if count == 1
|
75
|
+
"#{count} records"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Register FactoryDoctor listener
|
82
|
+
TestProf.activate('FDOC') do
|
83
|
+
RSpec.configure do |config|
|
84
|
+
listener = TestProf::FactoryDoctor::RSpecListener.new
|
85
|
+
|
86
|
+
config.reporter.register_listener(
|
87
|
+
listener, *TestProf::FactoryDoctor::RSpecListener::NOTIFICATIONS
|
88
|
+
)
|
89
|
+
|
90
|
+
config.after(:suite) { listener.print }
|
91
|
+
end
|
92
|
+
|
93
|
+
RSpec.shared_context "factory_doctor:ignore", fd_ignore: true do
|
94
|
+
around(:each) { |ex| TestProf::FactoryDoctor.ignore(&ex) }
|
95
|
+
end
|
96
|
+
end
|