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.
@@ -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
 
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vernier
4
+ module Hooks
5
+ autoload :ActiveSupport, "vernier/hooks/active_support"
6
+ end
7
+ 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.generate(data)
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: "GVL_THREAD_RESUMED",
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
- @name = pretty_name(name)
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
- @filenames = filenames.map do |filename|
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
- @frame_implementations = filenames.zip(lines).map do |filename, line|
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
- # FIXME: We need to get upstream support for JIT frames
200
- if line == -1
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
- @frame_categories = filenames.map do |filename|
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: @ruby_thread_id == ::Thread.main.object_id || (profile.threads.size == 1),
218
- processStartupTime: 0, # FIXME
219
- processShutdownTime: nil, # FIXME
220
- registerTime: (@started_at - 0) / 1_000_000.0,
221
- unregisterTime: ((@stopped_at - 0) / 1_000_000.0 if @stopped_at),
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 = case name
259
- when /\AGC/ then gc_category.idx
260
- when /\AThread/ then thread_category.idx
261
- else
262
- 0
263
- end
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: profile.func_table.fetch(:first_line),
376
- columnNumber: [0] * size,
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
@@ -1,12 +1,29 @@
1
1
  module Vernier
2
2
  class Result
3
- attr_reader :stack_table, :frame_table, :func_table
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vernier
4
- VERSION = "0.5.1"
4
+ VERSION = "0.7.0"
5
5
  end