test-prof 1.2.3 → 1.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9571b8fb7edc3bb1f92d9a6db079791a97c3cbeb11a29763ddc3e9f88bd5780a
4
- data.tar.gz: 4011afa256c8a5f250a6a62e9745ed254f5fe020a366e3b3e4c0a91afb227d68
3
+ metadata.gz: c81241143c4e7788e5fef3e5508534e878615a8e1b9e577a389d50da04cf445c
4
+ data.tar.gz: cd4d3ff2cee2cc93c569d8d25fa574653c8c9b29a1e67d97673b6e784e341146
5
5
  SHA512:
6
- metadata.gz: 9008e4dbd6131efde863a100447d0658663cfbea0486e3043664ddadff097f652230ddb162ee85893883eaf49c54ddeba806ba30563199857848466786e91d34
7
- data.tar.gz: f86145f0d6539f6de7b3cedd315583d977df01e7e64ff22c8300bbc7f29bfd946e9f738d3b3388696c3344a8ab9e444371993e90b79f204cacf06846d314b930
6
+ metadata.gz: 812d6f7fd0672cd5a08f3812aafb60b677be8ae8131620ca13495ae9f79b6b23bac3b3a9cec1c1f64bc6c7cba4cae156baa5fb2441756bf21dc41e2bfe2c4b70
7
+ data.tar.gz: 6990de149f5824297ea8ff60ed07cc4ad8098fbb52a990d602418aaf37222a9a0583895ec2fa8c9a78fc6e54c8abadb344cdf53468e2f39daf900bbb54377352
data/CHANGELOG.md CHANGED
@@ -2,9 +2,21 @@
2
2
 
3
3
  ## master (unreleased)
4
4
 
5
+ ## 1.3.1 (2023-12-12)
6
+
7
+ - Add support for dumping FactoryProf results in JSON format. ([@uzushino][])
8
+
9
+ ## 1.3.0 (2023-11-21)
10
+
11
+ - Add Vernier integration. ([@palkan][])
12
+
13
+ - StackProf uses JSON format by default. ([@palkan][])
14
+
15
+ - MemoryProf ia added. ([@Vankiru][])
16
+
5
17
  ## 1.2.3 (2023-09-11)
6
18
 
7
- - Minor fixes and dependecies upgrades.
19
+ - Minor fixes and dependencies upgrades.
8
20
 
9
21
  ## 1.2.2 (2023-06-27)
10
22
 
@@ -160,7 +172,7 @@ And for every test run see the overall factories usage:
160
172
 
161
173
  ## 1.0.0.rc2 (2021-01-06)
162
174
 
163
- - Make Rails fixtures accesible in `before_all`. ([@palkan][])
175
+ - Make Rails fixtures accessible in `before_all`. ([@palkan][])
164
176
 
165
177
  You can load and access fixtures when explicitly enabling them via `before_all(setup_fixtures: true, &block)`.
166
178
 
@@ -221,7 +233,7 @@ end
221
233
 
222
234
  See more in [#181](https://github.com/test-prof/test-prof/issues/181).
223
235
 
224
- - Adds the ability to define stackprof's interval sampling by using `TEST_STACK_PROF_INTERVAL` env variable ([@LynxEyes][])
236
+ - Adds the ability to define stackprof interval sampling by using `TEST_STACK_PROF_INTERVAL` env variable ([@LynxEyes][])
225
237
 
226
238
  Now you can use `$ TEST_STACK_PROF=1 TEST_STACK_PROF_INTERVAL=10000 rspec` to define a custom interval (in microseconds).
227
239
 
@@ -367,3 +379,5 @@ See [changelog](https://github.com/test-prof/test-prof/blob/v0.8.0/CHANGELOG.md)
367
379
  [@cbarton]: https://github.com/cbarton
368
380
  [@peret]: https://github.com/peret
369
381
  [@bf4]: https://github.com/bf4
382
+ [@Vankiru]: https://github.com/Vankiru
383
+ [@uzushino]: https://github.com/uzushino
data/README.md CHANGED
@@ -93,7 +93,7 @@ Supported RSpec version (for RSpec features only): >= 3.5.0 (for older RSpec ver
93
93
 
94
94
  Check out our [docs][].
95
95
 
96
- ## What's next?
96
+ ## What's next
97
97
 
98
98
  Have an idea? [Propose](https://github.com/test-prof/test-prof/issues/new) a feature request!
99
99
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "test_prof/event_prof/minitest"
4
4
  require "test_prof/factory_doctor/minitest"
5
+ require "test_prof/memory_prof/minitest"
5
6
 
6
7
  module Minitest # :nodoc:
7
8
  module TestProf # :nodoc:
@@ -13,6 +14,8 @@ module Minitest # :nodoc:
13
14
  opts[:per_example] = true if ENV["EVENT_PROF_EXAMPLES"]
14
15
  opts[:fdoc] = true if ENV["FDOC"]
15
16
  opts[:sample] = true if ENV["SAMPLE"] || ENV["SAMPLE_GROUPS"]
17
+ opts[:mem_prof_mode] = ENV["TEST_MEM_PROF"] if ENV["TEST_MEM_PROF"]
18
+ opts[:mem_prof_top_count] = ENV["TEST_MEM_PROF_COUNT"] if ENV["TEST_MEM_PROF_COUNT"]
16
19
  end
17
20
  end
18
21
  end
@@ -33,6 +36,12 @@ module Minitest # :nodoc:
33
36
  opts.on "--factory-doctor", TrueClass, "Enable Factory Doctor for your examples" do |flag|
34
37
  options[:fdoc] = flag
35
38
  end
39
+ opts.on "--mem-prof=MODE", "Enable MemoryProf for your examples" do |flag|
40
+ options[:mem_prof_mode] = flag
41
+ end
42
+ opts.on "--mem-prof-top-count=N", "Limits MemoryProf results with N groups/examples" do |flag|
43
+ options[:mem_prof_top_count] = flag
44
+ end
36
45
  end
37
46
 
38
47
  def self.plugin_test_prof_init(options)
@@ -40,6 +49,7 @@ module Minitest # :nodoc:
40
49
 
41
50
  reporter << TestProf::EventProfReporter.new(options[:io], options) if options[:event]
42
51
  reporter << TestProf::FactoryDoctorReporter.new(options[:io], options) if options[:fdoc]
52
+ reporter << TestProf::MemoryProfReporter.new(options[:io], options) if options[:mem_prof_mode]
43
53
 
44
54
  ::TestProf::MinitestSample.call if options[:sample]
45
55
  end
@@ -5,10 +5,12 @@ module TestProf
5
5
  module Adapters
6
6
  # ActiveRecord adapter for `before_all`
7
7
  module ActiveRecord
8
+ POOL_ARGS = ((::ActiveRecord::VERSION::MAJOR > 6) ? [:writing] : []).freeze
9
+
8
10
  class << self
9
11
  def all_connections
10
12
  @all_connections ||= if ::ActiveRecord::Base.respond_to? :connects_to
11
- ::ActiveRecord::Base.connection_handler.connection_pool_list.filter_map { |pool|
13
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(*POOL_ARGS).filter_map { |pool|
12
14
  begin
13
15
  pool.connection
14
16
  rescue *pool_connection_errors => error
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/ext/float_duration"
4
+
5
+ module TestProf::FactoryProf
6
+ module Printers
7
+ module Json # :nodoc: all
8
+ class << self
9
+ using TestProf::FloatDuration
10
+ include TestProf::Logging
11
+
12
+ def dump(result, start_time:)
13
+ return log(:info, "No factories detected") if result.raw_stats == {}
14
+
15
+ outpath = TestProf.artifact_path("test-prof.result.json")
16
+ File.write(outpath, convert_stats(result, start_time).to_json)
17
+
18
+ log :info, "Profile results to JSON: #{outpath}"
19
+ end
20
+
21
+ def convert_stats(result, start_time)
22
+ total_run_time = TestProf.now - start_time
23
+ total_count = result.stats.sum { |stat| stat[:total_count] }
24
+ total_top_level_count = result.stats.sum { |stat| stat[:top_level_count] }
25
+ total_time = result.stats.sum { |stat| stat[:top_level_time] }
26
+ total_uniq_factories = result.stats.map { |stat| stat[:name] }.uniq.count
27
+
28
+ {
29
+ total_count: total_count,
30
+ total_top_level_count: total_top_level_count,
31
+ total_time: total_time.duration,
32
+ total_run_time: total_run_time.duration,
33
+ total_uniq_factories: total_uniq_factories,
34
+
35
+ stats: result.stats
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -3,6 +3,7 @@
3
3
  require "test_prof/factory_prof/printers/simple"
4
4
  require "test_prof/factory_prof/printers/flamegraph"
5
5
  require "test_prof/factory_prof/printers/nate_heckler"
6
+ require "test_prof/factory_prof/printers/json"
6
7
  require "test_prof/factory_prof/factory_builders/factory_bot"
7
8
  require "test_prof/factory_prof/factory_builders/fabrication"
8
9
 
@@ -25,6 +26,8 @@ module TestProf
25
26
  Printers::Flamegraph
26
27
  when "nate_heckler"
27
28
  Printers::NateHeckler
29
+ when "json"
30
+ Printers::Json
28
31
  else
29
32
  Printers::Simple
30
33
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/base_reporter"
4
+
5
+ module Minitest
6
+ module TestProf
7
+ class MemoryProfReporter < BaseReporter # :nodoc:
8
+ attr_reader :tracker, :printer, :current_example
9
+
10
+ def initialize(io = $stdout, options = {})
11
+ super
12
+
13
+ configure_profiler(options)
14
+
15
+ @tracker = ::TestProf::MemoryProf.tracker
16
+ @printer = ::TestProf::MemoryProf.printer(tracker)
17
+
18
+ @current_example = nil
19
+ end
20
+
21
+ def prerecord(group, example)
22
+ set_current_example(group, example)
23
+ tracker.example_started(current_example)
24
+ end
25
+
26
+ def record(example)
27
+ tracker.example_finished(current_example)
28
+ end
29
+
30
+ def start
31
+ tracker.start
32
+ end
33
+
34
+ def report
35
+ tracker.finish
36
+ printer.print
37
+ end
38
+
39
+ private
40
+
41
+ def set_current_example(group, example)
42
+ @current_example = {
43
+ name: example.gsub(/^test_(?:\d+_)?/, ""),
44
+ location: location_with_line_number(group, example)
45
+ }
46
+ end
47
+
48
+ def configure_profiler(options)
49
+ ::TestProf::MemoryProf.configure do |config|
50
+ config.mode = options[:mem_prof_mode]
51
+ config.top_count = options[:mem_prof_top_count] if options[:mem_prof_top_count]
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module MemoryProf
5
+ class Printer
6
+ module NumberToHuman
7
+ BASE = 1024
8
+ UNITS = %w[B KB MB GB TB PB EB ZB]
9
+
10
+ class << self
11
+ def convert(number)
12
+ exponent = exponent(number)
13
+ human_size = number.to_f / (BASE**exponent)
14
+
15
+ "#{round(human_size)}#{UNITS[exponent]}"
16
+ end
17
+
18
+ private
19
+
20
+ def exponent(number)
21
+ return 0 unless number.positive?
22
+
23
+ max = UNITS.size - 1
24
+
25
+ exponent = (Math.log(number) / Math.log(BASE)).to_i
26
+ (exponent > max) ? max : exponent
27
+ end
28
+
29
+ def round(number)
30
+ if integer?(number)
31
+ number.round
32
+ else
33
+ number.round(2)
34
+ end
35
+ end
36
+
37
+ def integer?(number)
38
+ number.round == number
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/memory_prof/printer/number_to_human"
4
+ require "test_prof/ext/string_truncate"
5
+
6
+ module TestProf
7
+ module MemoryProf
8
+ class Printer
9
+ include Logging
10
+ using StringTruncate
11
+
12
+ def initialize(tracker)
13
+ @tracker = tracker
14
+ end
15
+
16
+ def print
17
+ messages = [
18
+ "MemoryProf results\n\n",
19
+ print_total,
20
+ print_block("groups", tracker.groups),
21
+ print_block("examples", tracker.examples)
22
+ ]
23
+
24
+ log :info, messages.join
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :tracker
30
+
31
+ def print_block(name, items)
32
+ return if items.empty?
33
+
34
+ <<~GROUP
35
+ Top #{tracker.top_count} #{name} (by #{mode}):
36
+
37
+ #{print_items(items)}
38
+ GROUP
39
+ end
40
+
41
+ def print_items(items)
42
+ messages =
43
+ items.map do |item|
44
+ <<~ITEM
45
+ #{item[:name].truncate(30)} (#{item[:location]}) – +#{memory_amount(item)} (#{memory_percentage(item)}%)
46
+ ITEM
47
+ end
48
+
49
+ messages.join
50
+ end
51
+
52
+ def memory_percentage(item)
53
+ (100.0 * item[:memory] / tracker.total_memory).round(2)
54
+ end
55
+
56
+ def number_to_human(value)
57
+ NumberToHuman.convert(value)
58
+ end
59
+ end
60
+
61
+ class AllocPrinter < Printer
62
+ private
63
+
64
+ def mode
65
+ "allocations"
66
+ end
67
+
68
+ def print_total
69
+ "Total allocations: #{tracker.total_memory}\n\n"
70
+ end
71
+
72
+ def memory_amount(item)
73
+ item[:memory]
74
+ end
75
+ end
76
+
77
+ class RssPrinter < Printer
78
+ private
79
+
80
+ def mode
81
+ "RSS"
82
+ end
83
+
84
+ def print_total
85
+ "Final RSS: #{number_to_human(tracker.total_memory)}\n\n"
86
+ end
87
+
88
+ def memory_amount(item)
89
+ number_to_human(item[:memory])
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module MemoryProf
5
+ class RSpecListener
6
+ NOTIFICATIONS = %i[
7
+ example_started
8
+ example_finished
9
+ example_group_started
10
+ example_group_finished
11
+ ].freeze
12
+
13
+ attr_reader :tracker, :printer
14
+
15
+ def initialize
16
+ @tracker = MemoryProf.tracker
17
+ @printer = MemoryProf.printer(tracker)
18
+
19
+ @tracker.start
20
+ end
21
+
22
+ def example_started(notification)
23
+ tracker.example_started(example(notification))
24
+ end
25
+
26
+ def example_finished(notification)
27
+ tracker.example_finished(example(notification))
28
+ end
29
+
30
+ def example_group_started(notification)
31
+ tracker.group_started(group(notification))
32
+ end
33
+
34
+ def example_group_finished(notification)
35
+ tracker.group_finished(group(notification))
36
+ end
37
+
38
+ def report
39
+ tracker.finish
40
+ printer.print
41
+ end
42
+
43
+ private
44
+
45
+ def example(notification)
46
+ {
47
+ name: notification.example.description,
48
+ location: notification.example.metadata[:location]
49
+ }
50
+ end
51
+
52
+ def group(notification)
53
+ {
54
+ name: notification.group.description,
55
+ location: notification.group.metadata[:location]
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ TestProf.activate("TEST_MEM_PROF") do
63
+ RSpec.configure do |config|
64
+ listener = nil
65
+
66
+ config.before(:suite) do
67
+ listener = TestProf::MemoryProf::RSpecListener.new
68
+
69
+ config.reporter.register_listener(
70
+ listener, *TestProf::MemoryProf::RSpecListener::NOTIFICATIONS
71
+ )
72
+ end
73
+
74
+ config.after(:suite) { listener&.report }
75
+ end
76
+ end
@@ -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?
@@ -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't 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
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ # Vernier wrapper.
5
+ #
6
+ # Has 2 modes: global and per-example.
7
+ #
8
+ # Example:
9
+ #
10
+ # # To activate global profiling you can use env variable
11
+ # TEST_VERNIER=1 rspec ...
12
+ #
13
+ # To profile a specific examples add :vernier tag to it:
14
+ #
15
+ # it "is doing heavy stuff", :vernier do
16
+ # ...
17
+ # end
18
+ #
19
+ module Vernier
20
+ # Vernier configuration
21
+ class Configuration
22
+ attr_accessor :mode, :target, :interval
23
+
24
+ def initialize
25
+ @mode = ENV.fetch("TEST_VERNIER_MODE", :wall).to_sym
26
+ @target = (ENV["TEST_VERNIER"] == "boot") ? :boot : :suite
27
+
28
+ sample_interval = ENV["TEST_VERNIER_INTERVAL"].to_i
29
+ @interval = (sample_interval > 0) ? sample_interval : nil
30
+ end
31
+
32
+ def boot?
33
+ target == :boot
34
+ end
35
+
36
+ def suite?
37
+ target == :suite
38
+ end
39
+ end
40
+
41
+ class << self
42
+ include Logging
43
+
44
+ def config
45
+ @config ||= Configuration.new
46
+ end
47
+
48
+ def configure
49
+ yield config
50
+ end
51
+
52
+ attr_reader :default_collector
53
+
54
+ # Run Vernier and automatically dump
55
+ # a report when the process exits or when the application is booted.
56
+ def run
57
+ collector = profile
58
+ return unless collector
59
+
60
+ @locked = true
61
+ @default_collector = collector
62
+
63
+ log :info, "Vernier enabled globally: " \
64
+ "mode – #{config.mode}, target – #{config.target}"
65
+
66
+ at_exit { dump(collector, "total") } if config.suite?
67
+ end
68
+
69
+ def profile(name = nil)
70
+ if locked?
71
+ log :warn, <<~MSG
72
+ Vernier has been already activated.
73
+
74
+ Make sure you do not have the TEST_VERNIER environmental variable set somewhere.
75
+ MSG
76
+
77
+ return false
78
+ end
79
+
80
+ return false unless init_vernier
81
+
82
+ options = {}
83
+
84
+ options[:interval] = config.interval if config.interval
85
+
86
+ if block_given?
87
+ options[:mode] = config.mode
88
+ options[:out] = build_path(name)
89
+ ::Vernier.trace(**options) { yield }
90
+ else
91
+ collector = ::Vernier::Collector.new(config.mode, **options)
92
+ collector.start
93
+
94
+ collector
95
+ end
96
+ end
97
+
98
+ def dump(collector, name)
99
+ result = collector.stop
100
+
101
+ path = build_path(name)
102
+
103
+ File.write(path, ::Vernier::Output::Firefox.new(result).output)
104
+
105
+ log :info, "Vernier report generated: #{path}"
106
+ end
107
+
108
+ private
109
+
110
+ def build_path(name)
111
+ TestProf.artifact_path(
112
+ "vernier-report-#{config.mode}-#{name}.json"
113
+ )
114
+ end
115
+
116
+ def locked?
117
+ @locked == true
118
+ end
119
+
120
+ def init_vernier
121
+ return @initialized if instance_variable_defined?(:@initialized)
122
+ @locked = false
123
+ @initialized = TestProf.require(
124
+ "vernier",
125
+ <<~MSG
126
+ Please, install 'vernier' first:
127
+ # Gemfile
128
+ gem 'vernier', '>= 0.3.0', require: false
129
+ MSG
130
+ ) { check_vernier_version }
131
+ end
132
+
133
+ def check_vernier_version
134
+ if Utils.verify_gem_version("vernier", at_least: "0.3.0")
135
+ true
136
+ else
137
+ log :error, <<~MSG
138
+ Please, upgrade 'vernier' to version >= 0.3.0.
139
+ MSG
140
+ false
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ require "test_prof/vernier/rspec" if TestProf.rspec?
148
+
149
+ # Hook to run Vernier globally
150
+ TestProf.activate("TEST_VERNIER") do
151
+ TestProf::Vernier.run
152
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestProf
4
- VERSION = "1.2.3"
4
+ VERSION = "1.3.1"
5
5
  end
data/lib/test_prof.rb CHANGED
@@ -5,9 +5,11 @@ require "test_prof/core"
5
5
 
6
6
  require "test_prof/ruby_prof"
7
7
  require "test_prof/stack_prof"
8
+ require "test_prof/vernier"
8
9
  require "test_prof/event_prof"
9
10
  require "test_prof/factory_doctor"
10
11
  require "test_prof/factory_prof"
12
+ require "test_prof/memory_prof"
11
13
  require "test_prof/rspec_stamp"
12
14
  require "test_prof/tag_prof"
13
15
  require "test_prof/rspec_dissect" if TestProf.rspec?
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.2.3
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-12 00:00:00.000000000 Z
11
+ date: 2023-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -177,9 +177,18 @@ files:
177
177
  - lib/test_prof/factory_prof/factory_builders/factory_bot.rb
178
178
  - lib/test_prof/factory_prof/nate_heckler.rb
179
179
  - lib/test_prof/factory_prof/printers/flamegraph.rb
180
+ - lib/test_prof/factory_prof/printers/json.rb
180
181
  - lib/test_prof/factory_prof/printers/nate_heckler.rb
181
182
  - lib/test_prof/factory_prof/printers/simple.rb
182
183
  - lib/test_prof/logging.rb
184
+ - lib/test_prof/memory_prof.rb
185
+ - lib/test_prof/memory_prof/minitest.rb
186
+ - lib/test_prof/memory_prof/printer.rb
187
+ - lib/test_prof/memory_prof/printer/number_to_human.rb
188
+ - lib/test_prof/memory_prof/rspec.rb
189
+ - lib/test_prof/memory_prof/tracker.rb
190
+ - lib/test_prof/memory_prof/tracker/linked_list.rb
191
+ - lib/test_prof/memory_prof/tracker/rss_tool.rb
183
192
  - lib/test_prof/recipes/logging.rb
184
193
  - lib/test_prof/recipes/minitest/before_all.rb
185
194
  - lib/test_prof/recipes/minitest/sample.rb
@@ -212,6 +221,8 @@ files:
212
221
  - lib/test_prof/utils/html_builder.rb
213
222
  - lib/test_prof/utils/rspec.rb
214
223
  - lib/test_prof/utils/sized_ordered_set.rb
224
+ - lib/test_prof/vernier.rb
225
+ - lib/test_prof/vernier/rspec.rb
215
226
  - lib/test_prof/version.rb
216
227
  homepage: http://github.com/test-prof/test-prof
217
228
  licenses:
@@ -238,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
238
249
  - !ruby/object:Gem::Version
239
250
  version: '0'
240
251
  requirements: []
241
- rubygems_version: 3.4.8
252
+ rubygems_version: 3.4.20
242
253
  signing_key:
243
254
  specification_version: 4
244
255
  summary: Ruby applications tests profiling tools