vernier 0.5.1 → 0.7.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 +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