vernier 0.1.1 → 0.2.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 +19 -5
- data/Rakefile +4 -0
- data/examples/threaded_http_requests.rb +38 -0
- data/ext/vernier/extconf.rb +1 -1
- data/ext/vernier/ruby_type_names.h +44 -0
- data/ext/vernier/stack.hh +106 -29
- data/ext/vernier/vernier.cc +1147 -113
- data/lib/vernier/collector.rb +52 -0
- data/lib/vernier/marker.rb +38 -0
- data/lib/vernier/output/firefox.rb +365 -0
- data/lib/vernier/output/top.rb +30 -0
- data/lib/vernier/version.rb +1 -1
- data/lib/vernier.rb +167 -5
- data/vernier.gemspec +1 -1
- metadata +10 -5
- data/Gemfile.lock +0 -24
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "marker"
|
4
|
+
|
5
|
+
module Vernier
|
6
|
+
class Collector
|
7
|
+
def initialize(mode)
|
8
|
+
@mode = mode
|
9
|
+
end
|
10
|
+
|
11
|
+
def stop
|
12
|
+
result = finish
|
13
|
+
|
14
|
+
markers = []
|
15
|
+
marker_list = self.markers
|
16
|
+
size = marker_list.size
|
17
|
+
marker_strings = Marker.name_table
|
18
|
+
|
19
|
+
marker_list.each_with_index do |(tid, id, ts), i|
|
20
|
+
name = marker_strings[id]
|
21
|
+
finish = nil
|
22
|
+
phase = Marker::Phase::INSTANT
|
23
|
+
|
24
|
+
if id == Marker::Type::GC_EXIT
|
25
|
+
# skip because these are incorporated in "GC enter"
|
26
|
+
else
|
27
|
+
if id == Marker::Type::GC_ENTER
|
28
|
+
j = i + 1
|
29
|
+
|
30
|
+
name = "GC pause"
|
31
|
+
phase = Marker::Phase::INTERVAL
|
32
|
+
|
33
|
+
while j < size
|
34
|
+
if marker_list[j][1] == Marker::Type::GC_EXIT
|
35
|
+
finish = marker_list[j][2]
|
36
|
+
break
|
37
|
+
end
|
38
|
+
|
39
|
+
j += 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
markers << [tid, name, ts, finish, phase]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
result.instance_variable_set(:@markers, markers)
|
48
|
+
|
49
|
+
result
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "vernier" # Make sure constants are loaded
|
4
|
+
|
5
|
+
module Vernier
|
6
|
+
module Marker
|
7
|
+
# These are equal to the marker phase types from gecko-profile.js
|
8
|
+
module Phase # :nodoc:
|
9
|
+
INSTANT = 0
|
10
|
+
INTERVAL = 1
|
11
|
+
INTERVAL_START = 2
|
12
|
+
INTERVAL_END = 3
|
13
|
+
end
|
14
|
+
|
15
|
+
MARKER_STRINGS = []
|
16
|
+
|
17
|
+
MARKER_STRINGS[Type::GVL_THREAD_STARTED] = "thread started"
|
18
|
+
MARKER_STRINGS[Type::GVL_THREAD_READY] = "thread ready"
|
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"
|
22
|
+
|
23
|
+
MARKER_STRINGS[Type::GC_START] = "GC start"
|
24
|
+
MARKER_STRINGS[Type::GC_END_MARK] = "GC end marking"
|
25
|
+
MARKER_STRINGS[Type::GC_END_SWEEP] = "GC end sweeping"
|
26
|
+
MARKER_STRINGS[Type::GC_ENTER] = "GC enter"
|
27
|
+
MARKER_STRINGS[Type::GC_EXIT] = "GC exit"
|
28
|
+
|
29
|
+
MARKER_STRINGS.freeze
|
30
|
+
|
31
|
+
##
|
32
|
+
# Return an array of marker names. The index of the string maps to the
|
33
|
+
# value of the corresponding constant
|
34
|
+
def self.name_table
|
35
|
+
MARKER_STRINGS
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,365 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Vernier
|
6
|
+
module Output
|
7
|
+
# https://profiler.firefox.com/
|
8
|
+
# https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js
|
9
|
+
class Firefox
|
10
|
+
class Categorizer
|
11
|
+
attr_reader :categories, :gc_category, :jit_category
|
12
|
+
def initialize
|
13
|
+
@categories = []
|
14
|
+
|
15
|
+
add_category(name: "Default", color: "grey")
|
16
|
+
add_category(name: "Idle", color: "transparent")
|
17
|
+
@gc_category = add_category(name: "GC", color: "red")
|
18
|
+
add_category(
|
19
|
+
name: "stdlib",
|
20
|
+
color: "red",
|
21
|
+
matcher: starts_with(RbConfig::CONFIG["rubylibdir"])
|
22
|
+
)
|
23
|
+
add_category(name: "cfunc", color: "yellow", matcher: "<cfunc>")
|
24
|
+
|
25
|
+
rails_components = %w[ activesupport activemodel activerecord
|
26
|
+
actionview actionpack activejob actionmailer actioncable
|
27
|
+
activestorage actionmailbox actiontext railties ]
|
28
|
+
add_category(
|
29
|
+
name: "Rails",
|
30
|
+
color: "green",
|
31
|
+
matcher: gem_path(*rails_components)
|
32
|
+
)
|
33
|
+
add_category(
|
34
|
+
name: "gem",
|
35
|
+
color: "red",
|
36
|
+
matcher: starts_with(*Gem.path)
|
37
|
+
)
|
38
|
+
add_category(
|
39
|
+
name: "Application",
|
40
|
+
color: "purple"
|
41
|
+
)
|
42
|
+
@jit_category = add_category(
|
43
|
+
name: "JIT",
|
44
|
+
color: "blue"
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_category(**kw)
|
49
|
+
category = Category.new(@categories.length, **kw)
|
50
|
+
@categories << category
|
51
|
+
category
|
52
|
+
end
|
53
|
+
|
54
|
+
def starts_with(*paths)
|
55
|
+
%r{\A#{Regexp.union(paths)}}
|
56
|
+
end
|
57
|
+
|
58
|
+
def gem_path(*names)
|
59
|
+
%r{\A#{Regexp.union(Gem.path)}/gems/#{Regexp.union(names)}}
|
60
|
+
end
|
61
|
+
|
62
|
+
def categorize(path)
|
63
|
+
@categories.detect { |category| category.matches?(path) } || @categories.first
|
64
|
+
end
|
65
|
+
|
66
|
+
class Category
|
67
|
+
attr_reader :idx, :name, :color, :matcher
|
68
|
+
def initialize(idx, name:, color:, matcher: nil)
|
69
|
+
@idx = idx
|
70
|
+
@name = name
|
71
|
+
@color = color
|
72
|
+
@matcher = matcher
|
73
|
+
end
|
74
|
+
|
75
|
+
def matches?(path)
|
76
|
+
@matcher && @matcher === path
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def initialize(profile)
|
82
|
+
@profile = profile
|
83
|
+
@categorizer = Categorizer.new
|
84
|
+
end
|
85
|
+
|
86
|
+
def output
|
87
|
+
::JSON.generate(data)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
attr_reader :profile
|
93
|
+
|
94
|
+
def data
|
95
|
+
threads = Hash.new {|h,k| h[k] = {
|
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
|
116
|
+
|
117
|
+
thread_data = profile.threads.map do |tid, thread_info|
|
118
|
+
data = threads[tid]
|
119
|
+
Thread.new(
|
120
|
+
profile,
|
121
|
+
@categorizer,
|
122
|
+
**thread_info,
|
123
|
+
**data
|
124
|
+
).data
|
125
|
+
end
|
126
|
+
|
127
|
+
{
|
128
|
+
meta: {
|
129
|
+
interval: 1, # FIXME: memory vs wall
|
130
|
+
startTime: profile.meta[:started_at] / 1_000_000.0,
|
131
|
+
endTime: (profile.timestamps&.max || 0) / 1_000_000.0,
|
132
|
+
processType: 0,
|
133
|
+
product: "Ruby/Vernier",
|
134
|
+
stackwalk: 1,
|
135
|
+
version: 28,
|
136
|
+
preprocessedProfileVersion: 47,
|
137
|
+
symbolicated: true,
|
138
|
+
markerSchema: [],
|
139
|
+
sampleUnits: {
|
140
|
+
time: "ms",
|
141
|
+
eventDelay: "ms",
|
142
|
+
threadCPUDelta: "µs"
|
143
|
+
}, # FIXME: memory vs wall
|
144
|
+
categories: @categorizer.categories.map do |category|
|
145
|
+
{
|
146
|
+
name: category.name,
|
147
|
+
color: category.color,
|
148
|
+
subcategories: []
|
149
|
+
}
|
150
|
+
end
|
151
|
+
},
|
152
|
+
libs: [],
|
153
|
+
threads: thread_data
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
class Thread
|
158
|
+
attr_reader :profile
|
159
|
+
|
160
|
+
def initialize(profile, categorizer, name:, tid:, samples:, weights:, timestamps:, categories:, markers:, started_at:, stopped_at: nil)
|
161
|
+
@profile = profile
|
162
|
+
@categorizer = categorizer
|
163
|
+
@tid = tid
|
164
|
+
@name = name
|
165
|
+
|
166
|
+
@samples, @weights, @timestamps = samples, weights, timestamps
|
167
|
+
@sample_categories = categories
|
168
|
+
@markers = markers
|
169
|
+
|
170
|
+
@started_at, @stopped_at = started_at, stopped_at
|
171
|
+
|
172
|
+
names = profile.func_table.fetch(:name)
|
173
|
+
filenames = profile.func_table.fetch(:filename)
|
174
|
+
|
175
|
+
stacks_size = profile.stack_table.fetch(:frame).size
|
176
|
+
@categorized_stacks = Hash.new do |h, k|
|
177
|
+
h[k] = h.size + stacks_size
|
178
|
+
end
|
179
|
+
|
180
|
+
@strings = Hash.new { |h, k| h[k] = h.size }
|
181
|
+
@func_names = names.map do |name|
|
182
|
+
@strings[name]
|
183
|
+
end
|
184
|
+
@filenames = filenames.map do |filename|
|
185
|
+
@strings[filename]
|
186
|
+
end
|
187
|
+
@frame_categories = filenames.map do |filename|
|
188
|
+
@categorizer.categorize(filename)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def data
|
193
|
+
{
|
194
|
+
name: @name,
|
195
|
+
isMainThread: @tid == ::Thread.main.native_thread_id,
|
196
|
+
processStartupTime: 0, # FIXME
|
197
|
+
processShutdownTime: nil, # FIXME
|
198
|
+
registerTime: (@started_at - 0) / 1_000_000.0,
|
199
|
+
unregisterTime: ((@stopped_at - 0) / 1_000_000.0 if @stopped_at),
|
200
|
+
pausedRanges: [],
|
201
|
+
pid: profile.pid || Process.pid,
|
202
|
+
tid: @tid,
|
203
|
+
frameTable: frame_table,
|
204
|
+
funcTable: func_table,
|
205
|
+
nativeSymbols: {},
|
206
|
+
samples: samples_table,
|
207
|
+
stackTable: stack_table,
|
208
|
+
resourceTable: {
|
209
|
+
length: 0,
|
210
|
+
lib: [],
|
211
|
+
name: [],
|
212
|
+
host: [],
|
213
|
+
type: []
|
214
|
+
},
|
215
|
+
markers: markers_table,
|
216
|
+
stringArray: string_table
|
217
|
+
}
|
218
|
+
end
|
219
|
+
|
220
|
+
def markers_table
|
221
|
+
size = @markers.size
|
222
|
+
|
223
|
+
string_indexes = []
|
224
|
+
start_times = []
|
225
|
+
end_times = []
|
226
|
+
phases = []
|
227
|
+
categories = []
|
228
|
+
|
229
|
+
@markers.each_with_index do |(_, name, start, finish, phase), i|
|
230
|
+
string_indexes << @strings[name]
|
231
|
+
start_times << (start / 1_000_000.0)
|
232
|
+
|
233
|
+
# Please don't hate me. Divide by 1,000,000 only if finish is not nil
|
234
|
+
end_times << (finish&./(1_000_000.0))
|
235
|
+
phases << phase
|
236
|
+
categories << (name =~ /GC/ ? gc_category.idx : 0)
|
237
|
+
end
|
238
|
+
|
239
|
+
{
|
240
|
+
data: [nil] * start_times.size,
|
241
|
+
name: string_indexes,
|
242
|
+
startTime: start_times,
|
243
|
+
endTime: end_times,
|
244
|
+
phase: phases,
|
245
|
+
category: categories,
|
246
|
+
length: start_times.size
|
247
|
+
}
|
248
|
+
end
|
249
|
+
|
250
|
+
def samples_table
|
251
|
+
samples = @samples
|
252
|
+
weights = @weights
|
253
|
+
categories = @sample_categories
|
254
|
+
size = samples.size
|
255
|
+
if categories.empty?
|
256
|
+
categories = [0] * size
|
257
|
+
end
|
258
|
+
|
259
|
+
if @timestamps
|
260
|
+
times = @timestamps.map { _1 / 1_000_000.0 }
|
261
|
+
else
|
262
|
+
# FIXME: record timestamps for memory samples
|
263
|
+
times = (0...size).to_a
|
264
|
+
end
|
265
|
+
|
266
|
+
raise unless samples.size == size
|
267
|
+
raise unless weights.size == size
|
268
|
+
raise unless times.size == size
|
269
|
+
|
270
|
+
samples = samples.zip(categories).map do |sample, category|
|
271
|
+
if category == 0
|
272
|
+
sample
|
273
|
+
else
|
274
|
+
@categorized_stacks[[sample, category]]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
{
|
279
|
+
stack: samples,
|
280
|
+
time: times,
|
281
|
+
weight: weights,
|
282
|
+
weightType: "samples",
|
283
|
+
#weightType: "bytes",
|
284
|
+
length: samples.length
|
285
|
+
}
|
286
|
+
end
|
287
|
+
|
288
|
+
def stack_table
|
289
|
+
frames = profile.stack_table.fetch(:frame).dup
|
290
|
+
prefixes = profile.stack_table.fetch(:parent).dup
|
291
|
+
categories = frames.map{|idx| @frame_categories[idx].idx }
|
292
|
+
|
293
|
+
@categorized_stacks.keys.each do |(stack, category)|
|
294
|
+
frames << frames[stack]
|
295
|
+
prefixes << prefixes[stack]
|
296
|
+
categories << category
|
297
|
+
end
|
298
|
+
|
299
|
+
size = frames.length
|
300
|
+
raise unless frames.size == size
|
301
|
+
raise unless prefixes.size == size
|
302
|
+
{
|
303
|
+
frame: frames,
|
304
|
+
category: categories,
|
305
|
+
subcategory: [0] * size,
|
306
|
+
prefix: prefixes,
|
307
|
+
length: prefixes.length
|
308
|
+
}
|
309
|
+
end
|
310
|
+
|
311
|
+
def frame_table
|
312
|
+
funcs = profile.frame_table.fetch(:func)
|
313
|
+
lines = profile.frame_table.fetch(:line)
|
314
|
+
size = funcs.length
|
315
|
+
none = [nil] * size
|
316
|
+
categories = @frame_categories.map(&:idx)
|
317
|
+
|
318
|
+
raise unless lines.size == funcs.size
|
319
|
+
|
320
|
+
{
|
321
|
+
address: [-1] * size,
|
322
|
+
inlineDepth: [0] * size,
|
323
|
+
category: categories,
|
324
|
+
subcategory: nil,
|
325
|
+
func: funcs,
|
326
|
+
nativeSymbol: none,
|
327
|
+
innerWindowID: none,
|
328
|
+
implementation: none,
|
329
|
+
line: lines,
|
330
|
+
column: none,
|
331
|
+
length: size
|
332
|
+
}
|
333
|
+
end
|
334
|
+
|
335
|
+
def func_table
|
336
|
+
size = @func_names.size
|
337
|
+
|
338
|
+
cfunc_idx = @strings["<cfunc>"]
|
339
|
+
is_js = @filenames.map { |fn| fn != cfunc_idx }
|
340
|
+
{
|
341
|
+
name: @func_names,
|
342
|
+
isJS: is_js,
|
343
|
+
relevantForJS: is_js,
|
344
|
+
resource: [-1] * size, # set to unidentified for now
|
345
|
+
fileName: @filenames,
|
346
|
+
lineNumber: profile.func_table.fetch(:first_line),
|
347
|
+
columnNumber: [0] * size,
|
348
|
+
#columnNumber: functions.map { _1.column },
|
349
|
+
length: size
|
350
|
+
}
|
351
|
+
end
|
352
|
+
|
353
|
+
def string_table
|
354
|
+
@strings.keys
|
355
|
+
end
|
356
|
+
|
357
|
+
private
|
358
|
+
|
359
|
+
def gc_category
|
360
|
+
@categorizer.gc_category
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vernier
|
4
|
+
module Output
|
5
|
+
class Top
|
6
|
+
def initialize(profile)
|
7
|
+
@profile = profile
|
8
|
+
end
|
9
|
+
|
10
|
+
def output
|
11
|
+
stack_weights = Hash.new(0)
|
12
|
+
@profile.samples.zip(@profile.weights) do |stack_idx, weight|
|
13
|
+
stack_weights[stack_idx] += weight
|
14
|
+
end
|
15
|
+
|
16
|
+
top_by_self = Hash.new(0)
|
17
|
+
stack_weights.each do |stack_idx, weight|
|
18
|
+
stack = @profile.stack(stack_idx)
|
19
|
+
top_by_self[stack.leaf_frame.name] += weight
|
20
|
+
end
|
21
|
+
|
22
|
+
s = +""
|
23
|
+
top_by_self.sort_by(&:last).reverse.each do |frame, samples|
|
24
|
+
s << "#{samples}\t#{frame}\n"
|
25
|
+
end
|
26
|
+
s
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/vernier/version.rb
CHANGED
data/lib/vernier.rb
CHANGED
@@ -1,19 +1,181 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "vernier/version"
|
4
|
+
require_relative "vernier/collector"
|
4
5
|
require_relative "vernier/vernier"
|
6
|
+
require_relative "vernier/output/firefox"
|
7
|
+
require_relative "vernier/output/top"
|
5
8
|
|
6
9
|
module Vernier
|
7
10
|
class Error < StandardError; end
|
8
11
|
|
12
|
+
class Result
|
13
|
+
attr_reader :weights, :samples, :stack_table, :frame_table, :func_table
|
14
|
+
attr_reader :timestamps, :sample_threads, :sample_categories
|
15
|
+
attr_reader :markers
|
16
|
+
|
17
|
+
attr_accessor :pid, :start_time, :end_time
|
18
|
+
attr_accessor :threads
|
19
|
+
attr_accessor :meta
|
20
|
+
|
21
|
+
def started_at
|
22
|
+
meta[:started_at]
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_gecko
|
26
|
+
Output::Firefox.new(self).output
|
27
|
+
end
|
28
|
+
|
29
|
+
def write(out:)
|
30
|
+
File.write(out, to_gecko)
|
31
|
+
end
|
32
|
+
|
33
|
+
def each_sample
|
34
|
+
return enum_for(__method__) unless block_given?
|
35
|
+
@samples.size.times do |sample_idx|
|
36
|
+
weight = @weights[sample_idx]
|
37
|
+
stack_idx = @samples[sample_idx]
|
38
|
+
yield stack(stack_idx), weight
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class BaseType
|
43
|
+
attr_reader :result, :idx
|
44
|
+
def initialize(result, idx)
|
45
|
+
@result = result
|
46
|
+
@idx = idx
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
idx.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
"#<#{self.class}\n#{to_s}>"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Func < BaseType
|
59
|
+
def label
|
60
|
+
result.func_table[:name][idx]
|
61
|
+
end
|
62
|
+
alias name label
|
63
|
+
|
64
|
+
def filename
|
65
|
+
result.func_table[:filename][idx]
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
"#{name} at #{filename}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Frame < BaseType
|
74
|
+
def label; func.label; end
|
75
|
+
def filename; func.filename; end
|
76
|
+
alias name label
|
77
|
+
|
78
|
+
def func
|
79
|
+
func_idx = result.frame_table[:func][idx]
|
80
|
+
Func.new(result, func_idx)
|
81
|
+
end
|
82
|
+
|
83
|
+
def line
|
84
|
+
result.frame_table[:line][idx]
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_s
|
88
|
+
"#{func}:#{line}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class Stack < BaseType
|
93
|
+
def each_frame
|
94
|
+
return enum_for(__method__) unless block_given?
|
95
|
+
|
96
|
+
stack_idx = idx
|
97
|
+
while stack_idx
|
98
|
+
frame_idx = result.stack_table[:frame][stack_idx]
|
99
|
+
yield Frame.new(result, frame_idx)
|
100
|
+
stack_idx = result.stack_table[:parent][stack_idx]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def leaf_frame_idx
|
105
|
+
result.stack_table[:frame][idx]
|
106
|
+
end
|
107
|
+
|
108
|
+
def leaf_frame
|
109
|
+
Frame.new(result, leaf_frame_idx)
|
110
|
+
end
|
111
|
+
|
112
|
+
def frames
|
113
|
+
each_frame.to_a
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_s
|
117
|
+
arr = []
|
118
|
+
each_frame do |frame|
|
119
|
+
arr << frame.to_s
|
120
|
+
end
|
121
|
+
arr.join("\n")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def stack(idx)
|
126
|
+
Stack.new(self, idx)
|
127
|
+
end
|
128
|
+
|
129
|
+
def total_bytes
|
130
|
+
@weights.sum
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.trace(mode: :wall, out: nil, interval: nil)
|
135
|
+
collector = Vernier::Collector.new(mode, { interval: })
|
136
|
+
collector.start
|
137
|
+
|
138
|
+
result = nil
|
139
|
+
begin
|
140
|
+
yield
|
141
|
+
ensure
|
142
|
+
result = collector.stop
|
143
|
+
end
|
144
|
+
|
145
|
+
if out
|
146
|
+
File.write(out, Output::Firefox.new(result).output)
|
147
|
+
end
|
148
|
+
result
|
149
|
+
end
|
150
|
+
|
9
151
|
def self.trace_retained(out: nil, gc: true)
|
10
152
|
3.times { GC.start } if gc
|
11
|
-
Vernier.trace_retained_start
|
12
|
-
yield
|
13
|
-
3.times { GC.start } if gc
|
14
|
-
result = Vernier.trace_retained_stop
|
15
153
|
|
16
|
-
|
154
|
+
start_time = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
|
155
|
+
|
156
|
+
collector = Vernier::Collector.new(:retained)
|
157
|
+
collector.start
|
158
|
+
|
159
|
+
result = nil
|
160
|
+
begin
|
161
|
+
yield
|
162
|
+
ensure
|
163
|
+
result = collector.stop
|
164
|
+
end_time = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
|
165
|
+
result.pid = Process.pid
|
166
|
+
result.start_time = start_time
|
167
|
+
result.end_time = end_time
|
168
|
+
end
|
169
|
+
|
170
|
+
if out
|
171
|
+
result.write(out:)
|
172
|
+
end
|
17
173
|
result
|
18
174
|
end
|
175
|
+
|
176
|
+
class Collector
|
177
|
+
def self.new(mode, options = {})
|
178
|
+
_new(mode, options)
|
179
|
+
end
|
180
|
+
end
|
19
181
|
end
|
data/vernier.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.description = spec.summary
|
13
13
|
spec.homepage = "https://github.com/jhawthorn/vernier"
|
14
14
|
spec.license = "MIT"
|
15
|
-
spec.required_ruby_version = ">= 2.
|
15
|
+
spec.required_ruby_version = ">= 3.2.0"
|
16
16
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
18
18
|
spec.metadata["source_code_uri"] = spec.homepage
|