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 +4 -4
- data/README.md +3 -1
- data/examples/rails.rb +18 -13
- data/examples/threaded_http_requests.rb +1 -1
- data/exe/vernier +3 -0
- data/ext/vernier/vernier.cc +112 -7
- data/lib/vernier/autorun.rb +2 -1
- data/lib/vernier/collector.rb +35 -1
- data/lib/vernier/hooks/active_support.rb +110 -0
- data/lib/vernier/hooks.rb +7 -0
- data/lib/vernier/output/firefox.rb +105 -29
- data/lib/vernier/result.rb +6 -0
- data/lib/vernier/thread_names.rb +52 -0
- data/lib/vernier/version.rb +1 -1
- data/lib/vernier.rb +3 -2
- data/vernier.gemspec +4 -2
- metadata +24 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9ba6d060b4eb230a865df67f378f81b08721d36a9f8cf394f9a8d5d0455b844
|
4
|
+
data.tar.gz: d01c209f5fab9bc99940fb5b967a722f69c75376dd6aba0c3e7759a9c52bc698
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
data/ext/vernier/vernier.cc
CHANGED
@@ -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("
|
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
|
-
|
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"),
|
1740
|
+
rb_funcall(obj, rb_intern("initialize"), 2, mode, options);
|
1636
1741
|
return obj;
|
1637
1742
|
}
|
1638
1743
|
|
data/lib/vernier/autorun.rb
CHANGED
@@ -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
|
|
data/lib/vernier/collector.rb
CHANGED
@@ -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
|
@@ -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: "
|
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
|
-
@
|
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
|
-
|
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: @
|
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:
|
376
|
-
columnNumber: [
|
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
|
data/lib/vernier/result.rb
CHANGED
@@ -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
|
data/lib/vernier/version.rb
CHANGED
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,
|
14
|
-
collector = Vernier::Collector.new(mode,
|
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 = "
|
12
|
-
spec.description =
|
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.
|
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-
|
12
|
-
dependencies:
|
13
|
-
|
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.
|
88
|
+
rubygems_version: 3.4.10
|
71
89
|
signing_key:
|
72
90
|
specification_version: 4
|
73
|
-
summary:
|
91
|
+
summary: A next generation CRuby profiler
|
74
92
|
test_files: []
|