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 +4 -4
- data/CHANGELOG.md +17 -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/factory_prof/printers/json.rb +41 -0
- data/lib/test_prof/factory_prof.rb +3 -0
- 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 +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c81241143c4e7788e5fef3e5508534e878615a8e1b9e577a389d50da04cf445c
|
4
|
+
data.tar.gz: cd4d3ff2cee2cc93c569d8d25fa574653c8c9b29a1e67d97673b6e784e341146
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
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
@@ -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
|
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.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-
|
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.
|
252
|
+
rubygems_version: 3.4.20
|
242
253
|
signing_key:
|
243
254
|
specification_version: 4
|
244
255
|
summary: Ruby applications tests profiling tools
|