vernier 0.5.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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