vernier 1.8.1 → 1.9.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.
@@ -3,8 +3,15 @@
3
3
 
4
4
  #include "ruby.h"
5
5
 
6
+ // HACK: This isn't public, but the objspace ext uses it
7
+ extern "C" size_t rb_obj_memsize_of(VALUE);
8
+
9
+ #define sym(name) ID2SYM(rb_intern_const(name))
10
+
6
11
  extern VALUE rb_mVernier;
7
12
 
8
13
  void Init_memory();
14
+ void Init_stack_table();
15
+ void Init_heap_tracker();
9
16
 
10
17
  #endif /* VERNIER_H */
@@ -5,6 +5,110 @@ require_relative "thread_names"
5
5
 
6
6
  module Vernier
7
7
  class Collector
8
+ class CustomCollector < Collector
9
+ def initialize(mode, options)
10
+ @stack_table = StackTable.new
11
+
12
+ @samples = []
13
+ @timestamps = []
14
+
15
+ @started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
16
+ super
17
+ end
18
+
19
+ def sample
20
+ @samples << @stack_table.current_stack
21
+ @timestamps << Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
22
+ end
23
+
24
+ def start
25
+ end
26
+
27
+ def finish
28
+ result = Result.new
29
+ result.instance_variable_set(:@threads, {
30
+ 0 => {
31
+ tid: 0,
32
+ name: "custom",
33
+ started_at: @started_at,
34
+ samples: @samples,
35
+ weights: [1] * @samples.size,
36
+ timestamps: @timestamps,
37
+ sample_categories: [0] * @samples.size,
38
+ }
39
+ })
40
+ result.instance_variable_set(:@meta, {
41
+ started_at: @started_at
42
+ })
43
+ result
44
+ end
45
+ end
46
+
47
+ class RetainedCollector < Collector
48
+ def initialize(mode, options)
49
+ @stack_table = StackTable.new
50
+ @heap_tracker = HeapTracker.new(@stack_table)
51
+
52
+ @started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
53
+ super
54
+ end
55
+
56
+ def start
57
+ @heap_tracker.collect
58
+ end
59
+
60
+ def drain
61
+ @heap_tracker.drain
62
+ end
63
+
64
+ def finish
65
+ @heap_tracker.drain
66
+
67
+ GC.start
68
+
69
+ @stack_table.finalize
70
+
71
+ GC.start
72
+
73
+ @heap_tracker.lock
74
+ tracker_data = @heap_tracker.data
75
+
76
+ samples = tracker_data.fetch(:samples)
77
+ weights = tracker_data.fetch(:weights)
78
+
79
+ result = Result.new
80
+ result.instance_variable_set(:@threads, {
81
+ 0 => {
82
+ tid: 0,
83
+ name: "retained memory",
84
+ started_at: @started_at,
85
+ samples: samples,
86
+ weights: weights,
87
+ sample_categories: [0] * samples.size,
88
+ }
89
+ })
90
+ result.instance_variable_set(:@meta, {
91
+ started_at: @started_at
92
+ })
93
+ result
94
+ end
95
+ end
96
+
97
+ def self.new(mode, options = {})
98
+ return super unless Collector.equal?(self)
99
+
100
+ case mode
101
+ when :wall
102
+ TimeCollector.new(mode, options)
103
+ when :custom
104
+ CustomCollector.new(mode, options)
105
+ when :retained
106
+ RetainedCollector.new(mode, options)
107
+ else
108
+ raise ArgumentError, "invalid mode: #{mode.inspect}"
109
+ end
110
+ end
111
+
8
112
  def initialize(mode, options = {})
9
113
  @gc = options.fetch(:gc, true) && (mode == :retained)
10
114
  GC.start if @gc
@@ -30,14 +134,20 @@ module Vernier
30
134
  @user_metadata = options[:metadata] || {}
31
135
  end
32
136
 
137
+ attr_reader :stack_table
138
+
33
139
  private def add_hook(hook)
34
- case hook.to_sym
140
+ case hook.to_s.to_sym
35
141
  when :rails, :activesupport
36
142
  @hooks << Vernier::Hooks::ActiveSupport.new(self)
37
143
  when :memory_usage
38
144
  @hooks << Vernier::Hooks::MemoryUsage.new(self)
39
145
  else
40
- warn "unknown hook: #{hook.inspect}"
146
+ if hook.respond_to?(:new)
147
+ @hooks << hook.new(self)
148
+ else
149
+ warn "unknown hook: #{hook.inspect}"
150
+ end
41
151
  end
42
152
  end
43
153
 
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vernier
4
+ # Plan: The heap tracker can be in a few states:
5
+ # * Idle
6
+ # * Collecting
7
+ # * Watching for new objects
8
+ # * Watching for freed objects
9
+ # * Draining
10
+ # * Ignoring new objects
11
+ # * Watching for freed objects
12
+ # * Locked
13
+ # * Ignoring new objects
14
+ # * Ignoring freed objects
15
+ # * Marking all existing objects (not yet implemented)
16
+ # * N.B. This prevents any objects which the tracker has seen from being GC'd
17
+ class HeapTracker
18
+ attr_reader :stack_table
19
+
20
+ def self.new(stack_table = StackTable.new)
21
+ _new(stack_table)
22
+ end
23
+
24
+ def inspect
25
+ "#<#{self.class} allocated_objects=#{allocated_objects} freed_objects=#{freed_objects} stack_table=#{stack_table.inspect}>"
26
+ end
27
+
28
+ def self.track(&block)
29
+ tracker = new
30
+ tracker.track(&block)
31
+ tracker
32
+ end
33
+
34
+ def track
35
+ collect
36
+ yield self
37
+ ensure
38
+ lock
39
+ end
40
+
41
+ def stack(obj)
42
+ idx = stack_idx(obj)
43
+ return nil unless idx
44
+ stack_table.stack(idx)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vernier
4
+ class MemoryLeakDetector
5
+ def self.start_thread(...)
6
+ detector = new(...)
7
+ detector.start_thread
8
+ detector
9
+ end
10
+
11
+ def initialize(idle_time: 0, collect_time:, drain_time: 0, **collector_options)
12
+ @idle_time = idle_time
13
+ @collect_time = collect_time
14
+ @drain_time = drain_time
15
+ @collector_options = collector_options
16
+ @thread = nil
17
+ end
18
+
19
+ def start_thread
20
+ @thread = Thread.new do
21
+ sleep @idle_time
22
+
23
+ collector = Collector.new(:retained, @collector_options)
24
+ collector.start
25
+
26
+ sleep @collect_time
27
+
28
+ collector.drain
29
+
30
+ sleep @drain_time
31
+
32
+ collector.stop
33
+ end
34
+ end
35
+
36
+ def result
37
+ @thread&.value
38
+ end
39
+ end
40
+ end
@@ -1,26 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Vernier
2
4
  class Result
3
5
  attr_accessor :stack_table
4
6
  alias _stack_table stack_table
5
7
 
6
- attr_reader :gc_markers
7
-
8
- attr_accessor :hooks
8
+ attr_accessor :hooks, :pid, :end_time
9
9
 
10
- attr_accessor :pid, :end_time
11
- attr_accessor :threads
12
- attr_accessor :meta
13
- attr_accessor :mode
10
+ attr_reader :meta, :threads, :gc_markers
14
11
 
15
12
  def main_thread
16
13
  threads.values.detect {|x| x[:is_main] }
17
14
  end
18
15
 
19
- # TODO: remove these
20
- def weights; threads.values.flat_map { _1[:weights] }; end
21
- def samples; threads.values.flat_map { _1[:samples] }; end
22
- def sample_categories; threads.values.flat_map { _1[:sample_categories] }; end
23
-
24
16
  # Realtime in nanoseconds since the unix epoch
25
17
  def started_at
26
18
  started_at_mono_ns = meta[:started_at]
@@ -41,10 +33,17 @@ module Vernier
41
33
  def write(out:, format: "firefox")
42
34
  case format
43
35
  when "cpuprofile"
44
- File.binwrite(out, to_cpuprofile)
45
- when nil, "firefox"
46
- gzip = out.end_with?(".gz")
47
- File.binwrite(out, to_firefox(gzip:))
36
+ if out.respond_to?(:write)
37
+ out.write(to_cpuprofile)
38
+ else
39
+ File.binwrite(out, to_cpuprofile)
40
+ end
41
+ when "firefox", nil
42
+ if out.respond_to?(:write)
43
+ out.write(to_firefox)
44
+ else
45
+ File.binwrite(out, to_firefox(gzip: out.end_with?(".gz")))
46
+ end
48
47
  else
49
48
  raise ArgumentError, "unknown format: #{format}"
50
49
  end
@@ -55,15 +54,15 @@ module Vernier
55
54
  end
56
55
 
57
56
  def inspect
58
- "#<#{self.class} #{elapsed_seconds} seconds, #{threads.count} threads, #{samples.count} samples, #{samples.uniq.size} unique>"
57
+ "#<#{self.class} #{elapsed_seconds rescue "?"} seconds, #{threads.count} threads, #{total_samples} samples, #{total_unique_samples} unique>"
59
58
  end
60
59
 
61
60
  def each_sample
62
61
  return enum_for(__method__) unless block_given?
63
- samples.size.times do |sample_idx|
64
- weight = weights[sample_idx]
65
- stack_idx = samples[sample_idx]
66
- yield stack(stack_idx), weight
62
+ threads.values.each do |thread|
63
+ thread[:samples].zip(thread[:weights]) do |stack_idx, weight|
64
+ yield stack(stack_idx), weight
65
+ end
67
66
  end
68
67
  end
69
68
 
@@ -71,8 +70,24 @@ module Vernier
71
70
  stack_table.stack(idx)
72
71
  end
73
72
 
73
+ def total_weights
74
+ threads.values.sum { _1[:weights].sum }
75
+ end
76
+
74
77
  def total_bytes
75
- weights.sum
78
+ unless meta[:mode] == :retained
79
+ raise NotImplementedError, "total_bytes is only implemented for retained mode"
80
+ end
81
+
82
+ total_weights
83
+ end
84
+
85
+ def total_samples
86
+ threads.values.sum { _1[:samples].count }
87
+ end
88
+
89
+ def total_unique_samples
90
+ threads.values.flat_map { _1[:samples] }.uniq.count
76
91
  end
77
92
  end
78
93
  end
@@ -23,15 +23,21 @@ module Vernier
23
23
  end
24
24
 
25
25
  def backtrace(stack_idx)
26
- full_stack(stack_idx).map do |stack_idx|
26
+ last_filename = nil
27
+ last_lineno = nil
28
+ full_stack(stack_idx).reverse.map do |stack_idx|
27
29
  frame_idx = stack_frame_idx(stack_idx)
28
30
  func_idx = frame_func_idx(frame_idx)
29
31
  line = frame_line_no(frame_idx)
30
- name = func_name(func_idx);
31
- filename = func_filename(func_idx);
32
+ line = last_lineno if line == 0
33
+ last_lineno = line
34
+ name = func_name(func_idx)
35
+ filename = func_path(func_idx)
36
+ filename = last_filename if filename.empty?
37
+ last_filename = filename
32
38
 
33
39
  "#{filename}:#{line}:in '#{name}'"
34
- end
40
+ end.reverse
35
41
  end
36
42
 
37
43
  def full_stack(stack_idx)
@@ -83,9 +89,14 @@ module Vernier
83
89
  def line
84
90
  stack_table.frame_line_no(idx)
85
91
  end
92
+ alias lineno line
86
93
 
87
94
  def to_s
88
- "#{func}:#{line}"
95
+ if (line = self.line) == 0
96
+ func.to_s
97
+ else
98
+ "#{func}:#{line}"
99
+ end
89
100
  end
90
101
  end
91
102
 
@@ -102,14 +113,16 @@ module Vernier
102
113
  end
103
114
  alias each_frame each
104
115
 
105
- def [](n)
106
- raise RangeError if n < 0
116
+ def [](offset)
117
+ raise RangeError if offset < 0
107
118
  stack_idx = idx
108
- while n > 0
119
+ while stack_idx && offset > 0
109
120
  stack_idx = stack_table.stack_parent_idx(stack_idx)
110
- n -= 1
121
+ offset -= 1
122
+ end
123
+ if stack_idx && offset == 0
124
+ Frame.new(stack_table, stack_table.stack_frame_idx(stack_idx))
111
125
  end
112
- Frame.new(stack_table, stack_table.stack_frame_idx(stack_idx))
113
126
  end
114
127
 
115
128
  def leaf_frame_idx
@@ -134,6 +147,7 @@ module Vernier
134
147
  end
135
148
 
136
149
  def stack(idx)
150
+ raise ArgumentError, "invalid index" unless idx
137
151
  Stack.new(self, idx)
138
152
  end
139
153
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vernier
4
- VERSION = "1.8.1"
4
+ VERSION = "1.9.0"
5
5
  end
data/lib/vernier.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require_relative "vernier/version"
4
4
  require_relative "vernier/collector"
5
5
  require_relative "vernier/stack_table"
6
+ require_relative "vernier/heap_tracker"
7
+ require_relative "vernier/memory_leak_detector"
6
8
  require_relative "vernier/parsed_profile"
7
9
  require_relative "vernier/result"
8
10
  require_relative "vernier/hooks"
@@ -63,10 +65,4 @@ module Vernier
63
65
  def self.trace_retained(**profile_options, &block)
64
66
  profile(**profile_options.merge(mode: :retained), &block)
65
67
  end
66
-
67
- class Collector
68
- def self.new(mode, options = {})
69
- _new(mode, options)
70
- end
71
- end
72
68
  end
data/vernier.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/vernier/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "vernier"
7
+ spec.version = Vernier::VERSION
8
+ spec.authors = ["John Hawthorn"]
9
+ spec.email = ["john@hawthorn.email"]
10
+
11
+ spec.summary = "A next generation CRuby profiler"
12
+ spec.description = "Next-generation Ruby 3.2.1+ sampling profiler. Tracks multiple threads, GVL activity, GC pauses, idle time, and more."
13
+ spec.homepage = "https://github.com/jhawthorn/vernier"
14
+ spec.license = "MIT"
15
+
16
+ unless ENV["IGNORE_REQUIRED_RUBY_VERSION"]
17
+ spec.required_ruby_version = ">= 3.2.1"
18
+ end
19
+
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = spec.homepage
22
+ spec.metadata["changelog_uri"] = spec.homepage
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`.split("\x0").reject do |f|
28
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
29
+ end
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+ spec.extensions = ["ext/vernier/extconf.rb"]
35
+
36
+ spec.add_development_dependency "activesupport"
37
+ spec.add_development_dependency "gvltest"
38
+ spec.add_development_dependency "rack"
39
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vernier
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Hawthorn
@@ -70,6 +70,7 @@ files:
70
70
  - bin/console
71
71
  - bin/setup
72
72
  - bin/vernier
73
+ - examples/custom_hook.rb
73
74
  - examples/fiber_stalls.rb
74
75
  - examples/gvl_sleep.rb
75
76
  - examples/measure_overhead.rb
@@ -79,20 +80,25 @@ files:
79
80
  - examples/threaded_http_requests.rb
80
81
  - exe/vernier
81
82
  - ext/vernier/extconf.rb
83
+ - ext/vernier/heap_tracker.cc
82
84
  - ext/vernier/memory.cc
83
85
  - ext/vernier/periodic_thread.hh
84
86
  - ext/vernier/ruby_type_names.h
85
87
  - ext/vernier/signal_safe_semaphore.hh
88
+ - ext/vernier/stack_table.cc
89
+ - ext/vernier/stack_table.hh
86
90
  - ext/vernier/timestamp.hh
87
91
  - ext/vernier/vernier.cc
88
92
  - ext/vernier/vernier.hh
89
93
  - lib/vernier.rb
90
94
  - lib/vernier/autorun.rb
91
95
  - lib/vernier/collector.rb
96
+ - lib/vernier/heap_tracker.rb
92
97
  - lib/vernier/hooks.rb
93
98
  - lib/vernier/hooks/active_support.rb
94
99
  - lib/vernier/hooks/memory_usage.rb
95
100
  - lib/vernier/marker.rb
101
+ - lib/vernier/memory_leak_detector.rb
96
102
  - lib/vernier/middleware.rb
97
103
  - lib/vernier/output/cpuprofile.rb
98
104
  - lib/vernier/output/file_listing.rb
@@ -105,6 +111,7 @@ files:
105
111
  - lib/vernier/stack_table_helpers.rb
106
112
  - lib/vernier/thread_names.rb
107
113
  - lib/vernier/version.rb
114
+ - vernier.gemspec
108
115
  homepage: https://github.com/jhawthorn/vernier
109
116
  licenses:
110
117
  - MIT
@@ -126,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
133
  - !ruby/object:Gem::Version
127
134
  version: '0'
128
135
  requirements: []
129
- rubygems_version: 3.7.2
136
+ rubygems_version: 3.6.9
130
137
  specification_version: 4
131
138
  summary: A next generation CRuby profiler
132
139
  test_files: []