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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -20
  3. data/README.md +10 -57
  4. data/assets/tagprof.demo.html +447 -0
  5. data/assets/tagprof.template.html +447 -0
  6. data/lib/minitest/event_prof_formatter.rb +18 -16
  7. data/lib/test_prof.rb +2 -1
  8. data/lib/test_prof/any_fixture.rb +80 -4
  9. data/lib/test_prof/any_fixture/dsl.rb +18 -0
  10. data/lib/test_prof/event_prof.rb +10 -108
  11. data/lib/test_prof/event_prof/custom_events.rb +30 -0
  12. data/lib/test_prof/event_prof/custom_events/factory_create.rb +1 -1
  13. data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +1 -1
  14. data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +1 -1
  15. data/lib/test_prof/event_prof/minitest.rb +6 -0
  16. data/lib/test_prof/event_prof/profiler.rb +129 -0
  17. data/lib/test_prof/event_prof/rspec.rb +20 -11
  18. data/lib/test_prof/factory_all_stub.rb +32 -0
  19. data/lib/test_prof/factory_all_stub/factory_bot_patch.rb +13 -0
  20. data/lib/test_prof/factory_doctor/rspec.rb +3 -2
  21. data/lib/test_prof/factory_prof/printers/flamegraph.rb +9 -13
  22. data/lib/test_prof/recipes/active_record_shared_connection.rb +55 -0
  23. data/lib/test_prof/recipes/logging.rb +37 -0
  24. data/lib/test_prof/recipes/rspec/any_fixture.rb +4 -1
  25. data/lib/test_prof/recipes/rspec/factory_all_stub.rb +10 -0
  26. data/lib/test_prof/rspec_dissect/rspec.rb +4 -2
  27. data/lib/test_prof/rspec_stamp/rspec.rb +3 -2
  28. data/lib/test_prof/tag_prof.rb +4 -0
  29. data/lib/test_prof/tag_prof/printers/html.rb +24 -0
  30. data/lib/test_prof/tag_prof/printers/simple.rb +82 -0
  31. data/lib/test_prof/tag_prof/result.rb +38 -0
  32. data/lib/test_prof/tag_prof/rspec.rb +43 -40
  33. data/lib/test_prof/utils/html_builder.rb +21 -0
  34. data/lib/test_prof/version.rb +1 -1
  35. metadata +20 -24
  36. data/assets/logo.svg +0 -1
  37. data/assets/testprof.png +0 -0
  38. data/guides/.rubocop.yml +0 -1
  39. data/guides/any_fixture.md +0 -114
  40. data/guides/before_all.md +0 -98
  41. data/guides/event_prof.md +0 -177
  42. data/guides/factory_default.md +0 -111
  43. data/guides/factory_doctor.md +0 -119
  44. data/guides/factory_prof.md +0 -86
  45. data/guides/let_it_be.md +0 -97
  46. data/guides/rspec_dissect.md +0 -60
  47. data/guides/rspec_stamp.md +0 -52
  48. data/guides/rubocop.md +0 -48
  49. data/guides/ruby_prof.md +0 -63
  50. data/guides/stack_prof.md +0 -47
  51. data/guides/tag_prof.md +0 -51
  52. data/guides/tests_sampling.md +0 -24
@@ -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] == val)
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
- class Cache # :nodoc:
9
- attr_reader :store
11
+ using FloatDuration
12
+ using StringStripHeredoc
10
13
 
11
- delegate :clear, to: :store
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
- return store[key] if store.key?(key)
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
@@ -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 profiler
76
- def build
77
- Profiler.new(
78
- event: config.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.activate('EVENT_PROF', 'factory.create') do
47
+ TestProf::EventProf::CustomEvents.register("factory.create") do
48
48
  if defined? TestProf::FactoryBot
49
49
  TestProf::EventProf::CustomEvents::FactoryCreate.setup!
50
50
  else
@@ -40,7 +40,7 @@ module TestProf::EventProf::CustomEvents
40
40
  end
41
41
  end
42
42
 
43
- TestProf.activate('EVENT_PROF', 'sidekiq.inline') do
43
+ TestProf::EventProf::CustomEvents.register("sidekiq.inline") do
44
44
  if TestProf.require(
45
45
  'sidekiq/testing',
46
46
  <<-MSG.strip_heredoc
@@ -27,7 +27,7 @@ module TestProf::EventProf::CustomEvents
27
27
  end
28
28
  end
29
29
 
30
- TestProf.activate('EVENT_PROF', 'sidekiq.jobs') do
30
+ TestProf::EventProf::CustomEvents.register("sidekiq.jobs") do
31
31
  if TestProf.require(
32
32
  'sidekiq/testing',
33
33
  <<-MSG.strip_heredoc
@@ -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