vernier 0.1.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +19 -5
- data/Rakefile +4 -0
- data/examples/threaded_http_requests.rb +38 -0
- data/ext/vernier/ruby_type_names.h +44 -0
- data/ext/vernier/vernier.cc +1218 -114
- 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 -6
- data/Gemfile.lock +0 -24
- data/ext/vernier/stack.hh +0 -78
@@ -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
|