test-prof 1.2.3 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
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