vernier 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +1 -0
- data/README.md +65 -0
- data/examples/custom_hook.rb +37 -0
- data/exe/vernier +1 -1
- data/ext/vernier/heap_tracker.cc +277 -0
- data/ext/vernier/memory.cc +1 -1
- data/ext/vernier/stack_table.cc +290 -0
- data/ext/vernier/stack_table.hh +314 -0
- data/ext/vernier/vernier.cc +67 -791
- data/ext/vernier/vernier.hh +7 -0
- data/lib/vernier/collector.rb +112 -2
- data/lib/vernier/heap_tracker.rb +47 -0
- data/lib/vernier/memory_leak_detector.rb +40 -0
- data/lib/vernier/output/firefox.rb +64 -17
- data/lib/vernier/output/top.rb +6 -4
- data/lib/vernier/result.rb +37 -22
- data/lib/vernier/stack_table_helpers.rb +24 -10
- data/lib/vernier/version.rb +1 -1
- data/lib/vernier.rb +2 -6
- metadata +8 -2
data/ext/vernier/vernier.hh
CHANGED
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
#include "ruby.h"
|
|
5
5
|
|
|
6
|
+
// HACK: This isn't public, but the objspace ext uses it
|
|
7
|
+
extern "C" size_t rb_obj_memsize_of(VALUE);
|
|
8
|
+
|
|
9
|
+
#define sym(name) ID2SYM(rb_intern_const(name))
|
|
10
|
+
|
|
6
11
|
extern VALUE rb_mVernier;
|
|
7
12
|
|
|
8
13
|
void Init_memory();
|
|
14
|
+
void Init_stack_table();
|
|
15
|
+
void Init_heap_tracker();
|
|
9
16
|
|
|
10
17
|
#endif /* VERNIER_H */
|
data/lib/vernier/collector.rb
CHANGED
|
@@ -5,6 +5,110 @@ require_relative "thread_names"
|
|
|
5
5
|
|
|
6
6
|
module Vernier
|
|
7
7
|
class Collector
|
|
8
|
+
class CustomCollector < Collector
|
|
9
|
+
def initialize(mode, options)
|
|
10
|
+
@stack_table = StackTable.new
|
|
11
|
+
|
|
12
|
+
@samples = []
|
|
13
|
+
@timestamps = []
|
|
14
|
+
|
|
15
|
+
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def sample
|
|
20
|
+
@samples << @stack_table.current_stack
|
|
21
|
+
@timestamps << Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def start
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def finish
|
|
28
|
+
result = Result.new
|
|
29
|
+
result.instance_variable_set(:@threads, {
|
|
30
|
+
0 => {
|
|
31
|
+
tid: 0,
|
|
32
|
+
name: "custom",
|
|
33
|
+
started_at: @started_at,
|
|
34
|
+
samples: @samples,
|
|
35
|
+
weights: [1] * @samples.size,
|
|
36
|
+
timestamps: @timestamps,
|
|
37
|
+
sample_categories: [0] * @samples.size,
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
result.instance_variable_set(:@meta, {
|
|
41
|
+
started_at: @started_at
|
|
42
|
+
})
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class RetainedCollector < Collector
|
|
48
|
+
def initialize(mode, options)
|
|
49
|
+
@stack_table = StackTable.new
|
|
50
|
+
@heap_tracker = HeapTracker.new(@stack_table)
|
|
51
|
+
|
|
52
|
+
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def start
|
|
57
|
+
@heap_tracker.collect
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def drain
|
|
61
|
+
@heap_tracker.drain
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def finish
|
|
65
|
+
@heap_tracker.drain
|
|
66
|
+
|
|
67
|
+
GC.start
|
|
68
|
+
|
|
69
|
+
@stack_table.finalize
|
|
70
|
+
|
|
71
|
+
GC.start
|
|
72
|
+
|
|
73
|
+
@heap_tracker.lock
|
|
74
|
+
tracker_data = @heap_tracker.data
|
|
75
|
+
|
|
76
|
+
samples = tracker_data.fetch(:samples)
|
|
77
|
+
weights = tracker_data.fetch(:weights)
|
|
78
|
+
|
|
79
|
+
result = Result.new
|
|
80
|
+
result.instance_variable_set(:@threads, {
|
|
81
|
+
0 => {
|
|
82
|
+
tid: 0,
|
|
83
|
+
name: "retained memory",
|
|
84
|
+
started_at: @started_at,
|
|
85
|
+
samples: samples,
|
|
86
|
+
weights: weights,
|
|
87
|
+
sample_categories: [0] * samples.size,
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
result.instance_variable_set(:@meta, {
|
|
91
|
+
started_at: @started_at
|
|
92
|
+
})
|
|
93
|
+
result
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.new(mode, options = {})
|
|
98
|
+
return super unless Collector.equal?(self)
|
|
99
|
+
|
|
100
|
+
case mode
|
|
101
|
+
when :wall
|
|
102
|
+
TimeCollector.new(mode, options)
|
|
103
|
+
when :custom
|
|
104
|
+
CustomCollector.new(mode, options)
|
|
105
|
+
when :retained
|
|
106
|
+
RetainedCollector.new(mode, options)
|
|
107
|
+
else
|
|
108
|
+
raise ArgumentError, "invalid mode: #{mode.inspect}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
8
112
|
def initialize(mode, options = {})
|
|
9
113
|
@gc = options.fetch(:gc, true) && (mode == :retained)
|
|
10
114
|
GC.start if @gc
|
|
@@ -30,14 +134,20 @@ module Vernier
|
|
|
30
134
|
@user_metadata = options[:metadata] || {}
|
|
31
135
|
end
|
|
32
136
|
|
|
137
|
+
attr_reader :stack_table
|
|
138
|
+
|
|
33
139
|
private def add_hook(hook)
|
|
34
|
-
case hook.to_sym
|
|
140
|
+
case hook.to_s.to_sym
|
|
35
141
|
when :rails, :activesupport
|
|
36
142
|
@hooks << Vernier::Hooks::ActiveSupport.new(self)
|
|
37
143
|
when :memory_usage
|
|
38
144
|
@hooks << Vernier::Hooks::MemoryUsage.new(self)
|
|
39
145
|
else
|
|
40
|
-
|
|
146
|
+
if hook.respond_to?(:new)
|
|
147
|
+
@hooks << hook.new(self)
|
|
148
|
+
else
|
|
149
|
+
warn "unknown hook: #{hook.inspect}"
|
|
150
|
+
end
|
|
41
151
|
end
|
|
42
152
|
end
|
|
43
153
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vernier
|
|
4
|
+
# Plan: The heap tracker can be in a few states:
|
|
5
|
+
# * Idle
|
|
6
|
+
# * Collecting
|
|
7
|
+
# * Watching for new objects
|
|
8
|
+
# * Watching for freed objects
|
|
9
|
+
# * Draining
|
|
10
|
+
# * Ignoring new objects
|
|
11
|
+
# * Watching for freed objects
|
|
12
|
+
# * Locked
|
|
13
|
+
# * Ignoring new objects
|
|
14
|
+
# * Ignoring freed objects
|
|
15
|
+
# * Marking all existing objects (not yet implemented)
|
|
16
|
+
# * N.B. This prevents any objects which the tracker has seen from being GC'd
|
|
17
|
+
class HeapTracker
|
|
18
|
+
attr_reader :stack_table
|
|
19
|
+
|
|
20
|
+
def self.new(stack_table = StackTable.new)
|
|
21
|
+
_new(stack_table)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def inspect
|
|
25
|
+
"#<#{self.class} allocated_objects=#{allocated_objects} freed_objects=#{freed_objects} stack_table=#{stack_table.inspect}>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.track(&block)
|
|
29
|
+
tracker = new
|
|
30
|
+
tracker.track(&block)
|
|
31
|
+
tracker
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def track
|
|
35
|
+
collect
|
|
36
|
+
yield self
|
|
37
|
+
ensure
|
|
38
|
+
lock
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def stack(obj)
|
|
42
|
+
idx = stack_idx(obj)
|
|
43
|
+
return nil unless idx
|
|
44
|
+
stack_table.stack(idx)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vernier
|
|
4
|
+
class MemoryLeakDetector
|
|
5
|
+
def self.start_thread(...)
|
|
6
|
+
detector = new(...)
|
|
7
|
+
detector.start_thread
|
|
8
|
+
detector
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(idle_time: 0, collect_time:, drain_time: 0, **collector_options)
|
|
12
|
+
@idle_time = idle_time
|
|
13
|
+
@collect_time = collect_time
|
|
14
|
+
@drain_time = drain_time
|
|
15
|
+
@collector_options = collector_options
|
|
16
|
+
@thread = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start_thread
|
|
20
|
+
@thread = Thread.new do
|
|
21
|
+
sleep @idle_time
|
|
22
|
+
|
|
23
|
+
collector = Collector.new(:retained, @collector_options)
|
|
24
|
+
collector.start
|
|
25
|
+
|
|
26
|
+
sleep @collect_time
|
|
27
|
+
|
|
28
|
+
collector.drain
|
|
29
|
+
|
|
30
|
+
sleep @drain_time
|
|
31
|
+
|
|
32
|
+
collector.stop
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def result
|
|
37
|
+
@thread&.value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -8,35 +8,59 @@ require_relative "filename_filter"
|
|
|
8
8
|
module Vernier
|
|
9
9
|
module Output
|
|
10
10
|
# https://profiler.firefox.com/
|
|
11
|
-
# https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.
|
|
11
|
+
# https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.ts
|
|
12
12
|
class Firefox
|
|
13
13
|
class Categorizer
|
|
14
|
+
RAILS_COMPONENTS = %w[ activesupport activemodel activerecord actionview
|
|
15
|
+
actionpack activejob actionmailer actioncable
|
|
16
|
+
activestorage actionmailbox actiontext railties ]
|
|
17
|
+
|
|
18
|
+
AVAILABLE_COLORS = %w[ transparent purple green orange yellow lightblue
|
|
19
|
+
blue brown magenta red lightred darkgrey grey ]
|
|
20
|
+
|
|
21
|
+
ORDERED_CATEGORIES = %w[ Kernel Rails gem Ruby ] # This is in the order of preference
|
|
22
|
+
|
|
14
23
|
attr_reader :categories
|
|
24
|
+
|
|
15
25
|
def initialize
|
|
16
26
|
@categories = []
|
|
17
27
|
@categories_by_name = {}
|
|
18
28
|
|
|
19
|
-
add_category(name: "
|
|
20
|
-
rails_components = %w[ activesupport activemodel activerecord
|
|
21
|
-
actionview actionpack activejob actionmailer actioncable
|
|
22
|
-
activestorage actionmailbox actiontext railties ]
|
|
29
|
+
add_category(name: "Kernel", color: "magenta") do |c|
|
|
23
30
|
c.add_subcategory(
|
|
24
|
-
name: "
|
|
25
|
-
matcher:
|
|
31
|
+
name: "Kernel",
|
|
32
|
+
matcher: starts_with("<internal")
|
|
26
33
|
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
add_category(name: "gem", color: "lightblue") do |c|
|
|
27
37
|
c.add_subcategory(
|
|
28
38
|
name: "gem",
|
|
29
39
|
matcher: starts_with(*Gem.path)
|
|
30
40
|
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
add_category(name: "Rails", color: "red") do |c|
|
|
44
|
+
RAILS_COMPONENTS.each do |subcategory|
|
|
45
|
+
c.add_subcategory(
|
|
46
|
+
name: subcategory,
|
|
47
|
+
matcher: gem_path(subcategory)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
add_category(name: "Ruby", color: "purple") do |c|
|
|
31
53
|
c.add_subcategory(
|
|
32
54
|
name: "stdlib",
|
|
33
55
|
matcher: starts_with(RbConfig::CONFIG["rubylibdir"])
|
|
34
56
|
)
|
|
35
57
|
end
|
|
58
|
+
|
|
36
59
|
add_category(name: "Idle", color: "transparent")
|
|
37
60
|
add_category(name: "Stalled", color: "transparent")
|
|
38
61
|
|
|
39
62
|
add_category(name: "GC", color: "red")
|
|
63
|
+
|
|
40
64
|
add_category(name: "cfunc", color: "yellow", matcher: "<cfunc>")
|
|
41
65
|
|
|
42
66
|
add_category(name: "Thread", color: "grey")
|
|
@@ -69,7 +93,10 @@ module Vernier
|
|
|
69
93
|
|
|
70
94
|
class Category
|
|
71
95
|
attr_reader :idx, :name, :color, :matcher, :subcategories
|
|
96
|
+
|
|
72
97
|
def initialize(idx, name:, color:, matcher: nil)
|
|
98
|
+
raise ArgumentError, "invalid color: #{color}" if color && AVAILABLE_COLORS.none?(color)
|
|
99
|
+
|
|
73
100
|
@idx = idx
|
|
74
101
|
@name = name
|
|
75
102
|
@color = color
|
|
@@ -315,19 +342,13 @@ module Vernier
|
|
|
315
342
|
func_implementations[func_idx]
|
|
316
343
|
end
|
|
317
344
|
|
|
318
|
-
cfunc_category = @categorizer.get_category("cfunc")
|
|
319
|
-
ruby_category = @categorizer.get_category("Ruby")
|
|
320
345
|
func_categories, func_subcategories = [], []
|
|
321
346
|
filenames.each do |filename|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
else
|
|
326
|
-
func_categories << ruby_category
|
|
327
|
-
subcategory = ruby_category.subcategories.detect {|c| c.matches?(filename) }&.idx || 0
|
|
328
|
-
func_subcategories << subcategory
|
|
329
|
-
end
|
|
347
|
+
category, subcategory = categorize_filename(filename)
|
|
348
|
+
func_categories << category
|
|
349
|
+
func_subcategories << subcategory
|
|
330
350
|
end
|
|
351
|
+
|
|
331
352
|
@frame_categories = @stack_table_hash[:frame_table].fetch(:func).map do |func_idx|
|
|
332
353
|
func_categories[func_idx]
|
|
333
354
|
end
|
|
@@ -336,6 +357,32 @@ module Vernier
|
|
|
336
357
|
end
|
|
337
358
|
end
|
|
338
359
|
|
|
360
|
+
def categorize_filename(filename)
|
|
361
|
+
return cfunc_category_and_subcategory if filename == "<cfunc>"
|
|
362
|
+
|
|
363
|
+
category, subcategory = find_category_and_subcategory(filename, Categorizer::ORDERED_CATEGORIES)
|
|
364
|
+
return category, subcategory if subcategory
|
|
365
|
+
|
|
366
|
+
ruby_category_and_subcategory
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def cfunc_category_and_subcategory
|
|
370
|
+
[@categorizer.get_category("cfunc"), 0]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def ruby_category_and_subcategory
|
|
374
|
+
[@categorizer.get_category("Ruby"), 0]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def find_category_and_subcategory(filename, categories)
|
|
378
|
+
categories.each do |category_name|
|
|
379
|
+
category = @categorizer.get_category(category_name)
|
|
380
|
+
subcategory = category.subcategories.detect {|c| c.matches?(filename) }&.idx
|
|
381
|
+
return category, subcategory if subcategory
|
|
382
|
+
end
|
|
383
|
+
[nil, nil]
|
|
384
|
+
end
|
|
385
|
+
|
|
339
386
|
def filter_filenames(filenames)
|
|
340
387
|
filter = FilenameFilter.new
|
|
341
388
|
filenames.map do |filename|
|
data/lib/vernier/output/top.rb
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
module Vernier
|
|
4
4
|
module Output
|
|
5
5
|
class Top
|
|
6
|
-
def initialize(profile)
|
|
6
|
+
def initialize(profile, row_limit)
|
|
7
7
|
@profile = profile
|
|
8
|
+
@row_limit = row_limit
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
class Table
|
|
11
|
-
def initialize(header)
|
|
12
|
+
def initialize(header, row_limit)
|
|
12
13
|
@header = header
|
|
13
14
|
@rows = []
|
|
15
|
+
@row_limit = row_limit
|
|
14
16
|
yield self
|
|
15
17
|
end
|
|
16
18
|
|
|
@@ -24,7 +26,7 @@ module Vernier
|
|
|
24
26
|
row_separator,
|
|
25
27
|
format_row(@header),
|
|
26
28
|
row_separator
|
|
27
|
-
] + @rows.map do |row|
|
|
29
|
+
] + @rows.first(@row_limit).map do |row|
|
|
28
30
|
format_row(row)
|
|
29
31
|
end + [row_separator]
|
|
30
32
|
).join("\n")
|
|
@@ -70,7 +72,7 @@ module Vernier
|
|
|
70
72
|
top_by_self[name] += weight
|
|
71
73
|
end
|
|
72
74
|
|
|
73
|
-
Table.new %w[Samples % name] do |t|
|
|
75
|
+
Table.new %w[Samples % name], @row_limit do |t|
|
|
74
76
|
top_by_self.sort_by(&:last).reverse.each do |frame, samples|
|
|
75
77
|
pct = 100.0 * samples / total
|
|
76
78
|
t << [samples.to_s, pct.round(1).to_s, frame]
|
data/lib/vernier/result.rb
CHANGED
|
@@ -1,26 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Vernier
|
|
2
4
|
class Result
|
|
3
5
|
attr_accessor :stack_table
|
|
4
6
|
alias _stack_table stack_table
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
attr_accessor :hooks
|
|
8
|
+
attr_accessor :hooks, :pid, :end_time
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
attr_accessor :threads
|
|
12
|
-
attr_accessor :meta
|
|
13
|
-
attr_accessor :mode
|
|
10
|
+
attr_reader :meta, :threads, :gc_markers
|
|
14
11
|
|
|
15
12
|
def main_thread
|
|
16
13
|
threads.values.detect {|x| x[:is_main] }
|
|
17
14
|
end
|
|
18
15
|
|
|
19
|
-
# TODO: remove these
|
|
20
|
-
def weights; threads.values.flat_map { _1[:weights] }; end
|
|
21
|
-
def samples; threads.values.flat_map { _1[:samples] }; end
|
|
22
|
-
def sample_categories; threads.values.flat_map { _1[:sample_categories] }; end
|
|
23
|
-
|
|
24
16
|
# Realtime in nanoseconds since the unix epoch
|
|
25
17
|
def started_at
|
|
26
18
|
started_at_mono_ns = meta[:started_at]
|
|
@@ -41,10 +33,17 @@ module Vernier
|
|
|
41
33
|
def write(out:, format: "firefox")
|
|
42
34
|
case format
|
|
43
35
|
when "cpuprofile"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
if out.respond_to?(:write)
|
|
37
|
+
out.write(to_cpuprofile)
|
|
38
|
+
else
|
|
39
|
+
File.binwrite(out, to_cpuprofile)
|
|
40
|
+
end
|
|
41
|
+
when "firefox", nil
|
|
42
|
+
if out.respond_to?(:write)
|
|
43
|
+
out.write(to_firefox)
|
|
44
|
+
else
|
|
45
|
+
File.binwrite(out, to_firefox(gzip: out.end_with?(".gz")))
|
|
46
|
+
end
|
|
48
47
|
else
|
|
49
48
|
raise ArgumentError, "unknown format: #{format}"
|
|
50
49
|
end
|
|
@@ -55,15 +54,15 @@ module Vernier
|
|
|
55
54
|
end
|
|
56
55
|
|
|
57
56
|
def inspect
|
|
58
|
-
"#<#{self.class} #{elapsed_seconds} seconds, #{threads.count} threads, #{
|
|
57
|
+
"#<#{self.class} #{elapsed_seconds rescue "?"} seconds, #{threads.count} threads, #{total_samples} samples, #{total_unique_samples} unique>"
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
def each_sample
|
|
62
61
|
return enum_for(__method__) unless block_given?
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
threads.values.each do |thread|
|
|
63
|
+
thread[:samples].zip(thread[:weights]) do |stack_idx, weight|
|
|
64
|
+
yield stack(stack_idx), weight
|
|
65
|
+
end
|
|
67
66
|
end
|
|
68
67
|
end
|
|
69
68
|
|
|
@@ -71,8 +70,24 @@ module Vernier
|
|
|
71
70
|
stack_table.stack(idx)
|
|
72
71
|
end
|
|
73
72
|
|
|
73
|
+
def total_weights
|
|
74
|
+
threads.values.sum { _1[:weights].sum }
|
|
75
|
+
end
|
|
76
|
+
|
|
74
77
|
def total_bytes
|
|
75
|
-
|
|
78
|
+
unless meta[:mode] == :retained
|
|
79
|
+
raise NotImplementedError, "total_bytes is only implemented for retained mode"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
total_weights
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def total_samples
|
|
86
|
+
threads.values.sum { _1[:samples].count }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def total_unique_samples
|
|
90
|
+
threads.values.flat_map { _1[:samples] }.uniq.count
|
|
76
91
|
end
|
|
77
92
|
end
|
|
78
93
|
end
|
|
@@ -23,15 +23,21 @@ module Vernier
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def backtrace(stack_idx)
|
|
26
|
-
|
|
26
|
+
last_filename = nil
|
|
27
|
+
last_lineno = nil
|
|
28
|
+
full_stack(stack_idx).reverse.map do |stack_idx|
|
|
27
29
|
frame_idx = stack_frame_idx(stack_idx)
|
|
28
30
|
func_idx = frame_func_idx(frame_idx)
|
|
29
31
|
line = frame_line_no(frame_idx)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
line = last_lineno if line == 0
|
|
33
|
+
last_lineno = line
|
|
34
|
+
name = func_name(func_idx)
|
|
35
|
+
filename = func_path(func_idx)
|
|
36
|
+
filename = last_filename if filename.empty?
|
|
37
|
+
last_filename = filename
|
|
32
38
|
|
|
33
39
|
"#{filename}:#{line}:in '#{name}'"
|
|
34
|
-
end
|
|
40
|
+
end.reverse
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
def full_stack(stack_idx)
|
|
@@ -83,9 +89,14 @@ module Vernier
|
|
|
83
89
|
def line
|
|
84
90
|
stack_table.frame_line_no(idx)
|
|
85
91
|
end
|
|
92
|
+
alias lineno line
|
|
86
93
|
|
|
87
94
|
def to_s
|
|
88
|
-
|
|
95
|
+
if (line = self.line) == 0
|
|
96
|
+
func.to_s
|
|
97
|
+
else
|
|
98
|
+
"#{func}:#{line}"
|
|
99
|
+
end
|
|
89
100
|
end
|
|
90
101
|
end
|
|
91
102
|
|
|
@@ -102,14 +113,16 @@ module Vernier
|
|
|
102
113
|
end
|
|
103
114
|
alias each_frame each
|
|
104
115
|
|
|
105
|
-
def [](
|
|
106
|
-
raise RangeError if
|
|
116
|
+
def [](offset)
|
|
117
|
+
raise RangeError if offset < 0
|
|
107
118
|
stack_idx = idx
|
|
108
|
-
while
|
|
119
|
+
while stack_idx && offset > 0
|
|
109
120
|
stack_idx = stack_table.stack_parent_idx(stack_idx)
|
|
110
|
-
|
|
121
|
+
offset -= 1
|
|
122
|
+
end
|
|
123
|
+
if stack_idx && offset == 0
|
|
124
|
+
Frame.new(stack_table, stack_table.stack_frame_idx(stack_idx))
|
|
111
125
|
end
|
|
112
|
-
Frame.new(stack_table, stack_table.stack_frame_idx(stack_idx))
|
|
113
126
|
end
|
|
114
127
|
|
|
115
128
|
def leaf_frame_idx
|
|
@@ -134,6 +147,7 @@ module Vernier
|
|
|
134
147
|
end
|
|
135
148
|
|
|
136
149
|
def stack(idx)
|
|
150
|
+
raise ArgumentError, "invalid index" unless idx
|
|
137
151
|
Stack.new(self, idx)
|
|
138
152
|
end
|
|
139
153
|
end
|
data/lib/vernier/version.rb
CHANGED
data/lib/vernier.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "vernier/version"
|
|
4
4
|
require_relative "vernier/collector"
|
|
5
5
|
require_relative "vernier/stack_table"
|
|
6
|
+
require_relative "vernier/heap_tracker"
|
|
7
|
+
require_relative "vernier/memory_leak_detector"
|
|
6
8
|
require_relative "vernier/parsed_profile"
|
|
7
9
|
require_relative "vernier/result"
|
|
8
10
|
require_relative "vernier/hooks"
|
|
@@ -63,10 +65,4 @@ module Vernier
|
|
|
63
65
|
def self.trace_retained(**profile_options, &block)
|
|
64
66
|
profile(**profile_options.merge(mode: :retained), &block)
|
|
65
67
|
end
|
|
66
|
-
|
|
67
|
-
class Collector
|
|
68
|
-
def self.new(mode, options = {})
|
|
69
|
-
_new(mode, options)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
68
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vernier
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- John Hawthorn
|
|
@@ -70,6 +70,7 @@ files:
|
|
|
70
70
|
- bin/console
|
|
71
71
|
- bin/setup
|
|
72
72
|
- bin/vernier
|
|
73
|
+
- examples/custom_hook.rb
|
|
73
74
|
- examples/fiber_stalls.rb
|
|
74
75
|
- examples/gvl_sleep.rb
|
|
75
76
|
- examples/measure_overhead.rb
|
|
@@ -79,20 +80,25 @@ files:
|
|
|
79
80
|
- examples/threaded_http_requests.rb
|
|
80
81
|
- exe/vernier
|
|
81
82
|
- ext/vernier/extconf.rb
|
|
83
|
+
- ext/vernier/heap_tracker.cc
|
|
82
84
|
- ext/vernier/memory.cc
|
|
83
85
|
- ext/vernier/periodic_thread.hh
|
|
84
86
|
- ext/vernier/ruby_type_names.h
|
|
85
87
|
- ext/vernier/signal_safe_semaphore.hh
|
|
88
|
+
- ext/vernier/stack_table.cc
|
|
89
|
+
- ext/vernier/stack_table.hh
|
|
86
90
|
- ext/vernier/timestamp.hh
|
|
87
91
|
- ext/vernier/vernier.cc
|
|
88
92
|
- ext/vernier/vernier.hh
|
|
89
93
|
- lib/vernier.rb
|
|
90
94
|
- lib/vernier/autorun.rb
|
|
91
95
|
- lib/vernier/collector.rb
|
|
96
|
+
- lib/vernier/heap_tracker.rb
|
|
92
97
|
- lib/vernier/hooks.rb
|
|
93
98
|
- lib/vernier/hooks/active_support.rb
|
|
94
99
|
- lib/vernier/hooks/memory_usage.rb
|
|
95
100
|
- lib/vernier/marker.rb
|
|
101
|
+
- lib/vernier/memory_leak_detector.rb
|
|
96
102
|
- lib/vernier/middleware.rb
|
|
97
103
|
- lib/vernier/output/cpuprofile.rb
|
|
98
104
|
- lib/vernier/output/file_listing.rb
|
|
@@ -127,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
127
133
|
- !ruby/object:Gem::Version
|
|
128
134
|
version: '0'
|
|
129
135
|
requirements: []
|
|
130
|
-
rubygems_version: 3.6.
|
|
136
|
+
rubygems_version: 3.6.9
|
|
131
137
|
specification_version: 4
|
|
132
138
|
summary: A next generation CRuby profiler
|
|
133
139
|
test_files: []
|