vernier 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
 
@@ -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
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vernier
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.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.7.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-13 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.4.19
89
107
  signing_key:
90
108
  specification_version: 4
91
109
  summary: A next generation CRuby profiler