vernier 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +156 -39
- 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 +23 -5
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
@@ -331,7 +331,7 @@ struct RawSample {
|
|
331
331
|
|
332
332
|
Frame frame(int i) const {
|
333
333
|
int idx = len - i - 1;
|
334
|
-
if (idx < 0) throw std::out_of_range("out of range");
|
334
|
+
if (idx < 0) throw std::out_of_range("VERNIER BUG: index out of range");
|
335
335
|
const Frame frame = {frames[idx], lines[idx]};
|
336
336
|
return frame;
|
337
337
|
}
|
@@ -446,7 +446,7 @@ struct FrameList {
|
|
446
446
|
|
447
447
|
int stack_index(const RawSample &stack) {
|
448
448
|
if (stack.empty()) {
|
449
|
-
throw std::runtime_error("empty stack");
|
449
|
+
throw std::runtime_error("VERNIER BUG: empty stack");
|
450
450
|
}
|
451
451
|
|
452
452
|
StackNode *node = &root_stack_node;
|
@@ -659,8 +659,8 @@ class Marker {
|
|
659
659
|
|
660
660
|
class MarkerTable {
|
661
661
|
public:
|
662
|
-
std::vector<Marker> list;
|
663
662
|
std::mutex mutex;
|
663
|
+
std::vector<Marker> list;
|
664
664
|
|
665
665
|
void record_interval(Marker::Type type, TimeStamp from, TimeStamp to, int stack_index = -1) {
|
666
666
|
const std::lock_guard<std::mutex> lock(mutex);
|
@@ -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,
|
@@ -780,25 +826,29 @@ class Thread {
|
|
780
826
|
int stack_on_suspend_idx;
|
781
827
|
SampleTranslator translator;
|
782
828
|
|
783
|
-
MarkerTable
|
784
|
-
|
785
|
-
std::string name;
|
829
|
+
unique_ptr<MarkerTable> markers;
|
786
830
|
|
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
|
-
name = Qnil;
|
790
833
|
ruby_thread_id = rb_obj_id(ruby_thread);
|
791
|
-
|
834
|
+
//ruby_thread_id = ULL2NUM(ruby_thread);
|
792
835
|
native_tid = get_native_thread_id();
|
793
836
|
started_at = state_changed_at = TimeStamp::Now();
|
794
|
-
|
795
|
-
markers = new MarkerTable();
|
837
|
+
markers = std::make_unique<MarkerTable>();
|
796
838
|
|
797
839
|
if (state == State::STARTED) {
|
798
840
|
markers->record(Marker::Type::MARKER_GVL_THREAD_STARTED);
|
799
841
|
}
|
800
842
|
}
|
801
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
|
+
|
802
852
|
void set_state(State new_state) {
|
803
853
|
if (state == Thread::State::STOPPED) {
|
804
854
|
return;
|
@@ -867,6 +917,10 @@ class Thread {
|
|
867
917
|
state_changed_at = now;
|
868
918
|
}
|
869
919
|
|
920
|
+
bool is_main() {
|
921
|
+
return rb_thread_main() == ruby_thread;
|
922
|
+
}
|
923
|
+
|
870
924
|
bool running() {
|
871
925
|
return state != State::STOPPED;
|
872
926
|
}
|
@@ -879,15 +933,15 @@ class ThreadTable {
|
|
879
933
|
public:
|
880
934
|
FrameList &frame_list;
|
881
935
|
|
882
|
-
std::vector<Thread> list;
|
936
|
+
std::vector<std::unique_ptr<Thread> > list;
|
883
937
|
std::mutex mutex;
|
884
938
|
|
885
939
|
ThreadTable(FrameList &frame_list) : frame_list(frame_list) {
|
886
940
|
}
|
887
941
|
|
888
942
|
void mark() {
|
889
|
-
for (auto &thread : list) {
|
890
|
-
thread
|
943
|
+
for (const auto &thread : list) {
|
944
|
+
thread->mark();
|
891
945
|
}
|
892
946
|
}
|
893
947
|
|
@@ -923,7 +977,8 @@ class ThreadTable {
|
|
923
977
|
|
924
978
|
//fprintf(stderr, "th %p (tid: %i) from %s to %s\n", (void *)th, native_tid, gvl_event_name(state), gvl_event_name(new_state));
|
925
979
|
|
926
|
-
for (auto &
|
980
|
+
for (auto &threadptr : list) {
|
981
|
+
auto &thread = *threadptr;
|
927
982
|
if (thread_equal(th, thread.ruby_thread)) {
|
928
983
|
if (new_state == Thread::State::SUSPENDED) {
|
929
984
|
|
@@ -950,7 +1005,7 @@ class ThreadTable {
|
|
950
1005
|
}
|
951
1006
|
|
952
1007
|
//fprintf(stderr, "NEW THREAD: th: %p, state: %i\n", th, new_state);
|
953
|
-
list.
|
1008
|
+
list.push_back(std::make_unique<Thread>(new_state, pthread_self(), th));
|
954
1009
|
}
|
955
1010
|
|
956
1011
|
bool thread_equal(VALUE a, VALUE b) {
|
@@ -1226,17 +1281,22 @@ class GlobalSignalHandler {
|
|
1226
1281
|
if (count == 0) clear_signal_handler();
|
1227
1282
|
}
|
1228
1283
|
|
1229
|
-
|
1284
|
+
bool record_sample(LiveSample &sample, pthread_t pthread_id) {
|
1230
1285
|
const std::lock_guard<std::mutex> lock(mutex);
|
1231
1286
|
|
1232
1287
|
assert(pthread_id);
|
1233
1288
|
|
1234
1289
|
live_sample = &sample;
|
1235
|
-
|
1236
|
-
|
1290
|
+
int rc = pthread_kill(pthread_id, SIGPROF);
|
1291
|
+
if (rc) {
|
1292
|
+
fprintf(stderr, "VERNIER BUG: pthread_kill of %lu failed (%i)\n", (unsigned long)pthread_id, rc);
|
1293
|
+
live_sample = NULL;
|
1294
|
+
return false;
|
1295
|
+
} else {
|
1296
|
+
sample.wait();
|
1297
|
+
live_sample = NULL;
|
1298
|
+
return true;
|
1237
1299
|
}
|
1238
|
-
sample.wait();
|
1239
|
-
live_sample = NULL;
|
1240
1300
|
}
|
1241
1301
|
|
1242
1302
|
private:
|
@@ -1276,9 +1336,40 @@ class TimeCollector : public BaseCollector {
|
|
1276
1336
|
SamplerSemaphore thread_stopped;
|
1277
1337
|
|
1278
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
|
+
}
|
1279
1351
|
|
1280
1352
|
public:
|
1281
|
-
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
|
+
|
1282
1373
|
}
|
1283
1374
|
|
1284
1375
|
private:
|
@@ -1307,9 +1398,9 @@ class TimeCollector : public BaseCollector {
|
|
1307
1398
|
rb_ary_push(list, ary);
|
1308
1399
|
}
|
1309
1400
|
for (auto &thread : threads.list) {
|
1310
|
-
for (auto& marker: thread
|
1401
|
+
for (auto& marker: thread->markers->list) {
|
1311
1402
|
VALUE ary = marker.to_array();
|
1312
|
-
RARRAY_ASET(ary, 0, thread
|
1403
|
+
RARRAY_ASET(ary, 0, thread->ruby_thread_id);
|
1313
1404
|
rb_ary_push(list, ary);
|
1314
1405
|
}
|
1315
1406
|
}
|
@@ -1325,14 +1416,21 @@ class TimeCollector : public BaseCollector {
|
|
1325
1416
|
TimeStamp sample_start = TimeStamp::Now();
|
1326
1417
|
|
1327
1418
|
threads.mutex.lock();
|
1328
|
-
for (auto &
|
1419
|
+
for (auto &threadptr : threads.list) {
|
1420
|
+
auto &thread = *threadptr;
|
1421
|
+
|
1329
1422
|
//if (thread.state == Thread::State::RUNNING) {
|
1330
1423
|
//if (thread.state == Thread::State::RUNNING || (thread.state == Thread::State::SUSPENDED && thread.stack_on_suspend_idx < 0)) {
|
1331
1424
|
if (thread.state == Thread::State::RUNNING) {
|
1332
1425
|
//fprintf(stderr, "sampling %p on tid:%i\n", thread.ruby_thread, thread.native_tid);
|
1333
|
-
GlobalSignalHandler::get_instance()->record_sample(sample, thread.pthread_id);
|
1334
|
-
|
1335
|
-
if (
|
1426
|
+
bool signal_sent = GlobalSignalHandler::get_instance()->record_sample(sample, thread.pthread_id);
|
1427
|
+
|
1428
|
+
if (!signal_sent) {
|
1429
|
+
// The thread has died. We probably should have caught
|
1430
|
+
// that by the GVL instrumentation, but let's try to get
|
1431
|
+
// it to a consistent state and stop profiling it.
|
1432
|
+
thread.set_state(Thread::State::STOPPED);
|
1433
|
+
} else if (sample.sample.gc) {
|
1336
1434
|
// fprintf(stderr, "skipping GC sample\n");
|
1337
1435
|
} else {
|
1338
1436
|
record_sample(sample.sample, sample_start, thread, CATEGORY_NORMAL);
|
@@ -1458,6 +1556,11 @@ class TimeCollector : public BaseCollector {
|
|
1458
1556
|
return false;
|
1459
1557
|
}
|
1460
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
|
+
|
1461
1564
|
GlobalSignalHandler::get_instance()->install();
|
1462
1565
|
|
1463
1566
|
running = true;
|
@@ -1465,7 +1568,7 @@ class TimeCollector : public BaseCollector {
|
|
1465
1568
|
int ret = pthread_create(&sample_thread, NULL, &sample_thread_entry, this);
|
1466
1569
|
if (ret != 0) {
|
1467
1570
|
perror("pthread_create");
|
1468
|
-
rb_bug("pthread_create");
|
1571
|
+
rb_bug("VERNIER: pthread_create failed");
|
1469
1572
|
}
|
1470
1573
|
|
1471
1574
|
// Set the state of the current Ruby thread to RUNNING, which we know it
|
@@ -1490,6 +1593,11 @@ class TimeCollector : public BaseCollector {
|
|
1490
1593
|
|
1491
1594
|
GlobalSignalHandler::get_instance()->uninstall();
|
1492
1595
|
|
1596
|
+
if (RTEST(tp_newobj)) {
|
1597
|
+
rb_tracepoint_disable(tp_newobj);
|
1598
|
+
tp_newobj = Qnil;
|
1599
|
+
}
|
1600
|
+
|
1493
1601
|
rb_internal_thread_remove_event_hook(thread_hook);
|
1494
1602
|
rb_remove_event_hook(internal_gc_event_cb);
|
1495
1603
|
rb_remove_event_hook(internal_thread_event_cb);
|
@@ -1511,15 +1619,16 @@ class TimeCollector : public BaseCollector {
|
|
1511
1619
|
|
1512
1620
|
for (const auto& thread: this->threads.list) {
|
1513
1621
|
VALUE hash = rb_hash_new();
|
1514
|
-
thread
|
1515
|
-
|
1516
|
-
|
1517
|
-
rb_hash_aset(
|
1518
|
-
rb_hash_aset(hash, sym("
|
1519
|
-
|
1520
|
-
|
1622
|
+
thread->samples.write_result(hash);
|
1623
|
+
thread->allocation_samples.write_result(hash);
|
1624
|
+
|
1625
|
+
rb_hash_aset(threads, thread->ruby_thread_id, hash);
|
1626
|
+
rb_hash_aset(hash, sym("tid"), ULL2NUM(thread->native_tid));
|
1627
|
+
rb_hash_aset(hash, sym("started_at"), ULL2NUM(thread->started_at.nanoseconds()));
|
1628
|
+
if (!thread->stopped_at.zero()) {
|
1629
|
+
rb_hash_aset(hash, sym("stopped_at"), ULL2NUM(thread->stopped_at.nanoseconds()));
|
1521
1630
|
}
|
1522
|
-
rb_hash_aset(hash, sym("
|
1631
|
+
rb_hash_aset(hash, sym("is_main"), thread->is_main() ? Qtrue : Qfalse);
|
1523
1632
|
|
1524
1633
|
}
|
1525
1634
|
|
@@ -1615,12 +1724,20 @@ static VALUE collector_new(VALUE self, VALUE mode, VALUE options) {
|
|
1615
1724
|
} else {
|
1616
1725
|
interval = TimeStamp::from_microseconds(NUM2UINT(intervalv));
|
1617
1726
|
}
|
1618
|
-
|
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);
|
1619
1736
|
} else {
|
1620
1737
|
rb_raise(rb_eArgError, "invalid mode");
|
1621
1738
|
}
|
1622
1739
|
VALUE obj = TypedData_Wrap_Struct(self, &rb_collector_type, collector);
|
1623
|
-
rb_funcall(obj, rb_intern("initialize"),
|
1740
|
+
rb_funcall(obj, rb_intern("initialize"), 2, mode, options);
|
1624
1741
|
return obj;
|
1625
1742
|
}
|
1626
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
|
@@ -70,5 +88,5 @@ requirements: []
|
|
70
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: []
|