vernier 0.5.0 → 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 +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: []
|