vernier 0.5.1 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da42588d9f2409a9d82a79439014490ecd81f00cb74c975267d6568a6e343c46
4
- data.tar.gz: 921543f2b8cfec4e5803c4a47c8953df1acff567b88ddce2e1a65faad9e7c895
3
+ metadata.gz: b9ba6d060b4eb230a865df67f378f81b08721d36a9f8cf394f9a8d5d0455b844
4
+ data.tar.gz: d01c209f5fab9bc99940fb5b967a722f69c75376dd6aba0c3e7759a9c52bc698
5
5
  SHA512:
6
- metadata.gz: a674536e944ef26f2db5893821d9d643fc8f651d45b7a72923ae9ab7c01f270431f0e5fadea76e3bb3feeabe30edde203b520310cbbbae6a53f03edb148db570
7
- data.tar.gz: 1f39e9f04a2fd1c80525a72ce27aca6e8d614dfa9a4ba9f9721beff08f11a41431f6690c6215f0592dcc7923e49e516a4a0ddb812b493820877ceca81b4373ab
6
+ metadata.gz: 65e359b25b1cc5a5e3dc2724ec7c98eb4f20203a98e0fd8f402b5875fe329afb6a20f6fc722f8da2febdd4cd30e8e2d56a00e42a4e5262baffb7a250fba79f3d
7
+ data.tar.gz: bf9ca5416e6b7b17d316eb9c762273ab711518c7ba50c862f803eed9e2d17fcbdeee378f9d6702d4029715926d105398105c5eb0f6aec046fa540611993ec89a
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Next-generation Ruby 3.2.1+ sampling profiler. Tracks multiple threads, GVL activity, GC pauses, idle time, and more.
4
4
 
5
+ <img width="500" alt="Screenshot 2024-02-29 at 22 47 43" src="https://github.com/jhawthorn/vernier/assets/131752/aa995a41-d74f-405f-8ada-2522dd72c2c8">
6
+
5
7
  ## Examples
6
8
 
7
9
  [Livestreamed demo: Pairin' with Aaron (YouTube)](https://www.youtube.com/watch?v=9nvX3OHykGQ#t=27m43)
@@ -35,7 +37,7 @@ gem 'vernier'
35
37
  Vernier.trace(out: "time_profile.json") { some_slow_method }
36
38
  ```
37
39
 
38
- The output can then be viewed in the Firefox Profiler (demo) or the [`profile-viewer` gem](https://github.com/tenderlove/profiler/tree/ruby) (a Ruby-customized version of the firefox profiler.
40
+ The output can be viewed in the web app at https://vernier.prof or locally using the [`profile-viewer` gem](https://github.com/tenderlove/profiler/tree/ruby) (both are lightly customized versions of the firefox profiler frontend, which profiles are also compatible with).
39
41
 
40
42
  - **Flame Graph**: Shows proportionally how much time is spent within particular stack frames. Frames are grouped together, which means that x-axis / left-to-right order is not meaningful.
41
43
  - **Stack Chart**: Shows the stack at each sample with the x-axis representing time and can be read left-to-right.
data/examples/rails.rb CHANGED
@@ -59,12 +59,17 @@ end
59
59
 
60
60
  silence do
61
61
  ActiveRecord::Schema.define do
62
+ create_table :users, force: true do |t|
63
+ t.string :login
64
+ end
62
65
  create_table :posts, force: true do |t|
66
+ t.belongs_to :user
63
67
  t.string :title
64
68
  t.text :body
65
69
  t.integer :likes
66
70
  end
67
71
  create_table :comments, force: true do |t|
72
+ t.belongs_to :user
68
73
  t.belongs_to :post
69
74
  t.string :title
70
75
  t.text :body
@@ -73,25 +78,33 @@ silence do
73
78
  end
74
79
  end
75
80
 
81
+ class User < ActiveRecord::Base
82
+ end
83
+
76
84
  class Post < ActiveRecord::Base
77
85
  has_many :comments
86
+ belongs_to :user
78
87
  end
79
88
 
80
89
  class Comment < ActiveRecord::Base
81
90
  belongs_to :post
91
+ belongs_to :user
82
92
  end
83
93
 
94
+ users = 0.upto(100).map do |i|
95
+ User.create!(login: "user#{i}")
96
+ end
84
97
  0.upto(100) do |i|
85
- post = Post.create!(title: "Post number #{i}", body: "blog " * 50, likes: ((i * 1337) % 30))
98
+ post = Post.create!(title: "Post number #{i}", body: "blog " * 50, likes: ((i * 1337) % 30), user: users.sample)
86
99
  5.times do
87
- post.comments.create!(post: post, title: "nice post!", body: "keep it up!", posted_at: Time.now)
100
+ post.comments.create!(post: post, title: "nice post!", body: "keep it up!", posted_at: Time.now, user: users.sample)
88
101
  end
89
102
  end
90
103
 
91
104
  class HomeController < ActionController::Base
92
105
  def show
93
- posts = Post.order(likes: :desc).includes(:comments).first(10)
94
- render json: posts
106
+ posts = Post.order(likes: :desc).includes(:user, comments: :user).first(10)
107
+ render json: posts, include: [:user, comments: { include: :user } ]
95
108
  end
96
109
  end
97
110
 
@@ -110,15 +123,7 @@ end
110
123
  # warm up
111
124
  make_request.call
112
125
 
113
- Vernier.trace(out: "rails.json") do |collector|
114
- ActiveSupport::Notifications.monotonic_subscribe do |name, start, finish, id, payload|
115
- collector.add_marker(
116
- name:,
117
- start: (start * 1_000_000_000).to_i,
118
- finish: (finish * 1_000_000_000).to_i,
119
- )
120
- end
121
-
126
+ Vernier.trace(out: "rails.json", hooks: [:rails], allocation_sample_rate: 100) do |collector|
122
127
  1000.times do
123
128
  make_request.call
124
129
  end
@@ -1,6 +1,6 @@
1
1
  require "vernier"
2
2
 
3
- Vernier.trace(out: "http_requests.json") do
3
+ Vernier.trace(out: "http_requests.json", allocation_sample_rate: 100) do
4
4
 
5
5
  require "net/http"
6
6
  require "uri"
data/exe/vernier CHANGED
@@ -16,6 +16,9 @@ parser = OptionParser.new(banner) do |o|
16
16
  o.on('--interval [MICROSECONDS]', Integer, "sampling interval (default 500)") do |i|
17
17
  options[:interval] = i
18
18
  end
19
+ o.on('--allocation_sample_rate [ALLOCATIONS]', Integer, "allocation sampling interval (default 0 disabled)") do |i|
20
+ options[:allocation_sample_rate] = i
21
+ end
19
22
  o.on('--signal [NAME]', String, "specify a signal to start and stop the profiler") do |s|
20
23
  options[:signal] = s
21
24
  end
@@ -693,6 +693,51 @@ enum Category{
693
693
  CATEGORY_IDLE
694
694
  };
695
695
 
696
+ class ObjectSampleList {
697
+ public:
698
+
699
+ std::vector<int> stacks;
700
+ std::vector<TimeStamp> timestamps;
701
+ std::vector<int> weights;
702
+
703
+ size_t size() {
704
+ return stacks.size();
705
+ }
706
+
707
+ bool empty() {
708
+ return size() == 0;
709
+ }
710
+
711
+ void record_sample(int stack_index, TimeStamp time, int weight) {
712
+ stacks.push_back(stack_index);
713
+ timestamps.push_back(time);
714
+ weights.push_back(1);
715
+ }
716
+
717
+ void write_result(VALUE result) const {
718
+ VALUE allocations = rb_hash_new();
719
+ rb_hash_aset(result, sym("allocations"), allocations);
720
+
721
+ VALUE samples = rb_ary_new();
722
+ rb_hash_aset(allocations, sym("samples"), samples);
723
+ for (auto& stack_index: this->stacks) {
724
+ rb_ary_push(samples, INT2NUM(stack_index));
725
+ }
726
+
727
+ VALUE weights = rb_ary_new();
728
+ rb_hash_aset(allocations, sym("weights"), weights);
729
+ for (auto& weight: this->weights) {
730
+ rb_ary_push(weights, INT2NUM(weight));
731
+ }
732
+
733
+ VALUE timestamps = rb_ary_new();
734
+ rb_hash_aset(allocations, sym("timestamps"), timestamps);
735
+ for (auto& timestamp: this->timestamps) {
736
+ rb_ary_push(timestamps, ULL2NUM(timestamp.nanoseconds()));
737
+ }
738
+ }
739
+ };
740
+
696
741
  class SampleList {
697
742
  public:
698
743
 
@@ -758,6 +803,7 @@ class SampleList {
758
803
  class Thread {
759
804
  public:
760
805
  SampleList samples;
806
+ ObjectSampleList allocation_samples;
761
807
 
762
808
  enum State {
763
809
  STARTED,
@@ -782,15 +828,12 @@ class Thread {
782
828
 
783
829
  unique_ptr<MarkerTable> markers;
784
830
 
785
- std::string name;
786
-
787
831
  // FIXME: don't use pthread at start
788
832
  Thread(State state, pthread_t pthread_id, VALUE ruby_thread) : pthread_id(pthread_id), ruby_thread(ruby_thread), state(state), stack_on_suspend_idx(-1) {
789
833
  ruby_thread_id = rb_obj_id(ruby_thread);
790
834
  //ruby_thread_id = ULL2NUM(ruby_thread);
791
835
  native_tid = get_native_thread_id();
792
836
  started_at = state_changed_at = TimeStamp::Now();
793
- name = "";
794
837
  markers = std::make_unique<MarkerTable>();
795
838
 
796
839
  if (state == State::STARTED) {
@@ -798,6 +841,14 @@ class Thread {
798
841
  }
799
842
  }
800
843
 
844
+ void record_newobj(VALUE obj, FrameList &frame_list) {
845
+ RawSample sample;
846
+ sample.sample();
847
+
848
+ int stack_idx = translator.translate(frame_list, sample);
849
+ allocation_samples.record_sample(stack_idx, TimeStamp::Now(), 1);
850
+ }
851
+
801
852
  void set_state(State new_state) {
802
853
  if (state == Thread::State::STOPPED) {
803
854
  return;
@@ -866,6 +917,10 @@ class Thread {
866
917
  state_changed_at = now;
867
918
  }
868
919
 
920
+ bool is_main() {
921
+ return rb_thread_main() == ruby_thread;
922
+ }
923
+
869
924
  bool running() {
870
925
  return state != State::STOPPED;
871
926
  }
@@ -1281,9 +1336,40 @@ class TimeCollector : public BaseCollector {
1281
1336
  SamplerSemaphore thread_stopped;
1282
1337
 
1283
1338
  TimeStamp interval;
1339
+ unsigned int allocation_sample_rate;
1340
+ unsigned int allocation_sample_tick = 0;
1341
+
1342
+ VALUE tp_newobj = Qnil;
1343
+
1344
+ static void newobj_i(VALUE tpval, void *data) {
1345
+ TimeCollector *collector = static_cast<TimeCollector *>(data);
1346
+ rb_trace_arg_t *tparg = rb_tracearg_from_tracepoint(tpval);
1347
+ VALUE obj = rb_tracearg_object(tparg);
1348
+
1349
+ collector->record_newobj(obj);
1350
+ }
1284
1351
 
1285
1352
  public:
1286
- TimeCollector(TimeStamp interval) : interval(interval), threads(frame_list) {
1353
+ TimeCollector(TimeStamp interval, unsigned int allocation_sample_rate) : interval(interval), allocation_sample_rate(allocation_sample_rate), threads(frame_list) {
1354
+ }
1355
+
1356
+ void record_newobj(VALUE obj) {
1357
+ if (++allocation_sample_tick < allocation_sample_rate) {
1358
+ return;
1359
+ }
1360
+ allocation_sample_tick = 0;
1361
+
1362
+ VALUE current_thread = rb_thread_current();
1363
+ threads.mutex.lock();
1364
+ for (auto &threadptr : threads.list) {
1365
+ auto &thread = *threadptr;
1366
+ if (current_thread == thread.ruby_thread) {
1367
+ thread.record_newobj(obj, threads.frame_list);
1368
+ break;
1369
+ }
1370
+ }
1371
+ threads.mutex.unlock();
1372
+
1287
1373
  }
1288
1374
 
1289
1375
  private:
@@ -1470,6 +1556,11 @@ class TimeCollector : public BaseCollector {
1470
1556
  return false;
1471
1557
  }
1472
1558
 
1559
+ if (allocation_sample_rate > 0) {
1560
+ tp_newobj = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, newobj_i, this);
1561
+ rb_tracepoint_enable(tp_newobj);
1562
+ }
1563
+
1473
1564
  GlobalSignalHandler::get_instance()->install();
1474
1565
 
1475
1566
  running = true;
@@ -1502,6 +1593,11 @@ class TimeCollector : public BaseCollector {
1502
1593
 
1503
1594
  GlobalSignalHandler::get_instance()->uninstall();
1504
1595
 
1596
+ if (RTEST(tp_newobj)) {
1597
+ rb_tracepoint_disable(tp_newobj);
1598
+ tp_newobj = Qnil;
1599
+ }
1600
+
1505
1601
  rb_internal_thread_remove_event_hook(thread_hook);
1506
1602
  rb_remove_event_hook(internal_gc_event_cb);
1507
1603
  rb_remove_event_hook(internal_thread_event_cb);
@@ -1524,6 +1620,7 @@ class TimeCollector : public BaseCollector {
1524
1620
  for (const auto& thread: this->threads.list) {
1525
1621
  VALUE hash = rb_hash_new();
1526
1622
  thread->samples.write_result(hash);
1623
+ thread->allocation_samples.write_result(hash);
1527
1624
 
1528
1625
  rb_hash_aset(threads, thread->ruby_thread_id, hash);
1529
1626
  rb_hash_aset(hash, sym("tid"), ULL2NUM(thread->native_tid));
@@ -1531,7 +1628,7 @@ class TimeCollector : public BaseCollector {
1531
1628
  if (!thread->stopped_at.zero()) {
1532
1629
  rb_hash_aset(hash, sym("stopped_at"), ULL2NUM(thread->stopped_at.nanoseconds()));
1533
1630
  }
1534
- rb_hash_aset(hash, sym("name"), rb_str_new(thread->name.data(), thread->name.length()));
1631
+ rb_hash_aset(hash, sym("is_main"), thread->is_main() ? Qtrue : Qfalse);
1535
1632
 
1536
1633
  }
1537
1634
 
@@ -1627,12 +1724,20 @@ static VALUE collector_new(VALUE self, VALUE mode, VALUE options) {
1627
1724
  } else {
1628
1725
  interval = TimeStamp::from_microseconds(NUM2UINT(intervalv));
1629
1726
  }
1630
- collector = new TimeCollector(interval);
1727
+
1728
+ VALUE allocation_sample_ratev = rb_hash_aref(options, sym("allocation_sample_rate"));
1729
+ unsigned int allocation_sample_rate;
1730
+ if (NIL_P(allocation_sample_ratev)) {
1731
+ allocation_sample_rate = 0;
1732
+ } else {
1733
+ allocation_sample_rate = NUM2UINT(allocation_sample_ratev);
1734
+ }
1735
+ collector = new TimeCollector(interval, allocation_sample_rate);
1631
1736
  } else {
1632
1737
  rb_raise(rb_eArgError, "invalid mode");
1633
1738
  }
1634
1739
  VALUE obj = TypedData_Wrap_Struct(self, &rb_collector_type, collector);
1635
- rb_funcall(obj, rb_intern("initialize"), 1, mode);
1740
+ rb_funcall(obj, rb_intern("initialize"), 2, mode, options);
1636
1741
  return obj;
1637
1742
  }
1638
1743
 
@@ -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,34 @@
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={})
8
9
  @mode = mode
9
10
  @markers = []
11
+ @hooks = []
12
+
13
+ @thread_names = ThreadNames.new
14
+
15
+ if options[:hooks]
16
+ Array(options[:hooks]).each do |hook|
17
+ add_hook(hook)
18
+ end
19
+ end
20
+ @hooks.each do |hook|
21
+ hook.enable
22
+ end
23
+ end
24
+
25
+ private def add_hook(hook)
26
+ case hook
27
+ when :rails, :activesupport
28
+ @hooks << Vernier::Hooks::ActiveSupport.new(self)
29
+ else
30
+ warn "Unknown hook: #{hook}"
31
+ end
10
32
  end
11
33
 
12
34
  ##
@@ -47,6 +69,18 @@ module Vernier
47
69
  def stop
48
70
  result = finish
49
71
 
72
+ @thread_names.finish
73
+
74
+ @hooks.each do |hook|
75
+ hook.disable
76
+ end
77
+
78
+ result.threads.each do |obj_id, thread|
79
+ thread[:name] ||= @thread_names[obj_id]
80
+ end
81
+
82
+ result.hooks = @hooks
83
+
50
84
  end_time = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
51
85
  result.pid = Process.pid
52
86
  result.end_time = end_time
@@ -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
@@ -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
@@ -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,7 +230,8 @@ 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
 
@@ -211,10 +258,31 @@ module Vernier
211
258
  end
212
259
  end
213
260
 
261
+ def filter_filenames(filenames)
262
+ pwd = "#{Dir.pwd}/"
263
+ gem_regex = %r{\A#{Regexp.union(Gem.path)}/gems/}
264
+ 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}
265
+ rubylibdir = "#{RbConfig::CONFIG["rubylibdir"]}/"
266
+
267
+ filenames.map do |filename|
268
+ if filename.match?(gem_regex)
269
+ gem_match_regex =~ filename
270
+ "gem:#$1-#$2:#$3"
271
+ elsif filename.start_with?(pwd)
272
+ filename.delete_prefix(pwd)
273
+ elsif filename.start_with?(rubylibdir)
274
+ path = filename.delete_prefix(rubylibdir)
275
+ "rubylib:#{RUBY_VERSION}:#{path}"
276
+ else
277
+ filename
278
+ end
279
+ end
280
+ end
281
+
214
282
  def data
215
283
  {
216
284
  name: @name,
217
- isMainThread: @ruby_thread_id == ::Thread.main.object_id || (profile.threads.size == 1),
285
+ isMainThread: @is_main,
218
286
  processStartupTime: 0, # FIXME
219
287
  processShutdownTime: nil, # FIXME
220
288
  registerTime: (@started_at - 0) / 1_000_000.0,
@@ -226,6 +294,7 @@ module Vernier
226
294
  funcTable: func_table,
227
295
  nativeSymbols: {},
228
296
  samples: samples_table,
297
+ jsAllocations: allocations_table,
229
298
  stackTable: stack_table,
230
299
  resourceTable: {
231
300
  length: 0,
@@ -236,7 +305,7 @@ module Vernier
236
305
  },
237
306
  markers: markers_table,
238
307
  stringArray: string_table
239
- }
308
+ }.compact
240
309
  end
241
310
 
242
311
  def markers_table
@@ -277,6 +346,25 @@ module Vernier
277
346
  }
278
347
  end
279
348
 
349
+ def allocations_table
350
+ return nil if !@allocations
351
+ samples, weights, timestamps = @allocations.values_at(:samples, :weights, :timestamps)
352
+ return nil if samples.size == 0
353
+ size = samples.size
354
+ timestamps = timestamps.map { _1 / 1_000_000.0 }
355
+ ret = {
356
+ "time": timestamps,
357
+ "className": ["Object"]*size,
358
+ "typeName": ["JSObject"]*size,
359
+ "coarseType": ["Object"]*size,
360
+ "weight": weights,
361
+ "inNursery": [false] * size,
362
+ "stack": samples,
363
+ "length": size
364
+ }
365
+ ret
366
+ end
367
+
280
368
  def samples_table
281
369
  samples = @samples
282
370
  weights = @weights
@@ -366,14 +454,21 @@ module Vernier
366
454
 
367
455
  cfunc_idx = @strings["<cfunc>"]
368
456
  is_js = @filenames.map { |fn| fn != cfunc_idx }
457
+ line_numbers = profile.func_table.fetch(:first_line).map.with_index do |line, i|
458
+ if is_js[i] || line != 0
459
+ line
460
+ else
461
+ nil
462
+ end
463
+ end
369
464
  {
370
465
  name: @func_names,
371
466
  isJS: is_js,
372
467
  relevantForJS: is_js,
373
468
  resource: [-1] * size, # set to unidentified for now
374
469
  fileName: @filenames,
375
- lineNumber: profile.func_table.fetch(:first_line),
376
- columnNumber: [0] * size,
470
+ lineNumber: line_numbers,
471
+ columnNumber: [nil] * size,
377
472
  #columnNumber: functions.map { _1.column },
378
473
  length: size
379
474
  }
@@ -385,25 +480,6 @@ module Vernier
385
480
 
386
481
  private
387
482
 
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
483
  def gc_category
408
484
  @categorizer.get_category("GC")
409
485
  end
@@ -3,10 +3,16 @@ module Vernier
3
3
  attr_reader :stack_table, :frame_table, :func_table
4
4
  attr_reader :markers
5
5
 
6
+ attr_accessor :hooks
7
+
6
8
  attr_accessor :pid, :end_time
7
9
  attr_accessor :threads
8
10
  attr_accessor :meta
9
11
 
12
+ def main_thread
13
+ threads.values.detect {|x| x[:is_main] }
14
+ end
15
+
10
16
  # TODO: remove these
11
17
  def weights; threads.values.flat_map { _1[:weights] }; end
12
18
  def samples; threads.values.flat_map { _1[:samples] }; 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.6.0"
5
5
  end
data/lib/vernier.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "vernier/version"
4
4
  require_relative "vernier/collector"
5
5
  require_relative "vernier/result"
6
+ require_relative "vernier/hooks"
6
7
  require_relative "vernier/vernier"
7
8
  require_relative "vernier/output/firefox"
8
9
  require_relative "vernier/output/top"
@@ -10,8 +11,8 @@ require_relative "vernier/output/top"
10
11
  module Vernier
11
12
  class Error < StandardError; end
12
13
 
13
- def self.trace(mode: :wall, out: nil, interval: nil)
14
- collector = Vernier::Collector.new(mode, { interval: })
14
+ def self.trace(mode: :wall, out: nil, **collector_options)
15
+ collector = Vernier::Collector.new(mode, collector_options)
15
16
  collector.start
16
17
 
17
18
  result = nil
data/vernier.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["John Hawthorn"]
9
9
  spec.email = ["john@hawthorn.email"]
10
10
 
11
- spec.summary = "An experimental profiler"
12
- spec.description = spec.summary
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
13
  spec.homepage = "https://github.com/jhawthorn/vernier"
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = ">= 3.2.1"
@@ -29,4 +29,6 @@ Gem::Specification.new do |spec|
29
29
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
30
  spec.require_paths = ["lib"]
31
31
  spec.extensions = ["ext/vernier/extconf.rb"]
32
+
33
+ spec.add_development_dependency "activesupport"
32
34
  end
metadata CHANGED
@@ -1,16 +1,31 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vernier
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.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-02-28 00:00:00.000000000 Z
12
- dependencies: []
13
- description: An experimental profiler
11
+ date: 2024-03-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Next-generation Ruby 3.2.1+ sampling profiler. Tracks multiple threads,
28
+ GVL activity, GC pauses, idle time, and more.
14
29
  email:
15
30
  - john@hawthorn.email
16
31
  executables:
@@ -39,10 +54,13 @@ files:
39
54
  - lib/vernier.rb
40
55
  - lib/vernier/autorun.rb
41
56
  - lib/vernier/collector.rb
57
+ - lib/vernier/hooks.rb
58
+ - lib/vernier/hooks/active_support.rb
42
59
  - lib/vernier/marker.rb
43
60
  - lib/vernier/output/firefox.rb
44
61
  - lib/vernier/output/top.rb
45
62
  - lib/vernier/result.rb
63
+ - lib/vernier/thread_names.rb
46
64
  - lib/vernier/version.rb
47
65
  - vernier.gemspec
48
66
  homepage: https://github.com/jhawthorn/vernier
@@ -67,8 +85,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
85
  - !ruby/object:Gem::Version
68
86
  version: '0'
69
87
  requirements: []
70
- rubygems_version: 3.5.3
88
+ rubygems_version: 3.4.10
71
89
  signing_key:
72
90
  specification_version: 4
73
- summary: An experimental profiler
91
+ summary: A next generation CRuby profiler
74
92
  test_files: []