vernier 0.5.1 → 0.6.0

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