vernier 1.10.1 → 1.12.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/Gemfile +2 -0
- data/README.md +19 -10
- data/exe/vernier +3 -0
- data/ext/vernier/extconf.rb +4 -3
- data/ext/vernier/heap_tracker.cc +10 -3
- data/ext/vernier/memory.cc +19 -5
- data/ext/vernier/periodic_thread.hh +17 -2
- data/ext/vernier/signal_safe_semaphore.hh +3 -1
- data/ext/vernier/stack_table.cc +18 -6
- data/ext/vernier/stack_table.hh +24 -26
- data/ext/vernier/vernier.cc +127 -117
- data/ext/vernier/vernier.hh +12 -0
- data/lib/vernier/autorun.rb +2 -1
- data/lib/vernier/collector.rb +5 -0
- data/lib/vernier/hooks/active_support.rb +714 -32
- data/lib/vernier/output/cpuprofile.rb +6 -0
- data/lib/vernier/output/file_listing.rb +56 -27
- data/lib/vernier/output/firefox.rb +21 -19
- data/lib/vernier/output/top.rb +8 -4
- data/lib/vernier/output_helpers.rb +33 -0
- data/lib/vernier/thread_names.rb +6 -0
- data/lib/vernier/version.rb +1 -1
- data/vernier.gemspec +2 -2
- metadata +5 -4
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
|
|
3
|
+
require_relative "../output_helpers"
|
|
4
|
+
|
|
3
5
|
module Vernier
|
|
4
6
|
module Output
|
|
5
7
|
class Cpuprofile
|
|
8
|
+
include OutputHelpers
|
|
9
|
+
|
|
6
10
|
def initialize(profile)
|
|
7
11
|
@profile = profile
|
|
8
12
|
end
|
|
@@ -98,7 +102,9 @@ module Vernier
|
|
|
98
102
|
line = stack_table.frame_line_no(frame_idx) - 1
|
|
99
103
|
|
|
100
104
|
func_name = stack_table.func_name(func_idx)
|
|
105
|
+
func_name = sanitize_string(func_name) if func_name
|
|
101
106
|
filename = stack_table.func_filename(func_idx)
|
|
107
|
+
filename = sanitize_string(filename) if filename
|
|
102
108
|
|
|
103
109
|
node_id = nodes.length
|
|
104
110
|
node = {
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "filename_filter"
|
|
4
|
+
require_relative "../output_helpers"
|
|
4
5
|
require "cgi/escape"
|
|
5
6
|
|
|
6
7
|
module Vernier
|
|
7
8
|
module Output
|
|
8
9
|
class FileListing
|
|
10
|
+
include OutputHelpers
|
|
11
|
+
|
|
9
12
|
class SamplesByLocation
|
|
10
13
|
attr_accessor :self, :total
|
|
11
14
|
def initialize
|
|
12
15
|
@self = @total = 0
|
|
13
16
|
end
|
|
14
|
-
|
|
15
|
-
def +(other)
|
|
16
|
-
ret = SamplesByLocation.new
|
|
17
|
-
ret.self = @self + other.self
|
|
18
|
-
ret.total = @total + other.total
|
|
19
|
-
ret
|
|
20
|
-
end
|
|
21
17
|
end
|
|
22
18
|
|
|
23
19
|
def initialize(profile)
|
|
@@ -25,6 +21,8 @@ module Vernier
|
|
|
25
21
|
end
|
|
26
22
|
|
|
27
23
|
def samples_by_file
|
|
24
|
+
return @samples_by_file if defined?(@samples_by_file)
|
|
25
|
+
|
|
28
26
|
thread = @profile.main_thread
|
|
29
27
|
if Hash === thread
|
|
30
28
|
# live profile
|
|
@@ -38,22 +36,9 @@ module Vernier
|
|
|
38
36
|
weights = thread[:weights]
|
|
39
37
|
samples = thread[:samples]
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
h[k] = SamplesByLocation.new
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
samples.zip(weights).each do |stack_idx, weight|
|
|
46
|
-
# self time
|
|
47
|
-
top_frame_index = stack_table.stack_frame_idx(stack_idx)
|
|
48
|
-
self_samples_by_frame[top_frame_index].self += weight
|
|
39
|
+
stack_weights = collapse_stack_weights(samples, weights)
|
|
49
40
|
|
|
50
|
-
|
|
51
|
-
while stack_idx
|
|
52
|
-
frame_idx = stack_table.stack_frame_idx(stack_idx)
|
|
53
|
-
self_samples_by_frame[frame_idx].total += weight
|
|
54
|
-
stack_idx = stack_table.stack_parent_idx(stack_idx)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
41
|
+
self_by_frame, total_by_frame = frame_weights(stack_table, stack_weights)
|
|
57
42
|
|
|
58
43
|
samples_by_file = Hash.new do |h, k|
|
|
59
44
|
h[k] = Hash.new do |h2, k2|
|
|
@@ -61,19 +46,64 @@ module Vernier
|
|
|
61
46
|
end
|
|
62
47
|
end
|
|
63
48
|
|
|
64
|
-
|
|
49
|
+
self_by_frame.each_with_index do |self_weight, frame|
|
|
50
|
+
total_weight = total_by_frame[frame]
|
|
51
|
+
next if self_weight == 0 && total_weight == 0
|
|
52
|
+
|
|
65
53
|
line = stack_table.frame_line_no(frame)
|
|
66
54
|
func_index = stack_table.frame_func_idx(frame)
|
|
67
55
|
filename = stack_table.func_filename(func_index)
|
|
68
56
|
|
|
69
|
-
samples_by_file[filename][line]
|
|
57
|
+
location = samples_by_file[filename][line]
|
|
58
|
+
location.self += self_weight
|
|
59
|
+
location.total += total_weight
|
|
70
60
|
end
|
|
71
61
|
|
|
72
|
-
samples_by_file.transform_keys! do |filename|
|
|
62
|
+
@samples_by_file = samples_by_file.transform_keys! do |filename|
|
|
73
63
|
filename_filter.call(filename)
|
|
74
64
|
end
|
|
75
65
|
end
|
|
76
66
|
|
|
67
|
+
# Returns [self_weight, total_weight] arrays indexed by frame, from a
|
|
68
|
+
# hash of per-stack weights.
|
|
69
|
+
#
|
|
70
|
+
# The stack table is a prefix tree whose parent is always interned
|
|
71
|
+
# before its children (parent idx < child idx), so total weights are
|
|
72
|
+
# accumulated bottom-up in a single reverse pass.
|
|
73
|
+
private def frame_weights(stack_table, stack_weights)
|
|
74
|
+
# Scatter the sparse weights into an array indexed by stack.
|
|
75
|
+
stack_totals = Array.new(stack_table.stack_count, 0)
|
|
76
|
+
stack_weights.each do |stack_idx, weight|
|
|
77
|
+
stack_totals[stack_idx] = weight
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Accumulate each stack's total into its parent, children first,
|
|
81
|
+
# so a single descending pass completes every subtree's total.
|
|
82
|
+
(stack_totals.length - 1).downto(0) do |stack_idx|
|
|
83
|
+
parent_idx = stack_table.stack_parent_idx(stack_idx)
|
|
84
|
+
next unless parent_idx
|
|
85
|
+
|
|
86
|
+
if parent_idx >= stack_idx
|
|
87
|
+
raise "Invalid profile: stack table is not parent-before-child ordered"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
stack_totals[parent_idx] += stack_totals[stack_idx]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
self_by_frame = Array.new(stack_table.frame_count, 0)
|
|
94
|
+
total_by_frame = Array.new(stack_table.frame_count, 0)
|
|
95
|
+
stack_weights.each do |stack_idx, weight|
|
|
96
|
+
self_by_frame[stack_table.stack_frame_idx(stack_idx)] += weight
|
|
97
|
+
end
|
|
98
|
+
stack_totals.each_with_index do |total, stack_idx|
|
|
99
|
+
next if total == 0
|
|
100
|
+
|
|
101
|
+
total_by_frame[stack_table.stack_frame_idx(stack_idx)] += total
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
[self_by_frame, total_by_frame]
|
|
105
|
+
end
|
|
106
|
+
|
|
77
107
|
def output(template: nil)
|
|
78
108
|
output = +""
|
|
79
109
|
|
|
@@ -98,8 +128,7 @@ module Vernier
|
|
|
98
128
|
end
|
|
99
129
|
|
|
100
130
|
def total
|
|
101
|
-
|
|
102
|
-
thread[:weights].sum
|
|
131
|
+
@total ||= @profile.main_thread[:weights].sum
|
|
103
132
|
end
|
|
104
133
|
|
|
105
134
|
def format_file(output, filename, all_samples, total:)
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "rbconfig"
|
|
5
5
|
|
|
6
6
|
require_relative "filename_filter"
|
|
7
|
+
require_relative "../output_helpers"
|
|
7
8
|
|
|
8
9
|
module Vernier
|
|
9
10
|
module Output
|
|
@@ -120,7 +121,7 @@ module Vernier
|
|
|
120
121
|
end
|
|
121
122
|
|
|
122
123
|
def output(gzip: false)
|
|
123
|
-
result = ::JSON.generate(data)
|
|
124
|
+
result = ::JSON.generate(data(profile.meta[:hide_anonymous_threads]))
|
|
124
125
|
if gzip
|
|
125
126
|
require "zlib"
|
|
126
127
|
result = Zlib.gzip(result)
|
|
@@ -132,7 +133,7 @@ module Vernier
|
|
|
132
133
|
|
|
133
134
|
attr_reader :profile
|
|
134
135
|
|
|
135
|
-
def data
|
|
136
|
+
def data(hide_anonymous_threads)
|
|
136
137
|
#markers_by_thread = profile.markers.group_by { |marker| marker[0] }
|
|
137
138
|
|
|
138
139
|
threads = profile.threads.map do |ruby_thread_id, thread_info|
|
|
@@ -146,6 +147,12 @@ module Vernier
|
|
|
146
147
|
)
|
|
147
148
|
end
|
|
148
149
|
|
|
150
|
+
initially_visible_threads = if hide_anonymous_threads
|
|
151
|
+
threads.each_index.reject { |i| threads[i].anonymous? && !threads[i].is_main && !threads[i].is_start }.to_a
|
|
152
|
+
else
|
|
153
|
+
threads.each_index.to_a
|
|
154
|
+
end
|
|
155
|
+
|
|
149
156
|
{
|
|
150
157
|
meta: {
|
|
151
158
|
interval: 1, # FIXME: memory vs wall
|
|
@@ -171,7 +178,7 @@ module Vernier
|
|
|
171
178
|
}
|
|
172
179
|
end,
|
|
173
180
|
sourceCodeIsNotOnSearchfox: true,
|
|
174
|
-
initialVisibleThreads:
|
|
181
|
+
initialVisibleThreads: initially_visible_threads,
|
|
175
182
|
initialSelectedThreads: Array(threads.find_index(&:is_start)),
|
|
176
183
|
vernierUserMetadata: profile.meta[:user_metadata],
|
|
177
184
|
extra: [
|
|
@@ -267,20 +274,24 @@ module Vernier
|
|
|
267
274
|
end
|
|
268
275
|
|
|
269
276
|
class Thread
|
|
277
|
+
include OutputHelpers
|
|
278
|
+
|
|
270
279
|
SAMPLE_CATEGORY_NAMES = {
|
|
271
280
|
1 => "Idle",
|
|
272
281
|
2 => "Stalled"
|
|
273
282
|
}.freeze
|
|
274
283
|
|
|
275
|
-
attr_reader :profile, :is_start
|
|
284
|
+
attr_reader :profile, :is_start, :anonymous, :is_main
|
|
285
|
+
alias :anonymous? :anonymous
|
|
276
286
|
|
|
277
|
-
def initialize(ruby_thread_id, profile, categorizer, name:, tid:, samples:, weights:, timestamps: nil, sample_categories: nil, markers:, started_at:, stopped_at: nil, allocations: nil, is_main: nil, is_start: nil)
|
|
287
|
+
def initialize(ruby_thread_id, profile, categorizer, name:, tid:, samples:, weights:, timestamps: nil, sample_categories: nil, markers:, started_at:, anonymous:, stopped_at: nil, allocations: nil, is_main: nil, is_start: nil)
|
|
278
288
|
@ruby_thread_id = ruby_thread_id
|
|
279
289
|
@profile = profile
|
|
280
290
|
@categorizer = categorizer
|
|
281
291
|
@tid = tid
|
|
282
292
|
@name = name
|
|
283
293
|
@is_main = is_main
|
|
294
|
+
@anonymous = anonymous
|
|
284
295
|
if is_main.nil?
|
|
285
296
|
@is_main = @ruby_thread_id == ::Thread.main.object_id
|
|
286
297
|
end
|
|
@@ -438,7 +449,6 @@ module Vernier
|
|
|
438
449
|
funcTable: func_table,
|
|
439
450
|
nativeSymbols: {},
|
|
440
451
|
samples: samples_table,
|
|
441
|
-
jsAllocations: allocations_table,
|
|
442
452
|
stackTable: stack_table,
|
|
443
453
|
resourceTable: {
|
|
444
454
|
length: 0,
|
|
@@ -449,7 +459,10 @@ module Vernier
|
|
|
449
459
|
},
|
|
450
460
|
markers: markers_table,
|
|
451
461
|
stringArray: string_table
|
|
452
|
-
}.
|
|
462
|
+
}.tap do |thread|
|
|
463
|
+
allocations = allocations_table
|
|
464
|
+
thread[:jsAllocations] = allocations if allocations
|
|
465
|
+
end
|
|
453
466
|
end
|
|
454
467
|
|
|
455
468
|
def markers_table
|
|
@@ -638,18 +651,7 @@ module Vernier
|
|
|
638
651
|
|
|
639
652
|
def string_table
|
|
640
653
|
@strings.keys.map do |string|
|
|
641
|
-
|
|
642
|
-
string
|
|
643
|
-
elsif string.encoding == Encoding::UTF_8
|
|
644
|
-
if string.valid_encoding?
|
|
645
|
-
string
|
|
646
|
-
else
|
|
647
|
-
string.scrub
|
|
648
|
-
end
|
|
649
|
-
else
|
|
650
|
-
# TODO: We might want to guess UTF-8 and escape the binary more explicitly
|
|
651
|
-
string.dup.force_encoding("UTF-8").scrub
|
|
652
|
-
end
|
|
654
|
+
sanitize_string(string)
|
|
653
655
|
end
|
|
654
656
|
end
|
|
655
657
|
|
data/lib/vernier/output/top.rb
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../output_helpers"
|
|
4
|
+
|
|
3
5
|
module Vernier
|
|
4
6
|
module Output
|
|
5
7
|
class Top
|
|
8
|
+
include OutputHelpers
|
|
9
|
+
|
|
6
10
|
def initialize(profile, row_limit)
|
|
7
11
|
@profile = profile
|
|
8
12
|
@row_limit = row_limit
|
|
@@ -57,10 +61,10 @@ module Vernier
|
|
|
57
61
|
@profile._stack_table
|
|
58
62
|
end
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
thread[:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
samples = thread[:samples]
|
|
65
|
+
weights = thread[:weights]
|
|
66
|
+
|
|
67
|
+
stack_weights = collapse_stack_weights(samples, weights)
|
|
64
68
|
|
|
65
69
|
total = stack_weights.values.sum
|
|
66
70
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vernier
|
|
4
|
+
module OutputHelpers
|
|
5
|
+
# Collapses parallel samples/weights arrays into a hash of total weight
|
|
6
|
+
# per stack index. Retained-mode profiles have one sample per object and
|
|
7
|
+
# many samples share the same stack, so collapsing first keeps later
|
|
8
|
+
# aggregation proportional to the number of unique stacks.
|
|
9
|
+
def collapse_stack_weights(samples, weights)
|
|
10
|
+
stack_weights = Hash.new(0)
|
|
11
|
+
samples.each_with_index do |stack_idx, idx|
|
|
12
|
+
stack_weights[stack_idx] += weights[idx]
|
|
13
|
+
end
|
|
14
|
+
stack_weights
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Returns a string safe to embed in JSON output: valid UTF-8 with any
|
|
18
|
+
# invalid bytes replaced. Method names and paths can carry arbitrary
|
|
19
|
+
# encodings (e.g. BINARY or Shift_JIS source), which would otherwise
|
|
20
|
+
# raise JSON::GeneratorError.
|
|
21
|
+
def sanitize_string(string)
|
|
22
|
+
if string.ascii_only?
|
|
23
|
+
string
|
|
24
|
+
elsif string.encoding == Encoding::UTF_8
|
|
25
|
+
string.valid_encoding? ? string : string.scrub
|
|
26
|
+
else
|
|
27
|
+
# We don't know the real encoding; interpret the bytes as UTF-8
|
|
28
|
+
# and replace anything invalid.
|
|
29
|
+
string.dup.force_encoding(Encoding::UTF_8).scrub
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/vernier/thread_names.rb
CHANGED
|
@@ -3,6 +3,7 @@ module Vernier
|
|
|
3
3
|
class ThreadNames
|
|
4
4
|
def initialize
|
|
5
5
|
@names = {}
|
|
6
|
+
@anonymous = {}
|
|
6
7
|
@tp = TracePoint.new(:thread_end) do |e|
|
|
7
8
|
collect_thread(e.self)
|
|
8
9
|
end
|
|
@@ -13,6 +14,10 @@ module Vernier
|
|
|
13
14
|
@names[object_id] || "thread obj_id:#{object_id}"
|
|
14
15
|
end
|
|
15
16
|
|
|
17
|
+
def anonymous?(object_id)
|
|
18
|
+
@anonymous[object_id]
|
|
19
|
+
end
|
|
20
|
+
|
|
16
21
|
def finish
|
|
17
22
|
collect_running
|
|
18
23
|
@tp.disable
|
|
@@ -27,6 +32,7 @@ module Vernier
|
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def collect_thread(th)
|
|
35
|
+
@anonymous[th.object_id] = !th.name
|
|
30
36
|
@names[th.object_id] = pretty_name(th)
|
|
31
37
|
end
|
|
32
38
|
|
data/lib/vernier/version.rb
CHANGED
data/vernier.gemspec
CHANGED
|
@@ -9,12 +9,12 @@ Gem::Specification.new do |spec|
|
|
|
9
9
|
spec.email = ["john@hawthorn.email"]
|
|
10
10
|
|
|
11
11
|
spec.summary = "A next generation CRuby profiler"
|
|
12
|
-
spec.description = "Next-generation Ruby 3.
|
|
12
|
+
spec.description = "Next-generation Ruby 3.3+ sampling profiler. Tracks multiple threads, GVL activity, GC pauses, idle time, and more."
|
|
13
13
|
spec.homepage = "https://github.com/jhawthorn/vernier"
|
|
14
14
|
spec.license = "MIT"
|
|
15
15
|
|
|
16
16
|
unless ENV["IGNORE_REQUIRED_RUBY_VERSION"]
|
|
17
|
-
spec.required_ruby_version = ">= 3.
|
|
17
|
+
spec.required_ruby_version = ">= 3.3.0"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
spec.metadata["homepage_uri"] = spec.homepage
|
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.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- John Hawthorn
|
|
@@ -51,7 +51,7 @@ dependencies:
|
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
|
-
description: Next-generation Ruby 3.
|
|
54
|
+
description: Next-generation Ruby 3.3+ sampling profiler. Tracks multiple threads,
|
|
55
55
|
GVL activity, GC pauses, idle time, and more.
|
|
56
56
|
email:
|
|
57
57
|
- john@hawthorn.email
|
|
@@ -106,6 +106,7 @@ files:
|
|
|
106
106
|
- lib/vernier/output/firefox.rb
|
|
107
107
|
- lib/vernier/output/markdown.rb
|
|
108
108
|
- lib/vernier/output/top.rb
|
|
109
|
+
- lib/vernier/output_helpers.rb
|
|
109
110
|
- lib/vernier/parsed_profile.rb
|
|
110
111
|
- lib/vernier/result.rb
|
|
111
112
|
- lib/vernier/stack_table.rb
|
|
@@ -127,14 +128,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
127
128
|
requirements:
|
|
128
129
|
- - ">="
|
|
129
130
|
- !ruby/object:Gem::Version
|
|
130
|
-
version: 3.
|
|
131
|
+
version: 3.3.0
|
|
131
132
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
133
|
requirements:
|
|
133
134
|
- - ">="
|
|
134
135
|
- !ruby/object:Gem::Version
|
|
135
136
|
version: '0'
|
|
136
137
|
requirements: []
|
|
137
|
-
rubygems_version: 4.0.
|
|
138
|
+
rubygems_version: 4.0.20
|
|
138
139
|
specification_version: 4
|
|
139
140
|
summary: A next generation CRuby profiler
|
|
140
141
|
test_files: []
|