test-prof 1.2.2 → 1.3.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.
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module MemoryProf
5
+ class Tracker
6
+ # LinkedList is a linked list that track memory usage for individual examples/groups.
7
+ # A list node (`LinkedListNode`) represents an example/group and its memory usage info:
8
+ #
9
+ # * memory_at_start - the amount of memory at the start of an example/group
10
+ # * memory_at_finish - the amount of memory at the end of an example/group
11
+ # * nested_memory - the amount of memory allocated by examples/groups defined inside a group
12
+ # * previous - a link to the previous node
13
+ #
14
+ # Each node has a link to its previous node, and the head node points to the current example/group.
15
+ # If we picture a linked list as a tree with root being the top level group and leaves being
16
+ # current examples/groups, then the head node will always point to a leaf in that tree.
17
+ #
18
+ # For example, if we have the following spec:
19
+ #
20
+ # describe Question do
21
+ # decribe "#publish" do
22
+ # context "when not published" do
23
+ # it "makes the question visible" do
24
+ # ...
25
+ # end
26
+ # end
27
+ # end
28
+ # end
29
+ #
30
+ # At the moment when rspec is executing the example, the list has the following structure
31
+ # (^ denotes the head node):
32
+ #
33
+ # ^"makes the question visible" -> "when not published" -> "#publish" -> Question
34
+ #
35
+ # LinkedList supports two method for working with it:
36
+ #
37
+ # * add_node – adds a node to the beginig of the list. At this point an example or group
38
+ # has started and we track how much memory has already been used.
39
+ # * remove_node – removes and returns the head node from the list. It means that the node
40
+ # example/group has finished and it is time to calculate its memory usage.
41
+ #
42
+ # When we remove a node we add its total_memory to the previous node.nested_memory, thus
43
+ # gradually tracking the amount of memory used by nested examples inside a group.
44
+ #
45
+ # In the example above, after we remove the node "makes the question visible", we add its total
46
+ # memory usage to nested_memory of the "when not published" node. If the "when not published"
47
+ # group contains other examples or sub-groups, their total_memory will also be added to
48
+ # "when not published" nested_memory. So when the group finishes we will have the total amount
49
+ # of memory used by its nested examples/groups, and thus we will be able to calculate the memory
50
+ # used by hooks and other code inside a group by subtracting nested_memory from total_memory.
51
+ class LinkedList
52
+ attr_reader :head
53
+
54
+ def initialize(memory_at_start)
55
+ add_node(:total, memory_at_start)
56
+ end
57
+
58
+ def add_node(item, memory_at_start)
59
+ @head = LinkedListNode.new(
60
+ item: item,
61
+ previous: head,
62
+ memory_at_start: memory_at_start
63
+ )
64
+ end
65
+
66
+ def remove_node(item, memory_at_finish)
67
+ return if head.item != item
68
+ head.finish(memory_at_finish)
69
+
70
+ current = head
71
+ @head = head.previous
72
+
73
+ current
74
+ end
75
+ end
76
+
77
+ class LinkedListNode
78
+ attr_reader :item, :previous, :memory_at_start, :memory_at_finish, :nested_memory
79
+
80
+ def initialize(item:, memory_at_start:, previous:)
81
+ @item = item
82
+ @previous = previous
83
+
84
+ @memory_at_start = memory_at_start || 0
85
+ @memory_at_finish = nil
86
+ @nested_memory = 0
87
+ end
88
+
89
+ def total_memory
90
+ return 0 if memory_at_finish.nil?
91
+ # It seems that on Windows Minitest may release a lot of memory to
92
+ # the OS when it finishes and executes #report, leading to memory_at_finish
93
+ # being less than memory_at_start. In this case we return nested_memory
94
+ # which does not account for the memory used in `after` hooks, but it
95
+ # is better than nothing.
96
+ return nested_memory if memory_at_start > memory_at_finish
97
+
98
+ memory_at_finish - memory_at_start
99
+ end
100
+
101
+ def hooks_memory
102
+ total_memory - nested_memory
103
+ end
104
+
105
+ def finish(memory_at_finish)
106
+ @memory_at_finish = memory_at_finish
107
+
108
+ previous&.add_nested(self)
109
+ end
110
+
111
+ protected
112
+
113
+ def add_nested(node)
114
+ @nested_memory += node.total_memory
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module TestProf
6
+ module MemoryProf
7
+ class Tracker
8
+ module RssTool
9
+ class ProcFS
10
+ def initialize
11
+ @statm = File.open("/proc/#{$$}/statm", "r")
12
+ @page_size = get_page_size
13
+ end
14
+
15
+ def track
16
+ @statm.seek(0)
17
+ @statm.gets.split(/\s/)[1].to_i * @page_size
18
+ end
19
+
20
+ private
21
+
22
+ def get_page_size
23
+ [
24
+ -> {
25
+ require "etc"
26
+ Etc.sysconf(Etc::SC_PAGE_SIZE)
27
+ },
28
+ -> { `getconf PAGE_SIZE`.to_i },
29
+ -> { 0x1000 }
30
+ ].each do |strategy|
31
+ page_size = begin
32
+ strategy.call
33
+ rescue
34
+ next
35
+ end
36
+ return page_size
37
+ end
38
+ end
39
+ end
40
+
41
+ class PS
42
+ def track
43
+ `ps -o rss -p #{$$}`.strip.split.last.to_i * 1024
44
+ end
45
+ end
46
+
47
+ class GetProcess
48
+ def track
49
+ command.strip.split.last.to_i
50
+ end
51
+
52
+ private
53
+
54
+ def command
55
+ `powershell -Command "Get-Process -Id #{$$} | select WS"`
56
+ end
57
+ end
58
+
59
+ TOOLS = {
60
+ linux: ProcFS,
61
+ macosx: PS,
62
+ unix: PS,
63
+ windows: GetProcess
64
+ }.freeze
65
+
66
+ class << self
67
+ def tool
68
+ TOOLS[os_type]&.new
69
+ end
70
+
71
+ def os_type
72
+ case RbConfig::CONFIG["host_os"]
73
+ when /linux/
74
+ :linux
75
+ when /darwin|mac os/
76
+ :macosx
77
+ when /solaris|bsd/
78
+ :unix
79
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
80
+ :windows
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/memory_prof/tracker/linked_list"
4
+ require "test_prof/memory_prof/tracker/rss_tool"
5
+
6
+ module TestProf
7
+ module MemoryProf
8
+ # Tracker is responsible for tracking memory usage and determining
9
+ # the top n examples and groups. There are two types of trackers:
10
+ # AllocTracker and RssTracker.
11
+ #
12
+ # A tracker consists of four main parts:
13
+ # * list - a linked list that is being used to track memmory for individual groups/examples.
14
+ # list is an instance of LinkedList (for more info see tracker/linked_list.rb)
15
+ # * examples – the top n examples, an instance of Utils::SizedOrderedSet.
16
+ # * groups – the top n groups, an instance of Utils::SizedOrderedSet.
17
+ # * track - a method that fetches the amount of memory in use at a certain point.
18
+ class Tracker
19
+ attr_reader :top_count, :examples, :groups, :total_memory, :list
20
+
21
+ def initialize(top_count)
22
+ raise "Your Ruby Engine or OS is not supported" unless supported?
23
+
24
+ @top_count = top_count
25
+
26
+ @examples = Utils::SizedOrderedSet.new(top_count, sort_by: :memory)
27
+ @groups = Utils::SizedOrderedSet.new(top_count, sort_by: :memory)
28
+ end
29
+
30
+ def start
31
+ @list = LinkedList.new(track)
32
+ end
33
+
34
+ def finish
35
+ node = list.remove_node(:total, track)
36
+ @total_memory = node.total_memory
37
+ end
38
+
39
+ def example_started(example)
40
+ list.add_node(example, track)
41
+ end
42
+
43
+ def example_finished(example)
44
+ node = list.remove_node(example, track)
45
+ examples << {**example, memory: node.total_memory}
46
+ end
47
+
48
+ def group_started(group)
49
+ list.add_node(group, track)
50
+ end
51
+
52
+ def group_finished(group)
53
+ node = list.remove_node(group, track)
54
+ groups << {**group, memory: node.hooks_memory}
55
+ end
56
+ end
57
+
58
+ class AllocTracker < Tracker
59
+ def track
60
+ GC.stat[:total_allocated_objects]
61
+ end
62
+
63
+ def supported?
64
+ RUBY_ENGINE != "jruby"
65
+ end
66
+ end
67
+
68
+ class RssTracker < Tracker
69
+ def initialize(top_count)
70
+ @rss_tool = RssTool.tool
71
+
72
+ super
73
+ end
74
+
75
+ def track
76
+ @rss_tool.track
77
+ end
78
+
79
+ def supported?
80
+ !!@rss_tool
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/memory_prof/tracker"
4
+ require "test_prof/memory_prof/printer"
5
+
6
+ module TestProf
7
+ # MemoryProf can help in detecting test examples causing memory spikes.
8
+ # It supports two metrics: RSS and allocations.
9
+ #
10
+ # Example:
11
+ #
12
+ # TEST_MEM_PROF='rss' rspec ...
13
+ # TEST_MEM_PROF='alloc' rspec ...
14
+ #
15
+ # By default MemoryProf shows the top 5 examples and groups (for RSpec) but you can
16
+ # set how many items to display with `TEST_MEM_PROF_COUNT`:
17
+ #
18
+ # TEST_MEM_PROF='rss' TEST_MEM_PROF_COUNT=10 rspec ...
19
+ #
20
+ # The examples block shows the amount of memory used by each example, and the groups
21
+ # block displays the memory allocated by other code defined in the groups. For example,
22
+ # RSpec groups may include heavy `before(:all)` (or `before_all`) setup blocks, so it is
23
+ # helpful to see which groups use the most amount of memory outside of their examples.
24
+
25
+ module MemoryProf
26
+ # MemoryProf configuration
27
+ class Configuration
28
+ attr_reader :mode, :top_count
29
+
30
+ def initialize
31
+ self.mode = ENV["TEST_MEM_PROF"]
32
+ self.top_count = ENV["TEST_MEM_PROF_COUNT"]
33
+ end
34
+
35
+ def mode=(value)
36
+ @mode = (value == "alloc") ? :alloc : :rss
37
+ end
38
+
39
+ def top_count=(value)
40
+ @top_count = value.to_i
41
+ @top_count = 5 unless @top_count.positive?
42
+ end
43
+ end
44
+
45
+ class << self
46
+ TRACKERS = {
47
+ alloc: AllocTracker,
48
+ rss: RssTracker
49
+ }.freeze
50
+
51
+ PRINTERS = {
52
+ alloc: AllocPrinter,
53
+ rss: RssPrinter
54
+ }.freeze
55
+
56
+ def config
57
+ @config ||= Configuration.new
58
+ end
59
+
60
+ def configure
61
+ yield config
62
+ end
63
+
64
+ def tracker
65
+ tracker = TRACKERS[config.mode]
66
+ tracker.new(config.top_count)
67
+ end
68
+
69
+ def printer(tracker)
70
+ printer = PRINTERS[config.mode]
71
+ printer.new(tracker)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ require "test_prof/memory_prof/rspec" if TestProf.rspec?
78
+ require "test_prof/memory_prof/minitest" if TestProf.minitest?
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof"
3
+ require "test_prof/core"
4
4
 
5
5
  module TestProf
6
6
  module Rails
@@ -6,9 +6,9 @@ module TestProf
6
6
  # Do not add these classes to resulted sample
7
7
  CORE_RUNNABLES = [
8
8
  Minitest::Test,
9
- Minitest::Unit::TestCase,
9
+ defined?(Minitest::Unit::TestCase) ? Minitest::Unit::TestCase : nil,
10
10
  Minitest::Spec
11
- ].freeze
11
+ ].compact.freeze
12
12
 
13
13
  class << self
14
14
  def suites
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof"
3
+ require "test_prof/core"
4
4
  require "test_prof/recipes/rspec/before_all"
5
5
 
6
6
  module TestProf
@@ -53,7 +53,7 @@ module TestProf
53
53
  def initialize
54
54
  @printer = ENV["TEST_RUBY_PROF"].to_sym if PRINTERS.key?(ENV["TEST_RUBY_PROF"])
55
55
  @printer ||= ENV.fetch("TEST_RUBY_PROF_PRINTER", :flat).to_sym
56
- @mode = ENV.fetch("TEST_RUBY_PROF_MODE", :wall).to_sym
56
+ @mode = ENV.fetch("TEST_RUBY_PROF_MODE", :wall).to_s
57
57
  @min_percent = 1
58
58
  @include_threads = false
59
59
  @exclude_common_methods = true
@@ -84,6 +84,22 @@ module TestProf
84
84
 
85
85
  [type, ::RubyProf.const_get(PRINTERS[type])]
86
86
  end
87
+
88
+ # Based on deprecated https://github.com/ruby-prof/ruby-prof/blob/fd3a5236a459586c5ca7ce4de506c1835129516a/lib/ruby-prof.rb#L36
89
+ def ruby_prof_mode
90
+ case mode
91
+ when "wall", "wall_time"
92
+ ::RubyProf::WALL_TIME
93
+ when "allocations"
94
+ ::RubyProf::ALLOCATIONS
95
+ when "memory"
96
+ ::RubyProf::MEMORY
97
+ when "process", "process_time"
98
+ ::RubyProf::PROCESS_TIME
99
+ else
100
+ ::RubyProf::WALL_TIME
101
+ end
102
+ end
87
103
  end
88
104
 
89
105
  # Wrapper over RubyProf profiler and printer
@@ -166,7 +182,7 @@ module TestProf
166
182
  log :warn, <<~MSG
167
183
  RubyProf is activated globally, you cannot generate per-example report.
168
184
 
169
- Make sure you haven's set the TEST_RUBY_PROF environmental variable.
185
+ Make sure you haven't set the TEST_RUBY_PROF environmental variable.
170
186
  MSG
171
187
  return
172
188
  end
@@ -177,6 +193,7 @@ module TestProf
177
193
 
178
194
  options[:include_threads] = [Thread.current] unless
179
195
  config.include_threads?
196
+ options[:measure_mode] = config.ruby_prof_mode
180
197
 
181
198
  profiler = ::RubyProf::Profile.new(options)
182
199
  profiler.exclude_common_methods! if config.exclude_common_methods?
@@ -206,7 +223,6 @@ module TestProf
206
223
 
207
224
  def init_ruby_prof
208
225
  return @initialized if instance_variable_defined?(:@initialized)
209
- ENV["RUBY_PROF_MEASURE_MODE"] = config.mode.to_s
210
226
  @initialized = TestProf.require(
211
227
  "ruby-prof",
212
228
  <<~MSG
@@ -19,12 +19,13 @@ module TestProf
19
19
 
20
20
  def example_started(notification)
21
21
  return unless profile?(notification.example)
22
+
22
23
  notification.example.metadata[:sprof_report] = TestProf::StackProf.profile
23
24
  end
24
25
 
25
26
  def example_finished(notification)
26
27
  return unless profile?(notification.example)
27
- return unless notification.example.metadata[:sprof_report] == false
28
+ return if notification.example.metadata[:sprof_report] == false
28
29
 
29
30
  TestProf::StackProf.dump(
30
31
  self.class.report_name_generator.call(notification.example)
@@ -33,7 +33,7 @@ module TestProf
33
33
  if FORMATS.include?(ENV["TEST_STACK_PROF_FORMAT"])
34
34
  ENV["TEST_STACK_PROF_FORMAT"]
35
35
  else
36
- "html"
36
+ "json"
37
37
  end
38
38
 
39
39
  sample_interval = ENV["TEST_STACK_PROF_INTERVAL"].to_i
@@ -81,9 +81,9 @@ module TestProf
81
81
  def profile(name = nil)
82
82
  if locked?
83
83
  log :warn, <<~MSG
84
- StackProf is activated globally, you cannot generate per-example report.
84
+ StackProf has been already activated.
85
85
 
86
- Make sure you haven's set the TEST_STACK_PROF environmental variable.
86
+ Make sure you have not set the TEST_STACK_PROF environmental variable.
87
87
  MSG
88
88
  return false
89
89
  end
@@ -53,6 +53,10 @@ module TestProf
53
53
  data.size
54
54
  end
55
55
 
56
+ def empty?
57
+ size.zero?
58
+ end
59
+
56
60
  def to_a
57
61
  data.dup
58
62
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/utils/rspec"
4
+
5
+ module TestProf
6
+ module Vernier
7
+ # Reporter for RSpec to profile specific examples with Vernier
8
+ class Listener # :nodoc:
9
+ class << self
10
+ attr_accessor :report_name_generator
11
+ end
12
+
13
+ self.report_name_generator = Utils::RSpec.method(:example_to_filename)
14
+
15
+ NOTIFICATIONS = %i[
16
+ example_started
17
+ example_finished
18
+ ].freeze
19
+
20
+ def example_started(notification)
21
+ return unless profile?(notification.example)
22
+ notification.example.metadata[:vernier_collector] = TestProf::Vernier.profile
23
+ end
24
+
25
+ def example_finished(notification)
26
+ return unless profile?(notification.example)
27
+ return unless notification.example.metadata[:vernier_collector]
28
+
29
+ TestProf::Vernier.dump(
30
+ notification.example.metadata[:vernier_collector],
31
+ self.class.report_name_generator.call(notification.example)
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ def profile?(example)
38
+ example.metadata.key?(:vernier)
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ RSpec.configure do |config|
45
+ config.before(:suite) do
46
+ listener = TestProf::Vernier::Listener.new
47
+
48
+ config.reporter.register_listener(
49
+ listener, *TestProf::Vernier::Listener::NOTIFICATIONS
50
+ )
51
+ end
52
+ end
53
+
54
+ # Handle boot profiling
55
+ RSpec.configure do |config|
56
+ config.append_before(:suite) do
57
+ TestProf::Vernier.dump(TestProf::Vernier.default_collector, "boot") if TestProf::Vernier.config.boot?
58
+ end
59
+ end