test-prof 1.2.3 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -3
- data/README.md +1 -1
- data/lib/minitest/test_prof_plugin.rb +10 -0
- data/lib/test_prof/before_all/adapters/active_record.rb +3 -1
- data/lib/test_prof/memory_prof/minitest.rb +56 -0
- data/lib/test_prof/memory_prof/printer/number_to_human.rb +44 -0
- data/lib/test_prof/memory_prof/printer.rb +93 -0
- data/lib/test_prof/memory_prof/rspec.rb +76 -0
- data/lib/test_prof/memory_prof/tracker/linked_list.rb +119 -0
- data/lib/test_prof/memory_prof/tracker/rss_tool.rb +87 -0
- data/lib/test_prof/memory_prof/tracker.rb +84 -0
- data/lib/test_prof/memory_prof.rb +78 -0
- data/lib/test_prof/stack_prof/rspec.rb +2 -1
- data/lib/test_prof/stack_prof.rb +3 -3
- data/lib/test_prof/utils/sized_ordered_set.rb +4 -0
- data/lib/test_prof/vernier/rspec.rb +59 -0
- data/lib/test_prof/vernier.rb +152 -0
- data/lib/test_prof/version.rb +1 -1
- data/lib/test_prof.rb +2 -0
- metadata +13 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0678bc968a267ebeece00e12338f6408683140428ecdd041ece64c70e6bb91c0'
|
4
|
+
data.tar.gz: 3ab5bc88b7b5dea557847f05a16d7fc3e4c6c9212115cd16984a56b7dd7b1737
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b32e7e689cef27e72ce5f7e65ca0463bae7b83dfcaf77eba17aa0cc9bae1b74d6223b6b141e805debb7640b74b75be34bf238a8f41cb3efaf0386929935d814
|
7
|
+
data.tar.gz: 34200774bc30c39acc48109fbc5031fd8f48abb5f7175799a1e1bbfd7650a11a441c9ba63638bf186d78ef5cd15b7b33e64782c781a7f28bd43e43cb6d1e5588
|
data/CHANGELOG.md
CHANGED
@@ -2,9 +2,17 @@
|
|
2
2
|
|
3
3
|
## master (unreleased)
|
4
4
|
|
5
|
+
## 1.3.0 (2023-11-21)
|
6
|
+
|
7
|
+
- Add Vernier integration. ([@palkan][])
|
8
|
+
|
9
|
+
- StackProf uses JSON format by default. ([@palkan][])
|
10
|
+
|
11
|
+
- MemoryProf ia added. ([@Vankiru][])
|
12
|
+
|
5
13
|
## 1.2.3 (2023-09-11)
|
6
14
|
|
7
|
-
- Minor fixes and
|
15
|
+
- Minor fixes and dependencies upgrades.
|
8
16
|
|
9
17
|
## 1.2.2 (2023-06-27)
|
10
18
|
|
@@ -160,7 +168,7 @@ And for every test run see the overall factories usage:
|
|
160
168
|
|
161
169
|
## 1.0.0.rc2 (2021-01-06)
|
162
170
|
|
163
|
-
- Make Rails fixtures
|
171
|
+
- Make Rails fixtures accessible in `before_all`. ([@palkan][])
|
164
172
|
|
165
173
|
You can load and access fixtures when explicitly enabling them via `before_all(setup_fixtures: true, &block)`.
|
166
174
|
|
@@ -221,7 +229,7 @@ end
|
|
221
229
|
|
222
230
|
See more in [#181](https://github.com/test-prof/test-prof/issues/181).
|
223
231
|
|
224
|
-
- Adds the ability to define stackprof
|
232
|
+
- Adds the ability to define stackprof interval sampling by using `TEST_STACK_PROF_INTERVAL` env variable ([@LynxEyes][])
|
225
233
|
|
226
234
|
Now you can use `$ TEST_STACK_PROF=1 TEST_STACK_PROF_INTERVAL=10000 rspec` to define a custom interval (in microseconds).
|
227
235
|
|
@@ -367,3 +375,4 @@ See [changelog](https://github.com/test-prof/test-prof/blob/v0.8.0/CHANGELOG.md)
|
|
367
375
|
[@cbarton]: https://github.com/cbarton
|
368
376
|
[@peret]: https://github.com/peret
|
369
377
|
[@bf4]: https://github.com/bf4
|
378
|
+
[@Vankiru]: https://github.com/Vankiru
|
data/README.md
CHANGED
@@ -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,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
|
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)
|
data/lib/test_prof/stack_prof.rb
CHANGED
@@ -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
|
-
"
|
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
|
84
|
+
StackProf has been already activated.
|
85
85
|
|
86
|
-
Make sure you
|
86
|
+
Make sure you have not set the TEST_STACK_PROF environmental variable.
|
87
87
|
MSG
|
88
88
|
return false
|
89
89
|
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
|
data/lib/test_prof/version.rb
CHANGED
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.
|
4
|
+
version: 1.3.0
|
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-
|
11
|
+
date: 2023-11-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -180,6 +180,14 @@ files:
|
|
180
180
|
- lib/test_prof/factory_prof/printers/nate_heckler.rb
|
181
181
|
- lib/test_prof/factory_prof/printers/simple.rb
|
182
182
|
- lib/test_prof/logging.rb
|
183
|
+
- lib/test_prof/memory_prof.rb
|
184
|
+
- lib/test_prof/memory_prof/minitest.rb
|
185
|
+
- lib/test_prof/memory_prof/printer.rb
|
186
|
+
- lib/test_prof/memory_prof/printer/number_to_human.rb
|
187
|
+
- lib/test_prof/memory_prof/rspec.rb
|
188
|
+
- lib/test_prof/memory_prof/tracker.rb
|
189
|
+
- lib/test_prof/memory_prof/tracker/linked_list.rb
|
190
|
+
- lib/test_prof/memory_prof/tracker/rss_tool.rb
|
183
191
|
- lib/test_prof/recipes/logging.rb
|
184
192
|
- lib/test_prof/recipes/minitest/before_all.rb
|
185
193
|
- lib/test_prof/recipes/minitest/sample.rb
|
@@ -212,6 +220,8 @@ files:
|
|
212
220
|
- lib/test_prof/utils/html_builder.rb
|
213
221
|
- lib/test_prof/utils/rspec.rb
|
214
222
|
- lib/test_prof/utils/sized_ordered_set.rb
|
223
|
+
- lib/test_prof/vernier.rb
|
224
|
+
- lib/test_prof/vernier/rspec.rb
|
215
225
|
- lib/test_prof/version.rb
|
216
226
|
homepage: http://github.com/test-prof/test-prof
|
217
227
|
licenses:
|
@@ -238,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
238
248
|
- !ruby/object:Gem::Version
|
239
249
|
version: '0'
|
240
250
|
requirements: []
|
241
|
-
rubygems_version: 3.4.
|
251
|
+
rubygems_version: 3.4.20
|
242
252
|
signing_key:
|
243
253
|
specification_version: 4
|
244
254
|
summary: Ruby applications tests profiling tools
|