vernier 0.5.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +22 -4
- data/examples/gvl_sleep.rb +62 -0
- data/examples/measure_overhead.rb +39 -0
- data/examples/rails.rb +18 -13
- data/examples/threaded_http_requests.rb +1 -1
- data/exe/vernier +3 -0
- data/ext/vernier/vernier.cc +475 -151
- data/lib/vernier/autorun.rb +2 -1
- data/lib/vernier/collector.rb +46 -1
- data/lib/vernier/hooks/active_support.rb +110 -0
- data/lib/vernier/hooks.rb +7 -0
- data/lib/vernier/middleware.rb +32 -0
- data/lib/vernier/output/firefox.rb +131 -49
- data/lib/vernier/result.rb +18 -1
- data/lib/vernier/stack_table.rb +42 -0
- data/lib/vernier/thread_names.rb +52 -0
- data/lib/vernier/version.rb +1 -1
- data/lib/vernier.rb +31 -17
- data/vernier.gemspec +5 -2
- metadata +42 -6
data/lib/vernier/autorun.rb
CHANGED
@@ -17,10 +17,11 @@ module Vernier
|
|
17
17
|
|
18
18
|
def self.start
|
19
19
|
interval = options.fetch(:interval, 500).to_i
|
20
|
+
allocation_sample_rate = options.fetch(:allocation_sample_rate, 0).to_i
|
20
21
|
|
21
22
|
STDERR.puts("starting profiler with interval #{interval}")
|
22
23
|
|
23
|
-
@collector = Vernier::Collector.new(:wall, interval:)
|
24
|
+
@collector = Vernier::Collector.new(:wall, interval:, allocation_sample_rate:)
|
24
25
|
@collector.start
|
25
26
|
end
|
26
27
|
|
data/lib/vernier/collector.rb
CHANGED
@@ -1,12 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "marker"
|
4
|
+
require_relative "thread_names"
|
4
5
|
|
5
6
|
module Vernier
|
6
7
|
class Collector
|
7
|
-
def initialize(mode)
|
8
|
+
def initialize(mode, options = {})
|
9
|
+
if options.fetch(:gc, true) && (mode == :retained)
|
10
|
+
GC.start
|
11
|
+
end
|
12
|
+
|
8
13
|
@mode = mode
|
14
|
+
@out = options[:out]
|
15
|
+
|
9
16
|
@markers = []
|
17
|
+
@hooks = []
|
18
|
+
|
19
|
+
@thread_names = ThreadNames.new
|
20
|
+
|
21
|
+
if options[:hooks]
|
22
|
+
Array(options[:hooks]).each do |hook|
|
23
|
+
add_hook(hook)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
@hooks.each do |hook|
|
27
|
+
hook.enable
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private def add_hook(hook)
|
32
|
+
case hook
|
33
|
+
when :rails, :activesupport
|
34
|
+
@hooks << Vernier::Hooks::ActiveSupport.new(self)
|
35
|
+
else
|
36
|
+
warn "Unknown hook: #{hook}"
|
37
|
+
end
|
10
38
|
end
|
11
39
|
|
12
40
|
##
|
@@ -47,6 +75,19 @@ module Vernier
|
|
47
75
|
def stop
|
48
76
|
result = finish
|
49
77
|
|
78
|
+
result.instance_variable_set("@stack_table", stack_table.to_h)
|
79
|
+
@thread_names.finish
|
80
|
+
|
81
|
+
@hooks.each do |hook|
|
82
|
+
hook.disable
|
83
|
+
end
|
84
|
+
|
85
|
+
result.threads.each do |obj_id, thread|
|
86
|
+
thread[:name] ||= @thread_names[obj_id]
|
87
|
+
end
|
88
|
+
|
89
|
+
result.hooks = @hooks
|
90
|
+
|
50
91
|
end_time = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
|
51
92
|
result.pid = Process.pid
|
52
93
|
result.end_time = end_time
|
@@ -65,6 +106,10 @@ module Vernier
|
|
65
106
|
|
66
107
|
result.instance_variable_set(:@markers, markers)
|
67
108
|
|
109
|
+
if @out
|
110
|
+
result.write(out: @out)
|
111
|
+
end
|
112
|
+
|
68
113
|
result
|
69
114
|
end
|
70
115
|
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vernier
|
4
|
+
module Hooks
|
5
|
+
class ActiveSupport
|
6
|
+
FIREFOX_MARKER_SCHEMA = Ractor.make_shareable([
|
7
|
+
{
|
8
|
+
name: "sql.active_record",
|
9
|
+
display: [ "marker-chart", "marker-table" ],
|
10
|
+
data: [
|
11
|
+
{ key: "sql", format: "string" },
|
12
|
+
{ key: "name", format: "string" },
|
13
|
+
{ key: "type_casted_binds", label: "binds", format: "string"
|
14
|
+
}
|
15
|
+
]
|
16
|
+
},
|
17
|
+
{
|
18
|
+
name: "instantiation.active_record",
|
19
|
+
display: [ "marker-chart", "marker-table" ],
|
20
|
+
data: [
|
21
|
+
{ key: "record_count", format: "integer" },
|
22
|
+
{ key: "class_name", format: "string" }
|
23
|
+
]
|
24
|
+
},
|
25
|
+
{
|
26
|
+
name: "process_action.action_controller",
|
27
|
+
display: [ "marker-chart", "marker-table" ],
|
28
|
+
data: [
|
29
|
+
{ key: "controller", format: "string" },
|
30
|
+
{ key: "action", format: "string" },
|
31
|
+
{ key: "status", format: "integer" },
|
32
|
+
{ key: "path", format: "string" },
|
33
|
+
{ key: "method", format: "string" }
|
34
|
+
]
|
35
|
+
},
|
36
|
+
{
|
37
|
+
name: "cache_read.active_support",
|
38
|
+
display: [ "marker-chart", "marker-table" ],
|
39
|
+
data: [
|
40
|
+
{ key: "key", format: "string" },
|
41
|
+
{ key: "store", format: "string" },
|
42
|
+
{ key: "hit", format: "string" },
|
43
|
+
{ key: "super_operation", format: "string" }
|
44
|
+
]
|
45
|
+
},
|
46
|
+
{
|
47
|
+
name: "cache_read_multi.active_support",
|
48
|
+
display: [ "marker-chart", "marker-table" ],
|
49
|
+
data: [
|
50
|
+
{ key: "key", format: "string" },
|
51
|
+
{ key: "store", format: "string" },
|
52
|
+
{ key: "hit", format: "string" },
|
53
|
+
{ key: "super_operation", format: "string" }
|
54
|
+
]
|
55
|
+
},
|
56
|
+
{
|
57
|
+
name: "cache_fetch_hit.active_support",
|
58
|
+
display: [ "marker-chart", "marker-table" ],
|
59
|
+
data: [
|
60
|
+
{ key: "key", format: "string" },
|
61
|
+
{ key: "store", format: "string" }
|
62
|
+
]
|
63
|
+
}
|
64
|
+
])
|
65
|
+
|
66
|
+
SERIALIZED_KEYS = FIREFOX_MARKER_SCHEMA.map do |format|
|
67
|
+
[
|
68
|
+
format[:name],
|
69
|
+
format[:data].map { _1[:key].to_sym }.freeze
|
70
|
+
]
|
71
|
+
end.to_h.freeze
|
72
|
+
|
73
|
+
def initialize(collector)
|
74
|
+
@collector = collector
|
75
|
+
end
|
76
|
+
|
77
|
+
def enable
|
78
|
+
require "active_support"
|
79
|
+
@subscription = ::ActiveSupport::Notifications.monotonic_subscribe(/\A[^!]/) do |name, start, finish, id, payload|
|
80
|
+
# Notifications.publish API may reach here without proper timing information included
|
81
|
+
unless Float === start && Float === finish
|
82
|
+
next
|
83
|
+
end
|
84
|
+
|
85
|
+
data = { type: name }
|
86
|
+
if keys = SERIALIZED_KEYS[name]
|
87
|
+
keys.each do |key|
|
88
|
+
data[key] = payload[key]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
@collector.add_marker(
|
92
|
+
name: name,
|
93
|
+
start: (start * 1_000_000_000.0).to_i,
|
94
|
+
finish: (finish * 1_000_000_000.0).to_i,
|
95
|
+
data: data
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def disable
|
101
|
+
::ActiveSupport::Notifications.unsubscribe(@subscription)
|
102
|
+
@subscription = nil
|
103
|
+
end
|
104
|
+
|
105
|
+
def firefox_marker_schema
|
106
|
+
FIREFOX_MARKER_SCHEMA
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Vernier
|
2
|
+
class Middleware
|
3
|
+
def initialize(app, permit: ->(_) { true })
|
4
|
+
@app = app
|
5
|
+
@permit = permit
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
request = Rack::Request.new(env)
|
10
|
+
return @app.call(env) unless request.GET.has_key?("vernier")
|
11
|
+
|
12
|
+
permitted = @permit.call(request)
|
13
|
+
return @app.call(env) unless permitted
|
14
|
+
|
15
|
+
interval = request.GET.fetch(:vernier_interval, 200).to_i
|
16
|
+
allocation_sample_rate = request.GET.fetch(:vernier_allocation_sample_rate, 200).to_i
|
17
|
+
|
18
|
+
result = Vernier.trace(interval:, allocation_sample_rate:, hooks: [:rails]) do
|
19
|
+
@app.call(env)
|
20
|
+
end
|
21
|
+
body = result.to_gecko
|
22
|
+
filename = "#{request.path.gsub("/", "_")}_#{DateTime.now.strftime("%Y-%m-%d-%H-%M-%S")}.vernier.json"
|
23
|
+
headers = {
|
24
|
+
"Content-Type" => "application/json; charset=utf-8",
|
25
|
+
"Content-Disposition" => "attachment; filename=\"#{filename}\"",
|
26
|
+
"Content-Length" => body.bytesize.to_s
|
27
|
+
}
|
28
|
+
|
29
|
+
Rack::Response.new(body, 200, headers).finish
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "json"
|
4
|
+
require "rbconfig"
|
4
5
|
|
5
6
|
module Vernier
|
6
7
|
module Output
|
@@ -89,7 +90,7 @@ module Vernier
|
|
89
90
|
end
|
90
91
|
|
91
92
|
def output
|
92
|
-
::JSON.
|
93
|
+
::JSON.fast_generate(data)
|
93
94
|
end
|
94
95
|
|
95
96
|
private
|
@@ -133,7 +134,8 @@ module Vernier
|
|
133
134
|
color: category.color,
|
134
135
|
subcategories: []
|
135
136
|
}
|
136
|
-
end
|
137
|
+
end,
|
138
|
+
sourceCodeIsNotOnSearchfox: true
|
137
139
|
},
|
138
140
|
libs: [],
|
139
141
|
threads: thread_data
|
@@ -141,9 +143,15 @@ module Vernier
|
|
141
143
|
end
|
142
144
|
|
143
145
|
def marker_schema
|
146
|
+
hook_additions = profile.hooks.flat_map do |hook|
|
147
|
+
if hook.respond_to?(:firefox_marker_schema)
|
148
|
+
hook.firefox_marker_schema
|
149
|
+
end
|
150
|
+
end.compact
|
151
|
+
|
144
152
|
[
|
145
153
|
{
|
146
|
-
name: "
|
154
|
+
name: "THREAD_RUNNING",
|
147
155
|
display: [ "marker-chart", "marker-table" ],
|
148
156
|
data: [
|
149
157
|
{
|
@@ -151,19 +159,57 @@ module Vernier
|
|
151
159
|
value: "The thread has acquired the GVL and is executing"
|
152
160
|
}
|
153
161
|
]
|
154
|
-
}
|
162
|
+
},
|
163
|
+
{
|
164
|
+
name: "THREAD_STALLED",
|
165
|
+
display: [ "marker-chart", "marker-table" ],
|
166
|
+
data: [
|
167
|
+
{
|
168
|
+
label: "Description",
|
169
|
+
value: "The thread is ready, but stalled waiting for the GVL to be available"
|
170
|
+
}
|
171
|
+
]
|
172
|
+
},
|
173
|
+
{
|
174
|
+
name: "THREAD_SUSPENDED",
|
175
|
+
display: [ "marker-chart", "marker-table" ],
|
176
|
+
data: [
|
177
|
+
{
|
178
|
+
label: "Description",
|
179
|
+
value: "The thread has voluntarily released the GVL (ex. to sleep, for I/O, waiting on a lock)"
|
180
|
+
}
|
181
|
+
]
|
182
|
+
},
|
183
|
+
{
|
184
|
+
name: "GC_PAUSE",
|
185
|
+
display: [ "marker-chart", "marker-table", "timeline-overview" ],
|
186
|
+
tooltipLabel: "{marker.name} - {marker.data.state}",
|
187
|
+
data: [
|
188
|
+
{
|
189
|
+
label: "Description",
|
190
|
+
value: "All threads are paused as GC is performed"
|
191
|
+
}
|
192
|
+
]
|
193
|
+
},
|
194
|
+
*hook_additions
|
155
195
|
]
|
156
196
|
end
|
157
197
|
|
158
198
|
class Thread
|
159
199
|
attr_reader :profile
|
160
200
|
|
161
|
-
def initialize(ruby_thread_id, profile, categorizer, name:, tid:, samples:, weights:, timestamps: nil, sample_categories: nil, markers:, started_at:, stopped_at: nil)
|
201
|
+
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)
|
162
202
|
@ruby_thread_id = ruby_thread_id
|
163
203
|
@profile = profile
|
164
204
|
@categorizer = categorizer
|
165
205
|
@tid = tid
|
166
|
-
@
|
206
|
+
@allocations = allocations
|
207
|
+
@name = name
|
208
|
+
@is_main = is_main
|
209
|
+
if is_main.nil?
|
210
|
+
@is_main = @ruby_thread_id == ::Thread.main.object_id
|
211
|
+
end
|
212
|
+
@is_main = true if profile.threads.size == 1
|
167
213
|
|
168
214
|
timestamps ||= [0] * samples.size
|
169
215
|
@samples, @weights, @timestamps = samples, weights, timestamps
|
@@ -184,41 +230,67 @@ module Vernier
|
|
184
230
|
@func_names = names.map do |name|
|
185
231
|
@strings[name]
|
186
232
|
end
|
187
|
-
|
233
|
+
|
234
|
+
@filenames = filter_filenames(filenames).map do |filename|
|
188
235
|
@strings[filename]
|
189
236
|
end
|
190
237
|
|
191
238
|
lines = profile.frame_table.fetch(:line)
|
192
239
|
|
193
|
-
|
240
|
+
func_implementations = filenames.map do |filename|
|
194
241
|
# Must match strings in `src/profile-logic/profile-data.js`
|
195
242
|
# inside the firefox profiler. See `getFriendlyStackTypeName`
|
196
243
|
if filename == "<cfunc>"
|
197
244
|
@strings["native"]
|
198
245
|
else
|
199
|
-
#
|
200
|
-
|
201
|
-
@strings["yjit"]
|
202
|
-
else
|
203
|
-
# nil means interpreter
|
204
|
-
nil
|
205
|
-
end
|
246
|
+
# nil means interpreter
|
247
|
+
nil
|
206
248
|
end
|
207
249
|
end
|
250
|
+
@frame_implementations = profile.frame_table.fetch(:func).map do |func_idx|
|
251
|
+
func_implementations[func_idx]
|
252
|
+
end
|
208
253
|
|
209
|
-
|
254
|
+
func_categories = filenames.map do |filename|
|
210
255
|
@categorizer.categorize(filename)
|
211
256
|
end
|
257
|
+
@frame_categories = profile.frame_table.fetch(:func).map do |func_idx|
|
258
|
+
func_categories[func_idx]
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def filter_filenames(filenames)
|
263
|
+
pwd = "#{Dir.pwd}/"
|
264
|
+
gem_regex = %r{\A#{Regexp.union(Gem.path)}/gems/}
|
265
|
+
gem_match_regex = %r{\A#{Regexp.union(Gem.path)}/gems/([a-zA-Z](?:[a-zA-Z0-9\.\_]|-[a-zA-Z])*)-([0-9][0-9A-Za-z\-_\.]*)/(.*)\z}
|
266
|
+
rubylibdir = "#{RbConfig::CONFIG["rubylibdir"]}/"
|
267
|
+
|
268
|
+
filenames.map do |filename|
|
269
|
+
if filename.match?(gem_regex)
|
270
|
+
gem_match_regex =~ filename
|
271
|
+
"gem:#$1-#$2:#$3"
|
272
|
+
elsif filename.start_with?(pwd)
|
273
|
+
filename.delete_prefix(pwd)
|
274
|
+
elsif filename.start_with?(rubylibdir)
|
275
|
+
path = filename.delete_prefix(rubylibdir)
|
276
|
+
"rubylib:#{RUBY_VERSION}:#{path}"
|
277
|
+
else
|
278
|
+
filename
|
279
|
+
end
|
280
|
+
end
|
212
281
|
end
|
213
282
|
|
214
283
|
def data
|
284
|
+
started_at = (@started_at - 0) / 1_000_000.0
|
285
|
+
stopped_at = (@stopped_at - 0) / 1_000_000.0 if @stopped_at
|
286
|
+
|
215
287
|
{
|
216
288
|
name: @name,
|
217
|
-
isMainThread: @
|
218
|
-
processStartupTime:
|
219
|
-
processShutdownTime:
|
220
|
-
registerTime:
|
221
|
-
unregisterTime:
|
289
|
+
isMainThread: @is_main,
|
290
|
+
processStartupTime: started_at,
|
291
|
+
processShutdownTime: stopped_at,
|
292
|
+
registerTime: started_at,
|
293
|
+
unregisterTime: stopped_at,
|
222
294
|
pausedRanges: [],
|
223
295
|
pid: profile.pid || Process.pid,
|
224
296
|
tid: @tid,
|
@@ -226,6 +298,7 @@ module Vernier
|
|
226
298
|
funcTable: func_table,
|
227
299
|
nativeSymbols: {},
|
228
300
|
samples: samples_table,
|
301
|
+
jsAllocations: allocations_table,
|
229
302
|
stackTable: stack_table,
|
230
303
|
resourceTable: {
|
231
304
|
length: 0,
|
@@ -236,7 +309,7 @@ module Vernier
|
|
236
309
|
},
|
237
310
|
markers: markers_table,
|
238
311
|
stringArray: string_table
|
239
|
-
}
|
312
|
+
}.compact
|
240
313
|
end
|
241
314
|
|
242
315
|
def markers_table
|
@@ -255,12 +328,14 @@ module Vernier
|
|
255
328
|
end_times << (finish&./(1_000_000.0))
|
256
329
|
phases << phase
|
257
330
|
|
258
|
-
category =
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
331
|
+
category =
|
332
|
+
if name.start_with?("GC")
|
333
|
+
gc_category.idx
|
334
|
+
elsif name.start_with?("Thread")
|
335
|
+
thread_category.idx
|
336
|
+
else
|
337
|
+
0
|
338
|
+
end
|
264
339
|
|
265
340
|
categories << category
|
266
341
|
data << datum
|
@@ -277,6 +352,25 @@ module Vernier
|
|
277
352
|
}
|
278
353
|
end
|
279
354
|
|
355
|
+
def allocations_table
|
356
|
+
return nil if !@allocations
|
357
|
+
samples, weights, timestamps = @allocations.values_at(:samples, :weights, :timestamps)
|
358
|
+
return nil if samples.size == 0
|
359
|
+
size = samples.size
|
360
|
+
timestamps = timestamps.map { _1 / 1_000_000.0 }
|
361
|
+
ret = {
|
362
|
+
"time": timestamps,
|
363
|
+
"className": ["Object"]*size,
|
364
|
+
"typeName": ["JSObject"]*size,
|
365
|
+
"coarseType": ["Object"]*size,
|
366
|
+
"weight": weights,
|
367
|
+
"inNursery": [false] * size,
|
368
|
+
"stack": samples,
|
369
|
+
"length": size
|
370
|
+
}
|
371
|
+
ret
|
372
|
+
end
|
373
|
+
|
280
374
|
def samples_table
|
281
375
|
samples = @samples
|
282
376
|
weights = @weights
|
@@ -366,14 +460,21 @@ module Vernier
|
|
366
460
|
|
367
461
|
cfunc_idx = @strings["<cfunc>"]
|
368
462
|
is_js = @filenames.map { |fn| fn != cfunc_idx }
|
463
|
+
line_numbers = profile.func_table.fetch(:first_line).map.with_index do |line, i|
|
464
|
+
if is_js[i] || line != 0
|
465
|
+
line
|
466
|
+
else
|
467
|
+
nil
|
468
|
+
end
|
469
|
+
end
|
369
470
|
{
|
370
471
|
name: @func_names,
|
371
472
|
isJS: is_js,
|
372
473
|
relevantForJS: is_js,
|
373
474
|
resource: [-1] * size, # set to unidentified for now
|
374
475
|
fileName: @filenames,
|
375
|
-
lineNumber:
|
376
|
-
columnNumber: [
|
476
|
+
lineNumber: line_numbers,
|
477
|
+
columnNumber: [nil] * size,
|
377
478
|
#columnNumber: functions.map { _1.column },
|
378
479
|
length: size
|
379
480
|
}
|
@@ -385,25 +486,6 @@ module Vernier
|
|
385
486
|
|
386
487
|
private
|
387
488
|
|
388
|
-
def pretty_name(name)
|
389
|
-
if name.empty?
|
390
|
-
begin
|
391
|
-
tr = ObjectSpace._id2ref(@ruby_thread_id)
|
392
|
-
name = tr.inspect if tr
|
393
|
-
rescue RangeError
|
394
|
-
# Thread was already GC'd
|
395
|
-
end
|
396
|
-
end
|
397
|
-
return name unless name.start_with?("#<Thread")
|
398
|
-
pretty = []
|
399
|
-
obj_address = name[/Thread:(0x\w+)/,1]
|
400
|
-
best_id = name[/\#<Thread:0x\w+@?\s?(.*)\s+\S+>/,1] || ""
|
401
|
-
Gem.path.each { |gem_dir| best_id = best_id.gsub(gem_dir, "...") }
|
402
|
-
pretty << best_id unless best_id.empty?
|
403
|
-
pretty << "(#{obj_address})"
|
404
|
-
pretty.join(' ')
|
405
|
-
end
|
406
|
-
|
407
489
|
def gc_category
|
408
490
|
@categorizer.get_category("GC")
|
409
491
|
end
|
data/lib/vernier/result.rb
CHANGED
@@ -1,12 +1,29 @@
|
|
1
1
|
module Vernier
|
2
2
|
class Result
|
3
|
-
|
3
|
+
def stack_table
|
4
|
+
@stack_table[:stack_table]
|
5
|
+
end
|
6
|
+
|
7
|
+
def frame_table
|
8
|
+
@stack_table[:frame_table]
|
9
|
+
end
|
10
|
+
|
11
|
+
def func_table
|
12
|
+
@stack_table[:func_table]
|
13
|
+
end
|
14
|
+
|
4
15
|
attr_reader :markers
|
5
16
|
|
17
|
+
attr_accessor :hooks
|
18
|
+
|
6
19
|
attr_accessor :pid, :end_time
|
7
20
|
attr_accessor :threads
|
8
21
|
attr_accessor :meta
|
9
22
|
|
23
|
+
def main_thread
|
24
|
+
threads.values.detect {|x| x[:is_main] }
|
25
|
+
end
|
26
|
+
|
10
27
|
# TODO: remove these
|
11
28
|
def weights; threads.values.flat_map { _1[:weights] }; end
|
12
29
|
def samples; threads.values.flat_map { _1[:samples] }; end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Vernier
|
2
|
+
class StackTable
|
3
|
+
def to_h
|
4
|
+
{
|
5
|
+
stack_table: {
|
6
|
+
parent: stack_count.times.map { stack_parent_idx(_1) },
|
7
|
+
frame: stack_count.times.map { stack_frame_idx(_1) }
|
8
|
+
},
|
9
|
+
frame_table: {
|
10
|
+
func: frame_count.times.map { frame_func_idx(_1) },
|
11
|
+
line: frame_count.times.map { frame_line_no(_1) }
|
12
|
+
},
|
13
|
+
func_table: {
|
14
|
+
name: func_count.times.map { func_name(_1) },
|
15
|
+
filename: func_count.times.map { func_filename(_1) },
|
16
|
+
first_line: func_count.times.map { func_first_lineno(_1) }
|
17
|
+
}
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def backtrace(stack_idx)
|
22
|
+
full_stack(stack_idx).map do |stack_idx|
|
23
|
+
frame_idx = stack_frame_idx(stack_idx)
|
24
|
+
func_idx = frame_func_idx(frame_idx)
|
25
|
+
line = frame_line_no(frame_idx)
|
26
|
+
name = func_name(func_idx);
|
27
|
+
filename = func_filename(func_idx);
|
28
|
+
|
29
|
+
"#{filename}:#{line}:in '#{name}'"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def full_stack(stack_idx)
|
34
|
+
full_stack = []
|
35
|
+
while stack_idx
|
36
|
+
full_stack << stack_idx
|
37
|
+
stack_idx = stack_parent_idx(stack_idx)
|
38
|
+
end
|
39
|
+
full_stack
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Vernier
|
2
|
+
# Collects names of all seen threads
|
3
|
+
class ThreadNames
|
4
|
+
def initialize
|
5
|
+
@names = {}
|
6
|
+
@tp = TracePoint.new(:thread_end) do |e|
|
7
|
+
collect_thread(e.self)
|
8
|
+
end
|
9
|
+
@tp.enable
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](object_id)
|
13
|
+
@names[object_id] || "unknown thread #{object_id}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def finish
|
17
|
+
collect_running
|
18
|
+
@tp.disable
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def collect_running
|
24
|
+
Thread.list.each do |th|
|
25
|
+
collect_thread(th)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def collect_thread(th)
|
30
|
+
@names[th.object_id] = pretty_name(th)
|
31
|
+
end
|
32
|
+
|
33
|
+
def pretty_name(thread)
|
34
|
+
name = thread.name
|
35
|
+
return name if name && !name.empty?
|
36
|
+
|
37
|
+
if thread == Thread.main
|
38
|
+
return "main"
|
39
|
+
end
|
40
|
+
|
41
|
+
name = Thread.instance_method(:inspect).bind_call(thread)
|
42
|
+
pretty = []
|
43
|
+
best_id = name[/\#<Thread:0x\w+@?\s?(.*)\s+\S+>/, 1]
|
44
|
+
if best_id
|
45
|
+
Gem.path.each { |gem_dir| best_id.gsub!(gem_dir, "") }
|
46
|
+
pretty << best_id unless best_id.empty?
|
47
|
+
end
|
48
|
+
pretty << "(#{thread.object_id})"
|
49
|
+
pretty.join(' ')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/vernier/version.rb
CHANGED