vernier 0.2.1 → 0.3.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/README.md +2 -2
- data/bin/vernier +4 -0
- data/examples/minitest.rb +20 -0
- data/examples/ractor.rb +11 -0
- data/examples/rails.rb +125 -0
- data/exe/vernier +38 -0
- data/ext/vernier/vernier.cc +558 -295
- data/lib/vernier/autorun.rb +65 -0
- data/lib/vernier/collector.rb +47 -28
- data/lib/vernier/marker.rb +11 -11
- data/lib/vernier/output/firefox.rb +74 -41
- data/lib/vernier/result.rb +139 -0
- data/lib/vernier/version.rb +1 -1
- data/lib/vernier.rb +3 -130
- metadata +11 -4
- data/sig/vernier.rbs +0 -4
@@ -0,0 +1,65 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
require "vernier"
|
3
|
+
|
4
|
+
module Vernier
|
5
|
+
module Autorun
|
6
|
+
class << self
|
7
|
+
attr_accessor :collector
|
8
|
+
attr_reader :options
|
9
|
+
end
|
10
|
+
@collector = nil
|
11
|
+
|
12
|
+
@options = ENV.to_h.select { |k,v| k.start_with?("VERNIER_") }
|
13
|
+
@options.transform_keys! do |key|
|
14
|
+
key.sub(/\AVERNIER_/, "").downcase.to_sym
|
15
|
+
end
|
16
|
+
@options.freeze
|
17
|
+
|
18
|
+
def self.start
|
19
|
+
interval = options.fetch(:interval, 500).to_i
|
20
|
+
|
21
|
+
STDERR.puts("starting profiler with interval #{interval}")
|
22
|
+
|
23
|
+
@collector = Vernier::Collector.new(:wall, interval:)
|
24
|
+
@collector.start
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.stop
|
28
|
+
result = @collector.stop
|
29
|
+
@collector = nil
|
30
|
+
output_path = options[:output]
|
31
|
+
output_path ||= Tempfile.create(["profile", ".vernier.json"]).path
|
32
|
+
File.write(output_path, Vernier::Output::Firefox.new(result).output)
|
33
|
+
|
34
|
+
STDERR.puts(result.inspect)
|
35
|
+
STDERR.puts("written to #{output_path}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.running?
|
39
|
+
!!@collector
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.at_exit
|
43
|
+
stop if running?
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.toggle
|
47
|
+
running? ? stop : start
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
unless Vernier::Autorun.options[:start_paused]
|
53
|
+
Vernier::Autorun.start
|
54
|
+
end
|
55
|
+
|
56
|
+
if signal = Vernier::Autorun.options[:signal]
|
57
|
+
STDERR.puts "to toggle profiler: kill -#{signal} #{Process.pid}"
|
58
|
+
trap(signal) do
|
59
|
+
Vernier::Autorun.toggle
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
at_exit do
|
64
|
+
Vernier::Autorun.at_exit
|
65
|
+
end
|
data/lib/vernier/collector.rb
CHANGED
@@ -6,44 +6,63 @@ module Vernier
|
|
6
6
|
class Collector
|
7
7
|
def initialize(mode)
|
8
8
|
@mode = mode
|
9
|
+
@markers = []
|
9
10
|
end
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
##
|
13
|
+
# Get the current time.
|
14
|
+
#
|
15
|
+
# This method returns the current time from Process.clock_gettime in
|
16
|
+
# integer nanoseconds. It's the same time used by Vernier internals and
|
17
|
+
# can be used to generate timestamps for custom markers.
|
18
|
+
def current_time
|
19
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
20
|
+
end
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
22
|
+
def add_marker(name:, start:, finish:, thread: Thread.current.native_thread_id, phase: Marker::Phase::INTERVAL, data: nil)
|
23
|
+
@markers << [thread,
|
24
|
+
name,
|
25
|
+
start,
|
26
|
+
finish,
|
27
|
+
phase,
|
28
|
+
data]
|
29
|
+
end
|
23
30
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
31
|
+
##
|
32
|
+
# Record an interval with a category and name. Yields to a block and
|
33
|
+
# records the amount of time spent in the block as an interval marker.
|
34
|
+
def record_interval(category, name = category)
|
35
|
+
start = current_time
|
36
|
+
yield
|
37
|
+
add_marker(
|
38
|
+
name: category,
|
39
|
+
start:,
|
40
|
+
finish: current_time,
|
41
|
+
phase: Marker::Phase::INTERVAL,
|
42
|
+
thread: Thread.current.native_thread_id,
|
43
|
+
data: { :type => 'UserTiming', :entryType => 'measure', :name => name }
|
44
|
+
)
|
45
|
+
end
|
29
46
|
|
30
|
-
|
31
|
-
|
47
|
+
def stop
|
48
|
+
result = finish
|
32
49
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
break
|
37
|
-
end
|
50
|
+
end_time = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
|
51
|
+
result.pid = Process.pid
|
52
|
+
result.end_time = end_time
|
38
53
|
|
39
|
-
|
40
|
-
end
|
41
|
-
end
|
54
|
+
marker_strings = Marker.name_table
|
42
55
|
|
43
|
-
|
44
|
-
|
56
|
+
markers = self.markers.map do |(tid, type, phase, ts, te, stack)|
|
57
|
+
name = marker_strings[type]
|
58
|
+
sym = Marker::MARKER_SYMBOLS[type]
|
59
|
+
data = { type: sym }
|
60
|
+
data[:cause] = { stack: stack } if stack
|
61
|
+
[tid, name, ts, te, phase, data]
|
45
62
|
end
|
46
63
|
|
64
|
+
markers.concat @markers
|
65
|
+
|
47
66
|
result.instance_variable_set(:@markers, markers)
|
48
67
|
|
49
68
|
result
|
data/lib/vernier/marker.rb
CHANGED
@@ -4,27 +4,27 @@ require_relative "vernier" # Make sure constants are loaded
|
|
4
4
|
|
5
5
|
module Vernier
|
6
6
|
module Marker
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
INTERVAL = 1
|
11
|
-
INTERVAL_START = 2
|
12
|
-
INTERVAL_END = 3
|
7
|
+
MARKER_SYMBOLS = []
|
8
|
+
Type.constants.each do |name|
|
9
|
+
MARKER_SYMBOLS[Type.const_get(name)] = name
|
13
10
|
end
|
11
|
+
MARKER_SYMBOLS.freeze
|
14
12
|
|
15
13
|
MARKER_STRINGS = []
|
16
14
|
|
17
|
-
MARKER_STRINGS[Type::GVL_THREAD_STARTED] = "
|
18
|
-
MARKER_STRINGS[Type::
|
19
|
-
MARKER_STRINGS[Type::GVL_THREAD_RESUMED] = "thread resumed"
|
20
|
-
MARKER_STRINGS[Type::GVL_THREAD_SUSPENDED] = "thread suspended"
|
21
|
-
MARKER_STRINGS[Type::GVL_THREAD_EXITED] = "thread exited"
|
15
|
+
MARKER_STRINGS[Type::GVL_THREAD_STARTED] = "Thread started"
|
16
|
+
MARKER_STRINGS[Type::GVL_THREAD_EXITED] = "Thread exited"
|
22
17
|
|
23
18
|
MARKER_STRINGS[Type::GC_START] = "GC start"
|
24
19
|
MARKER_STRINGS[Type::GC_END_MARK] = "GC end marking"
|
25
20
|
MARKER_STRINGS[Type::GC_END_SWEEP] = "GC end sweeping"
|
26
21
|
MARKER_STRINGS[Type::GC_ENTER] = "GC enter"
|
27
22
|
MARKER_STRINGS[Type::GC_EXIT] = "GC exit"
|
23
|
+
MARKER_STRINGS[Type::GC_PAUSE] = "GC pause"
|
24
|
+
|
25
|
+
MARKER_STRINGS[Type::THREAD_RUNNING] = "Thread Running"
|
26
|
+
MARKER_STRINGS[Type::THREAD_STALLED] = "Thread Stalled"
|
27
|
+
MARKER_STRINGS[Type::THREAD_SUSPENDED] = "Thread Suspended"
|
28
28
|
|
29
29
|
MARKER_STRINGS.freeze
|
30
30
|
|
@@ -8,13 +8,15 @@ module Vernier
|
|
8
8
|
# https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js
|
9
9
|
class Firefox
|
10
10
|
class Categorizer
|
11
|
-
attr_reader :categories
|
11
|
+
attr_reader :categories
|
12
12
|
def initialize
|
13
13
|
@categories = []
|
14
|
+
@categories_by_name = {}
|
14
15
|
|
15
16
|
add_category(name: "Default", color: "grey")
|
16
17
|
add_category(name: "Idle", color: "transparent")
|
17
|
-
|
18
|
+
|
19
|
+
add_category(name: "GC", color: "red")
|
18
20
|
add_category(
|
19
21
|
name: "stdlib",
|
20
22
|
color: "red",
|
@@ -39,18 +41,21 @@ module Vernier
|
|
39
41
|
name: "Application",
|
40
42
|
color: "purple"
|
41
43
|
)
|
42
|
-
|
43
|
-
|
44
|
-
color: "blue"
|
45
|
-
)
|
44
|
+
|
45
|
+
add_category(name: "Thread", color: "grey")
|
46
46
|
end
|
47
47
|
|
48
|
-
def add_category(**kw)
|
49
|
-
category = Category.new(@categories.length, **kw)
|
48
|
+
def add_category(name:, **kw)
|
49
|
+
category = Category.new(@categories.length, name: name, **kw)
|
50
50
|
@categories << category
|
51
|
+
@categories_by_name[name] = category
|
51
52
|
category
|
52
53
|
end
|
53
54
|
|
55
|
+
def get_category(name)
|
56
|
+
@categories_by_name[name]
|
57
|
+
end
|
58
|
+
|
54
59
|
def starts_with(*paths)
|
55
60
|
%r{\A#{Regexp.union(paths)}}
|
56
61
|
end
|
@@ -92,50 +97,30 @@ module Vernier
|
|
92
97
|
attr_reader :profile
|
93
98
|
|
94
99
|
def data
|
95
|
-
|
96
|
-
timestamps: [],
|
97
|
-
weights: [],
|
98
|
-
samples: [],
|
99
|
-
categories: [],
|
100
|
-
markers: [],
|
101
|
-
}}
|
102
|
-
|
103
|
-
profile.samples.size.times do |i|
|
104
|
-
tid = profile.sample_threads[i]
|
105
|
-
thread = threads[tid]
|
106
|
-
|
107
|
-
thread[:timestamps] << profile.timestamps[i]
|
108
|
-
thread[:weights] << profile.weights[i]
|
109
|
-
thread[:samples] << profile.samples[i]
|
110
|
-
thread[:categories] << profile.sample_categories[i]
|
111
|
-
end
|
112
|
-
|
113
|
-
profile.markers.each do |marker|
|
114
|
-
threads[marker[0]][:markers] << marker
|
115
|
-
end
|
100
|
+
markers_by_thread = profile.markers.group_by { |marker| marker[0] }
|
116
101
|
|
117
102
|
thread_data = profile.threads.map do |tid, thread_info|
|
118
|
-
|
103
|
+
markers = markers_by_thread[tid] || []
|
119
104
|
Thread.new(
|
120
105
|
profile,
|
121
106
|
@categorizer,
|
107
|
+
markers: markers,
|
122
108
|
**thread_info,
|
123
|
-
**data
|
124
109
|
).data
|
125
110
|
end
|
126
111
|
|
127
112
|
{
|
128
113
|
meta: {
|
129
114
|
interval: 1, # FIXME: memory vs wall
|
130
|
-
startTime: profile.
|
131
|
-
endTime: (profile.timestamps&.max || 0) / 1_000_000.0,
|
115
|
+
startTime: profile.started_at / 1_000_000.0,
|
116
|
+
#endTime: (profile.timestamps&.max || 0) / 1_000_000.0,
|
132
117
|
processType: 0,
|
133
118
|
product: "Ruby/Vernier",
|
134
119
|
stackwalk: 1,
|
135
120
|
version: 28,
|
136
121
|
preprocessedProfileVersion: 47,
|
137
122
|
symbolicated: true,
|
138
|
-
markerSchema:
|
123
|
+
markerSchema: marker_schema,
|
139
124
|
sampleUnits: {
|
140
125
|
time: "ms",
|
141
126
|
eventDelay: "ms",
|
@@ -154,17 +139,32 @@ module Vernier
|
|
154
139
|
}
|
155
140
|
end
|
156
141
|
|
142
|
+
def marker_schema
|
143
|
+
[
|
144
|
+
{
|
145
|
+
name: "GVL_THREAD_RESUMED",
|
146
|
+
display: [ "marker-chart", "marker-table" ],
|
147
|
+
data: [
|
148
|
+
{
|
149
|
+
label: "Description",
|
150
|
+
value: "The thread has acquired the GVL and is executing"
|
151
|
+
}
|
152
|
+
]
|
153
|
+
}
|
154
|
+
]
|
155
|
+
end
|
156
|
+
|
157
157
|
class Thread
|
158
158
|
attr_reader :profile
|
159
159
|
|
160
|
-
def initialize(profile, categorizer, name:, tid:, samples:, weights:, timestamps:,
|
160
|
+
def initialize(profile, categorizer, name:, tid:, samples:, weights:, timestamps:, sample_categories:, markers:, started_at:, stopped_at: nil)
|
161
161
|
@profile = profile
|
162
162
|
@categorizer = categorizer
|
163
163
|
@tid = tid
|
164
164
|
@name = name
|
165
165
|
|
166
166
|
@samples, @weights, @timestamps = samples, weights, timestamps
|
167
|
-
@sample_categories =
|
167
|
+
@sample_categories = sample_categories
|
168
168
|
@markers = markers
|
169
169
|
|
170
170
|
@started_at, @stopped_at = started_at, stopped_at
|
@@ -184,6 +184,25 @@ module Vernier
|
|
184
184
|
@filenames = filenames.map do |filename|
|
185
185
|
@strings[filename]
|
186
186
|
end
|
187
|
+
|
188
|
+
lines = profile.frame_table.fetch(:line)
|
189
|
+
|
190
|
+
@frame_implementations = filenames.zip(lines).map do |filename, line|
|
191
|
+
# Must match strings in `src/profile-logic/profile-data.js`
|
192
|
+
# inside the firefox profiler. See `getFriendlyStackTypeName`
|
193
|
+
if filename == "<cfunc>"
|
194
|
+
@strings["native"]
|
195
|
+
else
|
196
|
+
# FIXME: We need to get upstream support for JIT frames
|
197
|
+
if line == -1
|
198
|
+
@strings["yjit"]
|
199
|
+
else
|
200
|
+
# nil means interpreter
|
201
|
+
nil
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
187
206
|
@frame_categories = filenames.map do |filename|
|
188
207
|
@categorizer.categorize(filename)
|
189
208
|
end
|
@@ -225,19 +244,29 @@ module Vernier
|
|
225
244
|
end_times = []
|
226
245
|
phases = []
|
227
246
|
categories = []
|
247
|
+
data = []
|
228
248
|
|
229
|
-
@markers.each_with_index do |(_, name, start, finish, phase), i|
|
249
|
+
@markers.each_with_index do |(_, name, start, finish, phase, datum), i|
|
230
250
|
string_indexes << @strings[name]
|
231
251
|
start_times << (start / 1_000_000.0)
|
232
252
|
|
233
253
|
# Please don't hate me. Divide by 1,000,000 only if finish is not nil
|
234
254
|
end_times << (finish&./(1_000_000.0))
|
235
255
|
phases << phase
|
236
|
-
|
256
|
+
|
257
|
+
category = case name
|
258
|
+
when /\AGC/ then gc_category.idx
|
259
|
+
when /\AThread/ then thread_category.idx
|
260
|
+
else
|
261
|
+
0
|
262
|
+
end
|
263
|
+
|
264
|
+
categories << category
|
265
|
+
data << datum
|
237
266
|
end
|
238
267
|
|
239
268
|
{
|
240
|
-
data:
|
269
|
+
data: data,
|
241
270
|
name: string_indexes,
|
242
271
|
startTime: start_times,
|
243
272
|
endTime: end_times,
|
@@ -325,7 +354,7 @@ module Vernier
|
|
325
354
|
func: funcs,
|
326
355
|
nativeSymbol: none,
|
327
356
|
innerWindowID: none,
|
328
|
-
implementation:
|
357
|
+
implementation: @frame_implementations,
|
329
358
|
line: lines,
|
330
359
|
column: none,
|
331
360
|
length: size
|
@@ -357,7 +386,11 @@ module Vernier
|
|
357
386
|
private
|
358
387
|
|
359
388
|
def gc_category
|
360
|
-
@categorizer.
|
389
|
+
@categorizer.get_category("GC")
|
390
|
+
end
|
391
|
+
|
392
|
+
def thread_category
|
393
|
+
@categorizer.get_category("Thread")
|
361
394
|
end
|
362
395
|
end
|
363
396
|
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module Vernier
|
2
|
+
class Result
|
3
|
+
attr_reader :stack_table, :frame_table, :func_table
|
4
|
+
attr_reader :markers
|
5
|
+
|
6
|
+
attr_accessor :pid, :end_time
|
7
|
+
attr_accessor :threads
|
8
|
+
attr_accessor :meta
|
9
|
+
|
10
|
+
# TODO: remove these
|
11
|
+
def weights; threads.values.flat_map { _1[:weights] }; end
|
12
|
+
def samples; threads.values.flat_map { _1[:samples] }; end
|
13
|
+
def sample_categories; threads.values.flat_map { _1[:sample_categories] }; end
|
14
|
+
|
15
|
+
# Realtime in nanoseconds since the unix epoch
|
16
|
+
def started_at
|
17
|
+
started_at_mono_ns = meta[:started_at]
|
18
|
+
current_time_mono_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
19
|
+
current_time_real_ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
|
20
|
+
(current_time_real_ns - current_time_mono_ns + started_at_mono_ns)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_gecko
|
24
|
+
Output::Firefox.new(self).output
|
25
|
+
end
|
26
|
+
|
27
|
+
def write(out:)
|
28
|
+
File.write(out, to_gecko)
|
29
|
+
end
|
30
|
+
|
31
|
+
def elapsed_seconds
|
32
|
+
(end_time - started_at) / 1_000_000_000.0
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
"#<#{self.class} #{elapsed_seconds} seconds, #{threads.count} threads, #{weights.sum} samples, #{samples.uniq.size} unique>"
|
37
|
+
end
|
38
|
+
|
39
|
+
def each_sample
|
40
|
+
return enum_for(__method__) unless block_given?
|
41
|
+
samples.size.times do |sample_idx|
|
42
|
+
weight = weights[sample_idx]
|
43
|
+
stack_idx = samples[sample_idx]
|
44
|
+
yield stack(stack_idx), weight
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class BaseType
|
49
|
+
attr_reader :result, :idx
|
50
|
+
def initialize(result, idx)
|
51
|
+
@result = result
|
52
|
+
@idx = idx
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
idx.to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
def inspect
|
60
|
+
"#<#{self.class}\n#{to_s}>"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Func < BaseType
|
65
|
+
def label
|
66
|
+
result.func_table[:name][idx]
|
67
|
+
end
|
68
|
+
alias name label
|
69
|
+
|
70
|
+
def filename
|
71
|
+
result.func_table[:filename][idx]
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
"#{name} at #{filename}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Frame < BaseType
|
80
|
+
def label; func.label; end
|
81
|
+
def filename; func.filename; end
|
82
|
+
alias name label
|
83
|
+
|
84
|
+
def func
|
85
|
+
func_idx = result.frame_table[:func][idx]
|
86
|
+
Func.new(result, func_idx)
|
87
|
+
end
|
88
|
+
|
89
|
+
def line
|
90
|
+
result.frame_table[:line][idx]
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_s
|
94
|
+
"#{func}:#{line}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class Stack < BaseType
|
99
|
+
def each_frame
|
100
|
+
return enum_for(__method__) unless block_given?
|
101
|
+
|
102
|
+
stack_idx = idx
|
103
|
+
while stack_idx
|
104
|
+
frame_idx = result.stack_table[:frame][stack_idx]
|
105
|
+
yield Frame.new(result, frame_idx)
|
106
|
+
stack_idx = result.stack_table[:parent][stack_idx]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def leaf_frame_idx
|
111
|
+
result.stack_table[:frame][idx]
|
112
|
+
end
|
113
|
+
|
114
|
+
def leaf_frame
|
115
|
+
Frame.new(result, leaf_frame_idx)
|
116
|
+
end
|
117
|
+
|
118
|
+
def frames
|
119
|
+
each_frame.to_a
|
120
|
+
end
|
121
|
+
|
122
|
+
def to_s
|
123
|
+
arr = []
|
124
|
+
each_frame do |frame|
|
125
|
+
arr << frame.to_s
|
126
|
+
end
|
127
|
+
arr.join("\n")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def stack(idx)
|
132
|
+
Stack.new(self, idx)
|
133
|
+
end
|
134
|
+
|
135
|
+
def total_bytes
|
136
|
+
weights.sum
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/vernier/version.rb
CHANGED