vernier 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,10 +18,11 @@ module Vernier
18
18
  def self.start
19
19
  interval = options.fetch(:interval, 500).to_i
20
20
  allocation_sample_rate = options.fetch(:allocation_sample_rate, 0).to_i
21
+ hooks = options.fetch(:hooks, "").split(",")
21
22
 
22
23
  STDERR.puts("starting profiler with interval #{interval}")
23
24
 
24
- @collector = Vernier::Collector.new(:wall, interval:, allocation_sample_rate:)
25
+ @collector = Vernier::Collector.new(:wall, interval:, allocation_sample_rate:, hooks:)
25
26
  @collector.start
26
27
  end
27
28
 
@@ -5,8 +5,14 @@ require_relative "thread_names"
5
5
 
6
6
  module Vernier
7
7
  class Collector
8
- def initialize(mode, options={})
8
+ def initialize(mode, options = {})
9
+ if options.fetch(:gc, true) && (mode == :retained)
10
+ GC.start
11
+ end
12
+
9
13
  @mode = mode
14
+ @out = options[:out]
15
+
10
16
  @markers = []
11
17
  @hooks = []
12
18
 
@@ -23,11 +29,11 @@ module Vernier
23
29
  end
24
30
 
25
31
  private def add_hook(hook)
26
- case hook
32
+ case hook.to_sym
27
33
  when :rails, :activesupport
28
34
  @hooks << Vernier::Hooks::ActiveSupport.new(self)
29
35
  else
30
- warn "Unknown hook: #{hook}"
36
+ warn "Unknown hook: #{hook.inspect}"
31
37
  end
32
38
  end
33
39
 
@@ -69,6 +75,7 @@ module Vernier
69
75
  def stop
70
76
  result = finish
71
77
 
78
+ result.instance_variable_set("@stack_table", stack_table.to_h)
72
79
  @thread_names.finish
73
80
 
74
81
  @hooks.each do |hook|
@@ -99,6 +106,10 @@ module Vernier
99
106
 
100
107
  result.instance_variable_set(:@markers, markers)
101
108
 
109
+ if @out
110
+ result.write(out: @out)
111
+ end
112
+
102
113
  result
103
114
  end
104
115
  end
@@ -7,9 +7,12 @@ module Vernier
7
7
  {
8
8
  name: "sql.active_record",
9
9
  display: [ "marker-chart", "marker-table" ],
10
+ tooltipLabel: "{marker.data.name}",
11
+ chartLabel: "{marker.data.name}",
12
+ tableLabel: "{marker.data.sql}",
10
13
  data: [
11
- { key: "sql", format: "string" },
12
- { key: "name", format: "string" },
14
+ { key: "sql", format: "string", searchable: true },
15
+ { key: "name", format: "string", searchable: true },
13
16
  { key: "type_casted_binds", label: "binds", format: "string"
14
17
  }
15
18
  ]
@@ -17,25 +20,50 @@ module Vernier
17
20
  {
18
21
  name: "instantiation.active_record",
19
22
  display: [ "marker-chart", "marker-table" ],
23
+ tooltipLabel: "{marker.data.record_count} × {marker.data.class_name}",
24
+ chartLabel: "{marker.data.record_count} × {marker.data.class_name}",
25
+ tableLabel: "Instantiate {marker.data.record_count} × {marker.data.class_name}",
20
26
  data: [
21
27
  { key: "record_count", format: "integer" },
22
28
  { key: "class_name", format: "string" }
23
29
  ]
24
30
  },
31
+ {
32
+ name: "start_processing.action_controller",
33
+ display: [ "marker-chart", "marker-table" ],
34
+ tooltipLabel: '{marker.data.method} {marker.data.controller}#{marker.data.action}',
35
+ chartLabel: '{marker.data.method} {marker.data.controller}#{marker.data.action}',
36
+ tableLabel: '{marker.data.method} {marker.data.controller}#{marker.data.action}',
37
+ data: [
38
+ { key: "controller", format: "string" },
39
+ { key: "action", format: "string" },
40
+ { key: "status", format: "integer" },
41
+ { key: "path", format: "string" },
42
+ { key: "method", format: "string" },
43
+ { key: "format", format: "string" }
44
+ ]
45
+ },
25
46
  {
26
47
  name: "process_action.action_controller",
27
48
  display: [ "marker-chart", "marker-table" ],
49
+ tooltipLabel: '{marker.data.method} {marker.data.controller}#{marker.data.action}',
50
+ chartLabel: '{marker.data.method} {marker.data.controller}#{marker.data.action}',
51
+ tableLabel: '{marker.data.method} {marker.data.controller}#{marker.data.action}',
28
52
  data: [
29
53
  { key: "controller", format: "string" },
30
54
  { key: "action", format: "string" },
31
55
  { key: "status", format: "integer" },
32
56
  { key: "path", format: "string" },
33
- { key: "method", format: "string" }
57
+ { key: "method", format: "string" },
58
+ { key: "format", format: "string" }
34
59
  ]
35
60
  },
36
61
  {
37
62
  name: "cache_read.active_support",
38
63
  display: [ "marker-chart", "marker-table" ],
64
+ tooltipLabel: '{marker.data.super_operation} {marker.data.key}',
65
+ chartLabel: '{marker.data.super_operation} {marker.data.key}',
66
+ tableLabel: '{marker.data.super_operation} {marker.data.key}',
39
67
  data: [
40
68
  { key: "key", format: "string" },
41
69
  { key: "store", format: "string" },
@@ -55,11 +83,65 @@ module Vernier
55
83
  },
56
84
  {
57
85
  name: "cache_fetch_hit.active_support",
86
+ tooltipLabel: 'HIT {marker.data.key}',
87
+ chartLabel: 'HIT {marker.data.key}',
88
+ tableLabel: 'HIT {marker.data.key}',
58
89
  display: [ "marker-chart", "marker-table" ],
59
90
  data: [
60
91
  { key: "key", format: "string" },
61
92
  { key: "store", format: "string" }
62
93
  ]
94
+ },
95
+ {
96
+ name: "render_template.action_view",
97
+ display: [ "marker-chart", "marker-table" ],
98
+ tooltipLabel: '{marker.data.identifier}',
99
+ chartLabel: '{marker.data.identifier}',
100
+ tableLabel: '{marker.data.identifier}',
101
+ data: [
102
+ { key: "identifier", format: "string" }
103
+ ]
104
+ },
105
+ {
106
+ name: "render_layout.action_view",
107
+ display: [ "marker-chart", "marker-table" ],
108
+ tooltipLabel: '{marker.data.identifier}',
109
+ chartLabel: '{marker.data.identifier}',
110
+ tableLabel: '{marker.data.identifier}',
111
+ data: [
112
+ { key: "identifier", format: "string" }
113
+ ]
114
+ },
115
+ {
116
+ name: "render_partial.action_view",
117
+ display: [ "marker-chart", "marker-table" ],
118
+ tooltipLabel: '{marker.data.identifier}',
119
+ chartLabel: '{marker.data.identifier}',
120
+ tableLabel: '{marker.data.identifier}',
121
+ data: [
122
+ { key: "identifier", format: "string" }
123
+ ]
124
+ },
125
+ {
126
+ name: "render_collection.action_view",
127
+ display: [ "marker-chart", "marker-table" ],
128
+ tooltipLabel: '{marker.data.identifier}',
129
+ chartLabel: '{marker.data.identifier}',
130
+ tableLabel: '{marker.data.identifier}',
131
+ data: [
132
+ { key: "identifier", format: "string" },
133
+ { key: "count", format: "integer" }
134
+ ]
135
+ },
136
+ {
137
+ name: "load_config_initializer.railties",
138
+ display: [ "marker-chart", "marker-table" ],
139
+ tooltipLabel: '{marker.data.initializer}',
140
+ chartLabel: '{marker.data.initializer}',
141
+ tableLabel: '{marker.data.initializer}',
142
+ data: [
143
+ { key: "initializer", format: "string" }
144
+ ]
63
145
  }
64
146
  ])
65
147
 
@@ -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
@@ -90,7 +90,7 @@ module Vernier
90
90
  end
91
91
 
92
92
  def output
93
- ::JSON.generate(data)
93
+ ::JSON.fast_generate(data)
94
94
  end
95
95
 
96
96
  private
@@ -152,7 +152,7 @@ module Vernier
152
152
  [
153
153
  {
154
154
  name: "THREAD_RUNNING",
155
- display: [ "marker-chart", "marker-table" ],
155
+ display: [ "marker-chart" ],
156
156
  data: [
157
157
  {
158
158
  label: "Description",
@@ -162,7 +162,7 @@ module Vernier
162
162
  },
163
163
  {
164
164
  name: "THREAD_STALLED",
165
- display: [ "marker-chart", "marker-table" ],
165
+ display: [ "marker-chart" ],
166
166
  data: [
167
167
  {
168
168
  label: "Description",
@@ -172,7 +172,7 @@ module Vernier
172
172
  },
173
173
  {
174
174
  name: "THREAD_SUSPENDED",
175
- display: [ "marker-chart", "marker-table" ],
175
+ display: [ "marker-chart" ],
176
176
  data: [
177
177
  {
178
178
  label: "Description",
@@ -237,25 +237,26 @@ module Vernier
237
237
 
238
238
  lines = profile.frame_table.fetch(:line)
239
239
 
240
- @frame_implementations = filenames.zip(lines).map do |filename, line|
240
+ func_implementations = filenames.map do |filename|
241
241
  # Must match strings in `src/profile-logic/profile-data.js`
242
242
  # inside the firefox profiler. See `getFriendlyStackTypeName`
243
243
  if filename == "<cfunc>"
244
244
  @strings["native"]
245
245
  else
246
- # FIXME: We need to get upstream support for JIT frames
247
- if line == -1
248
- @strings["yjit"]
249
- else
250
- # nil means interpreter
251
- nil
252
- end
246
+ # nil means interpreter
247
+ nil
253
248
  end
254
249
  end
250
+ @frame_implementations = profile.frame_table.fetch(:func).map do |func_idx|
251
+ func_implementations[func_idx]
252
+ end
255
253
 
256
- @frame_categories = filenames.map do |filename|
254
+ func_categories = filenames.map do |filename|
257
255
  @categorizer.categorize(filename)
258
256
  end
257
+ @frame_categories = profile.frame_table.fetch(:func).map do |func_idx|
258
+ func_categories[func_idx]
259
+ end
259
260
  end
260
261
 
261
262
  def filter_filenames(filenames)
@@ -280,13 +281,16 @@ module Vernier
280
281
  end
281
282
 
282
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
+
283
287
  {
284
288
  name: @name,
285
289
  isMainThread: @is_main,
286
- processStartupTime: 0, # FIXME
287
- processShutdownTime: nil, # FIXME
288
- registerTime: (@started_at - 0) / 1_000_000.0,
289
- unregisterTime: ((@stopped_at - 0) / 1_000_000.0 if @stopped_at),
290
+ processStartupTime: started_at,
291
+ processShutdownTime: stopped_at,
292
+ registerTime: started_at,
293
+ unregisterTime: stopped_at,
290
294
  pausedRanges: [],
291
295
  pid: profile.pid || Process.pid,
292
296
  tid: @tid,
@@ -324,12 +328,14 @@ module Vernier
324
328
  end_times << (finish&./(1_000_000.0))
325
329
  phases << phase
326
330
 
327
- category = case name
328
- when /\AGC/ then gc_category.idx
329
- when /\AThread/ then thread_category.idx
330
- else
331
- 0
332
- 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
333
339
 
334
340
  categories << category
335
341
  data << datum
@@ -1,6 +1,17 @@
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
 
6
17
  attr_accessor :hooks
@@ -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
@@ -35,7 +35,7 @@ module Vernier
35
35
  return name if name && !name.empty?
36
36
 
37
37
  if thread == Thread.main
38
- return "main"
38
+ return $0
39
39
  end
40
40
 
41
41
  name = Thread.instance_method(:inspect).bind_call(thread)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vernier
4
- VERSION = "0.6.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/vernier.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "vernier/version"
4
4
  require_relative "vernier/collector"
5
+ require_relative "vernier/stack_table"
5
6
  require_relative "vernier/result"
6
7
  require_relative "vernier/hooks"
7
8
  require_relative "vernier/vernier"
@@ -11,7 +12,9 @@ require_relative "vernier/output/top"
11
12
  module Vernier
12
13
  class Error < StandardError; end
13
14
 
14
- def self.trace(mode: :wall, out: nil, **collector_options)
15
+ autoload :Middleware, "vernier/middleware"
16
+
17
+ def self.profile(mode: :wall, **collector_options)
15
18
  collector = Vernier::Collector.new(mode, collector_options)
16
19
  collector.start
17
20
 
@@ -20,33 +23,43 @@ module Vernier
20
23
  yield collector
21
24
  ensure
22
25
  result = collector.stop
23
- if out
24
- File.write(out, Output::Firefox.new(result).output)
25
- end
26
26
  end
27
27
 
28
28
  result
29
29
  end
30
30
 
31
- def self.trace_retained(out: nil, gc: true)
32
- 3.times { GC.start } if gc
31
+ class << self
32
+ alias_method :trace, :profile
33
+ alias_method :run, :profile
34
+ end
33
35
 
34
- collector = Vernier::Collector.new(:retained)
35
- collector.start
36
+ @collector = nil
36
37
 
37
- result = nil
38
- begin
39
- yield collector
40
- ensure
41
- result = collector.stop
42
- end
38
+ def self.start_profile(mode: :wall, **collector_options)
39
+ if @collector
40
+ @collector.stop
41
+ @collector = nil
43
42
 
44
- if out
45
- result.write(out:)
43
+ raise "Profile already started, stopping..."
46
44
  end
45
+
46
+ @collector = Vernier::Collector.new(mode, collector_options)
47
+ @collector.start
48
+ end
49
+
50
+ def self.stop_profile
51
+ raise "No profile started" unless @collector
52
+
53
+ result = @collector.stop
54
+ @collector = nil
55
+
47
56
  result
48
57
  end
49
58
 
59
+ def self.trace_retained(out: nil, gc: true, &block)
60
+ profile(mode: :retained, out:, gc:, &block)
61
+ end
62
+
50
63
  class Collector
51
64
  def self.new(mode, options = {})
52
65
  _new(mode, options)
data/vernier.gemspec CHANGED
@@ -31,4 +31,5 @@ Gem::Specification.new do |spec|
31
31
  spec.extensions = ["ext/vernier/extconf.rb"]
32
32
 
33
33
  spec.add_development_dependency "activesupport"
34
+ spec.add_development_dependency "rack"
34
35
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vernier
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Hawthorn
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-29 00:00:00.000000000 Z
11
+ date: 2024-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  description: Next-generation Ruby 3.2.1+ sampling profiler. Tracks multiple threads,
28
42
  GVL activity, GC pauses, idle time, and more.
29
43
  email:
@@ -42,6 +56,8 @@ files:
42
56
  - bin/console
43
57
  - bin/setup
44
58
  - bin/vernier
59
+ - examples/gvl_sleep.rb
60
+ - examples/measure_overhead.rb
45
61
  - examples/minitest.rb
46
62
  - examples/ractor.rb
47
63
  - examples/rails.rb
@@ -57,9 +73,11 @@ files:
57
73
  - lib/vernier/hooks.rb
58
74
  - lib/vernier/hooks/active_support.rb
59
75
  - lib/vernier/marker.rb
76
+ - lib/vernier/middleware.rb
60
77
  - lib/vernier/output/firefox.rb
61
78
  - lib/vernier/output/top.rb
62
79
  - lib/vernier/result.rb
80
+ - lib/vernier/stack_table.rb
63
81
  - lib/vernier/thread_names.rb
64
82
  - lib/vernier/version.rb
65
83
  - vernier.gemspec
@@ -85,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
85
103
  - !ruby/object:Gem::Version
86
104
  version: '0'
87
105
  requirements: []
88
- rubygems_version: 3.4.10
106
+ rubygems_version: 3.5.9
89
107
  signing_key:
90
108
  specification_version: 4
91
109
  summary: A next generation CRuby profiler