vernier 1.11.0 → 1.12.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 +19 -10
- data/exe/vernier +3 -0
- data/ext/vernier/extconf.rb +4 -3
- data/ext/vernier/heap_tracker.cc +10 -3
- data/ext/vernier/memory.cc +19 -5
- data/ext/vernier/periodic_thread.hh +17 -2
- data/ext/vernier/signal_safe_semaphore.hh +3 -1
- data/ext/vernier/stack_table.cc +18 -6
- data/ext/vernier/stack_table.hh +24 -26
- data/ext/vernier/vernier.cc +127 -117
- data/ext/vernier/vernier.hh +12 -0
- data/lib/vernier/autorun.rb +2 -1
- data/lib/vernier/collector.rb +3 -0
- data/lib/vernier/hooks/active_support.rb +714 -32
- data/lib/vernier/output/firefox.rb +17 -7
- data/lib/vernier/thread_names.rb +6 -0
- data/lib/vernier/version.rb +1 -1
- data/vernier.gemspec +2 -2
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 245f2f630047ca4214da2d8a939a3948f96486f626137e4cc951fb0161e8bba4
|
|
4
|
+
data.tar.gz: 8b6b4240f03d8e058a1fcae78029d2cb102de0bdebf53da66690143a14a7b8fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e0f0a556ccb06c4b95e531f724224de59805ef0821c275f84b0c72e15603b045a24ccc6b420d17fd6cf34bda8c15e100fe4b433fdfa8b0dc2450a76267814c12
|
|
7
|
+
data.tar.gz: 453a6a97efa3ea492cd79dd4a7455933d88e3dd3128e298c75c466bdecbae88d7594a671e8c383f89b9201f67d1f6e442e2cb7060bb5274dcba29bcba673082a
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vernier
|
|
2
2
|
|
|
3
|
-
Next-generation Ruby 3.
|
|
3
|
+
Next-generation Ruby 3.3+ sampling profiler. Tracks multiple threads, GVL activity, GC pauses, idle time, and more.
|
|
4
4
|
|
|
5
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
6
|
|
|
@@ -24,7 +24,7 @@ Rails benchmark - lobste.rs (time)
|
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
26
26
|
|
|
27
|
-
Vernier requires Ruby version 3.
|
|
27
|
+
Vernier requires Ruby version 3.3 or greater and a Unix-like operating system.
|
|
28
28
|
|
|
29
29
|
```ruby
|
|
30
30
|
gem "vernier", "~> 1.0"
|
|
@@ -193,14 +193,15 @@ See [`examples/custom_hook.rb`](examples/custom_hook.rb) for a complete example.
|
|
|
193
193
|
|
|
194
194
|
### Options
|
|
195
195
|
|
|
196
|
-
| Option
|
|
197
|
-
|
|
198
|
-
| `mode`
|
|
199
|
-
| `out`
|
|
200
|
-
| `interval`
|
|
201
|
-
| `allocation_interval`
|
|
202
|
-
| `gc`
|
|
203
|
-
| `metadata`
|
|
196
|
+
| Option | Middleware Param | Description | Default (Middleware Default) |
|
|
197
|
+
|--------------------------|-------------------------------|---------------------------------------------------------------|------------------------------|
|
|
198
|
+
| `mode` | N/A | Sampling mode: `:wall`, `:retained`, or `:custom`. | `:wall` (`:wall`) |
|
|
199
|
+
| `out` | N/A | File to write the profile to. | N/A (Auto-generated) |
|
|
200
|
+
| `interval` | `vernier_interval` | Sampling interval (µs). Only in `:wall` mode. | `500` (`200`) |
|
|
201
|
+
| `allocation_interval` | `vernier_allocation_interval` | Allocation sampling interval. Only in `:wall` mode. | `0` i.e. disabled (`200`) |
|
|
202
|
+
| `gc` | N/A | Run full GC cycle before profiling. Only in `:retained` mode. | `true` (N/A) |
|
|
203
|
+
| `metadata` | N/A | Metadata key-value pairs to include in the profile. | `{}` (N/A) |
|
|
204
|
+
| `hide_anonymous_threads` | N/A | Anonymous threads will be initially hidden in profile output. | `false` (N/A) |
|
|
204
205
|
|
|
205
206
|
#### Hook options
|
|
206
207
|
|
|
@@ -218,6 +219,14 @@ Vernier.profile(out: "time_profile.json", hooks: [:rails, :memory_usage]) do
|
|
|
218
219
|
end
|
|
219
220
|
```
|
|
220
221
|
|
|
222
|
+
### Overhead
|
|
223
|
+
|
|
224
|
+
Vernier is a sampling profiler, so its overhead is low and depends on the sampling `interval`. Profiling a CPU-bound loop at the default interval runs within a few percent of its unprofiled speed.
|
|
225
|
+
|
|
226
|
+
Allocation sampling (`allocation_interval`) costs more, since every allocation takes a slower path and sampled allocations record a stack. The cost grows with how often the code allocates and how small the interval is.
|
|
227
|
+
|
|
228
|
+
See [`examples/measure_overhead.rb`](examples/measure_overhead.rb) to measure it yourself.
|
|
229
|
+
|
|
221
230
|
## Development
|
|
222
231
|
|
|
223
232
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/exe/vernier
CHANGED
|
@@ -51,6 +51,9 @@ FLAGS:
|
|
|
51
51
|
options[:metadata] ||= Metadata.new
|
|
52
52
|
options[:metadata] << [key, value]
|
|
53
53
|
end
|
|
54
|
+
o.on('--hide-anonymous-threads', "Anonymous threads will be initially hidden in profiles") do
|
|
55
|
+
options[:hide_anonymous_threads] = true
|
|
56
|
+
end
|
|
54
57
|
o.on('--format [FORMAT]', String, "output format: firefox (default) or cpuprofile") do |output_format|
|
|
55
58
|
options[:format] = output_format
|
|
56
59
|
end
|
data/ext/vernier/extconf.rb
CHANGED
|
@@ -6,10 +6,11 @@ $CXXFLAGS += " -fvisibility=hidden "
|
|
|
6
6
|
$CXXFLAGS += " -std=c++14 "
|
|
7
7
|
$CXXFLAGS += " -ggdb3 -Og "
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
# Make VERNIER_ASSERT always check, even if Ruby wasn't built with RUBY_DEBUG.
|
|
10
|
+
# Set by our CI; gem installs get RUBY_ASSERT (compiled out) by default.
|
|
11
|
+
$defs << "-DVERNIER_ALWAYS_ASSERT" if ["1", "true"].include?(ENV["VERNIER_ALWAYS_ASSERT"])
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
have_header("ruby/thread.h")
|
|
13
14
|
|
|
14
15
|
have_func("pthread_setname_np")
|
|
15
16
|
have_func("pthread_condattr_setclock")
|
data/ext/vernier/heap_tracker.cc
CHANGED
|
@@ -27,14 +27,20 @@ class HeapTracker {
|
|
|
27
27
|
unsigned long long objects_allocated = 0;
|
|
28
28
|
unsigned long long tombstones = 0;
|
|
29
29
|
|
|
30
|
+
HeapTracker() = default;
|
|
31
|
+
HeapTracker(const HeapTracker &) = delete;
|
|
32
|
+
HeapTracker &operator=(const HeapTracker &) = delete;
|
|
33
|
+
|
|
30
34
|
std::unordered_map<VALUE, int> object_index;
|
|
31
35
|
std::vector<VALUE> object_list;
|
|
32
36
|
std::vector<int> frame_list;
|
|
33
37
|
|
|
34
38
|
static VALUE rb_new(VALUE self, VALUE stack_table_value) {
|
|
39
|
+
// get_stack_table can raise, so do it before allocating anything
|
|
40
|
+
StackTable *stack_table = get_stack_table(stack_table_value);
|
|
35
41
|
HeapTracker *heap_tracker = new HeapTracker();
|
|
36
42
|
heap_tracker->stack_table_value = stack_table_value;
|
|
37
|
-
heap_tracker->stack_table =
|
|
43
|
+
heap_tracker->stack_table = stack_table;
|
|
38
44
|
VALUE obj = TypedData_Wrap_Struct(rb_cHeapTracker, &rb_heap_tracker_type, heap_tracker);
|
|
39
45
|
rb_ivar_set(obj, rb_intern("@stack_table"), stack_table_value);
|
|
40
46
|
return obj;
|
|
@@ -63,7 +69,7 @@ class HeapTracker {
|
|
|
63
69
|
frame_list.push_back(stack_index);
|
|
64
70
|
object_index.emplace(obj, idx);
|
|
65
71
|
|
|
66
|
-
|
|
72
|
+
VERNIER_ASSERT(frame_list.size() == object_list.size());
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
void rebuild() {
|
|
@@ -175,7 +181,7 @@ class HeapTracker {
|
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
VALUE data() {
|
|
178
|
-
|
|
184
|
+
VERNIER_ASSERT(stopped);
|
|
179
185
|
VALUE hash = rb_hash_new();
|
|
180
186
|
VALUE samples = rb_ary_new();
|
|
181
187
|
rb_hash_aset(hash, sym("samples"), samples);
|
|
@@ -222,6 +228,7 @@ class HeapTracker {
|
|
|
222
228
|
object_index.clear();
|
|
223
229
|
for (int i = 0; i < object_list.size(); i++) {
|
|
224
230
|
VALUE obj = object_list[i];
|
|
231
|
+
if (obj == Qfalse) continue; // tombstone
|
|
225
232
|
VALUE reloc_obj = rb_gc_location(obj);
|
|
226
233
|
|
|
227
234
|
object_list[i] = reloc_obj;
|
data/ext/vernier/memory.cc
CHANGED
|
@@ -72,7 +72,7 @@ class MemoryTracker : public PeriodicThread {
|
|
|
72
72
|
MemoryTracker() : PeriodicThread(TimeStamp::from_milliseconds(10)) {
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
void run_iteration() {
|
|
75
|
+
void run_iteration() override {
|
|
76
76
|
record();
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -82,12 +82,19 @@ class MemoryTracker : public PeriodicThread {
|
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
static void
|
|
86
|
+
memory_tracker_free(void *data) {
|
|
87
|
+
MemoryTracker *memory_tracker = static_cast<MemoryTracker *>(data);
|
|
88
|
+
// The sampler thread holds a pointer to the tracker, so it must be
|
|
89
|
+
// stopped (and joined) before the tracker is destroyed.
|
|
90
|
+
memory_tracker->stop();
|
|
91
|
+
delete memory_tracker;
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
static const rb_data_type_t rb_memory_tracker_type = {
|
|
86
95
|
.wrap_struct_name = "vernier/memory_tracker",
|
|
87
96
|
.function = {
|
|
88
|
-
|
|
89
|
-
//.dfree = memory_tracker_free,
|
|
90
|
-
//.dsize = memory_tracker_memsize,
|
|
97
|
+
.dfree = memory_tracker_free,
|
|
91
98
|
},
|
|
92
99
|
};
|
|
93
100
|
|
|
@@ -116,9 +123,16 @@ VALUE memory_tracker_record(VALUE self) {
|
|
|
116
123
|
VALUE memory_tracker_results(VALUE self) {
|
|
117
124
|
MemoryTracker *memory_tracker;
|
|
118
125
|
TypedData_Get_Struct(self, MemoryTracker, &rb_memory_tracker_type, memory_tracker);
|
|
126
|
+
// Copy under the lock; don't call into Ruby (which can allocate, GC or
|
|
127
|
+
// raise) while holding it.
|
|
128
|
+
std::vector<MemoryTracker::Record> results;
|
|
129
|
+
{
|
|
130
|
+
const std::lock_guard<std::mutex> lock(memory_tracker->mutex);
|
|
131
|
+
results = memory_tracker->results;
|
|
132
|
+
}
|
|
119
133
|
VALUE timestamps = rb_ary_new();
|
|
120
134
|
VALUE memory = rb_ary_new();
|
|
121
|
-
for (const auto& record:
|
|
135
|
+
for (const auto& record: results) {
|
|
122
136
|
rb_ary_push(timestamps, ULL2NUM(record.timestamp.nanoseconds()));
|
|
123
137
|
rb_ary_push(memory, ULL2NUM(record.memory_rss));
|
|
124
138
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#include "ruby.h"
|
|
2
2
|
|
|
3
3
|
#include <atomic>
|
|
4
|
+
#include <errno.h>
|
|
4
5
|
#include "timestamp.hh"
|
|
5
6
|
|
|
6
7
|
#ifdef __APPLE__
|
|
@@ -41,7 +42,7 @@ inline void upgrade_thread_priority(pthread_t pthread) {
|
|
|
41
42
|
#endif
|
|
42
43
|
|
|
43
44
|
class PeriodicThread {
|
|
44
|
-
pthread_t pthread;
|
|
45
|
+
pthread_t pthread = 0;
|
|
45
46
|
TimeStamp interval;
|
|
46
47
|
|
|
47
48
|
pthread_mutex_t running_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
@@ -56,8 +57,20 @@ class PeriodicThread {
|
|
|
56
57
|
pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
|
|
57
58
|
#endif
|
|
58
59
|
pthread_cond_init(&running_cv, &attr);
|
|
60
|
+
pthread_condattr_destroy(&attr);
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
// Deleting through a base pointer must run the derived destructor.
|
|
64
|
+
// Subclasses must stop() the thread before they are destroyed, as it
|
|
65
|
+
// calls their virtual run_iteration().
|
|
66
|
+
virtual ~PeriodicThread() {
|
|
67
|
+
pthread_cond_destroy(&running_cv);
|
|
68
|
+
pthread_mutex_destroy(&running_mutex);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
PeriodicThread(const PeriodicThread &) = delete;
|
|
72
|
+
PeriodicThread &operator=(const PeriodicThread &) = delete;
|
|
73
|
+
|
|
61
74
|
void set_interval(TimeStamp timestamp) {
|
|
62
75
|
interval = timestamp;
|
|
63
76
|
}
|
|
@@ -99,10 +112,12 @@ class PeriodicThread {
|
|
|
99
112
|
auto offset = TimeStamp::NowRealtime() - TimeStamp::Now();
|
|
100
113
|
struct timespec next_sample_ts = (next_sample_schedule + offset).timespec();
|
|
101
114
|
#endif
|
|
115
|
+
// Wait until the deadline passes or we are asked to stop.
|
|
116
|
+
// Wakeups may be spurious, so only a timeout ends the wait.
|
|
102
117
|
int ret;
|
|
103
118
|
do {
|
|
104
119
|
ret = pthread_cond_timedwait(&running_cv, &running_mutex, &next_sample_ts);
|
|
105
|
-
} while(running && ret
|
|
120
|
+
} while (running && ret != ETIMEDOUT);
|
|
106
121
|
}
|
|
107
122
|
done = !running;
|
|
108
123
|
pthread_mutex_unlock(&running_mutex);
|
|
@@ -25,6 +25,9 @@ class SignalSafeSemaphore {
|
|
|
25
25
|
|
|
26
26
|
public:
|
|
27
27
|
|
|
28
|
+
SignalSafeSemaphore(const SignalSafeSemaphore &) = delete;
|
|
29
|
+
SignalSafeSemaphore &operator=(const SignalSafeSemaphore &) = delete;
|
|
30
|
+
|
|
28
31
|
SignalSafeSemaphore(unsigned int value = 0) {
|
|
29
32
|
#ifdef __APPLE__
|
|
30
33
|
sem = dispatch_semaphore_create(value);
|
|
@@ -56,7 +59,6 @@ class SignalSafeSemaphore {
|
|
|
56
59
|
if (ret != 0) {
|
|
57
60
|
rb_bug("VERNIER: sem_timedwait waited over 5 seconds");
|
|
58
61
|
}
|
|
59
|
-
assert(ret == 0);
|
|
60
62
|
#endif
|
|
61
63
|
}
|
|
62
64
|
|
data/ext/vernier/stack_table.cc
CHANGED
|
@@ -53,6 +53,9 @@ stack_table_current_stack(int argc, VALUE *argv, VALUE self) {
|
|
|
53
53
|
RawSample stack;
|
|
54
54
|
stack.sample(offset);
|
|
55
55
|
int stack_index = stack_table->stack_index(stack);
|
|
56
|
+
if (stack_index < 0) {
|
|
57
|
+
rb_raise(rb_eArgError, "no stack at the given offset");
|
|
58
|
+
}
|
|
56
59
|
return INT2NUM(stack_index);
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -174,7 +177,6 @@ StackTable::stack_table_func_name(VALUE self, VALUE idxval) {
|
|
|
174
177
|
return Qnil;
|
|
175
178
|
} else {
|
|
176
179
|
const auto &func_info = table[idx];
|
|
177
|
-
std::string label = func_info.full_label();
|
|
178
180
|
|
|
179
181
|
// Ruby constants are in an arbitrary (ASCII compatible) encoding and
|
|
180
182
|
// method names are in an arbitrary (ASCII compatible) encoding. These
|
|
@@ -188,7 +190,18 @@ StackTable::stack_table_func_name(VALUE self, VALUE idxval) {
|
|
|
188
190
|
//
|
|
189
191
|
// In the future we might keep class and method name separate for
|
|
190
192
|
// longer, preserve encodings, and defer formatting to the Ruby side.
|
|
191
|
-
|
|
193
|
+
//
|
|
194
|
+
// The label is assembled directly in a Ruby string so that no C++
|
|
195
|
+
// temporaries are alive across calls that may raise.
|
|
196
|
+
VALUE str = rb_utf8_str_new("", 0);
|
|
197
|
+
if (!func_info.method_name.empty()) {
|
|
198
|
+
rb_str_cat(str, func_info.classpath.data(), func_info.classpath.size());
|
|
199
|
+
rb_str_cat(str, func_info.is_singleton ? "." : "#", 1);
|
|
200
|
+
rb_str_cat(str, func_info.method_name.data(), func_info.method_name.size());
|
|
201
|
+
} else {
|
|
202
|
+
rb_str_cat(str, func_info.label.data(), func_info.label.size());
|
|
203
|
+
}
|
|
204
|
+
return rb_str_to_interned_str(str);
|
|
192
205
|
}
|
|
193
206
|
}
|
|
194
207
|
|
|
@@ -202,8 +215,7 @@ StackTable::stack_table_func_filename(VALUE self, VALUE idxval) {
|
|
|
202
215
|
return Qnil;
|
|
203
216
|
} else {
|
|
204
217
|
const auto &func_info = table[idx];
|
|
205
|
-
std::string filename = func_info.absolute_path;
|
|
206
|
-
if (filename.empty()) filename = func_info.path;
|
|
218
|
+
const std::string &filename = func_info.absolute_path.empty() ? func_info.path : func_info.absolute_path;
|
|
207
219
|
|
|
208
220
|
// Technically filesystems are binary and then Ruby interprets that as
|
|
209
221
|
// default_external encoding. But to keep things simple for now we are
|
|
@@ -222,7 +234,7 @@ StackTable::stack_table_func_path(VALUE self, VALUE idxval) {
|
|
|
222
234
|
return Qnil;
|
|
223
235
|
} else {
|
|
224
236
|
const auto &func_info = table[idx];
|
|
225
|
-
std::string filename = func_info.path;
|
|
237
|
+
const std::string &filename = func_info.path;
|
|
226
238
|
|
|
227
239
|
// Technically filesystems are binary and then Ruby interprets that as
|
|
228
240
|
// default_external encoding. But to keep things simple for now we are
|
|
@@ -241,7 +253,7 @@ StackTable::stack_table_func_absolute_path(VALUE self, VALUE idxval) {
|
|
|
241
253
|
return Qnil;
|
|
242
254
|
} else {
|
|
243
255
|
const auto &func_info = table[idx];
|
|
244
|
-
std::string filename = func_info.absolute_path;
|
|
256
|
+
const std::string &filename = func_info.absolute_path;
|
|
245
257
|
|
|
246
258
|
// Technically filesystems are binary and then Ruby interprets that as
|
|
247
259
|
// default_external encoding. But to keep things simple for now we are
|
data/ext/vernier/stack_table.hh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#define STACK_TABLE_HH STACK_TABLE_HH
|
|
3
3
|
|
|
4
4
|
#include <mutex>
|
|
5
|
-
#include <
|
|
5
|
+
#include <algorithm>
|
|
6
6
|
#include <string>
|
|
7
7
|
#include <unordered_map>
|
|
8
8
|
#include <vector>
|
|
@@ -30,7 +30,11 @@ namespace std {
|
|
|
30
30
|
{
|
|
31
31
|
std::size_t operator()(Frame const& s) const noexcept
|
|
32
32
|
{
|
|
33
|
-
|
|
33
|
+
// VALUEs are aligned so their low bits carry no entropy; mix in
|
|
34
|
+
// the line number rather than xoring it into the low bits.
|
|
35
|
+
std::size_t h = static_cast<std::size_t>(s.frame) >> 3;
|
|
36
|
+
h ^= static_cast<std::size_t>(s.line) + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2);
|
|
37
|
+
return h;
|
|
34
38
|
}
|
|
35
39
|
};
|
|
36
40
|
}
|
|
@@ -50,7 +54,7 @@ class RawSample {
|
|
|
50
54
|
|
|
51
55
|
public:
|
|
52
56
|
|
|
53
|
-
RawSample() : len(0),
|
|
57
|
+
RawSample() : len(0), offset(0), gc(false) { }
|
|
54
58
|
|
|
55
59
|
int size() const {
|
|
56
60
|
return len - offset;
|
|
@@ -58,7 +62,7 @@ class RawSample {
|
|
|
58
62
|
|
|
59
63
|
Frame frame(int i) const {
|
|
60
64
|
int idx = len - i - 1;
|
|
61
|
-
if (idx < 0)
|
|
65
|
+
if (idx < 0) rb_bug("VERNIER BUG: index out of range");
|
|
62
66
|
const Frame frame = {frames[idx], lines[idx]};
|
|
63
67
|
return frame;
|
|
64
68
|
}
|
|
@@ -95,34 +99,22 @@ struct FuncInfo {
|
|
|
95
99
|
return NIL_P(first_lineno) ? 0 : FIX2INT(first_lineno);
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
// Must not raise: this runs while std::string members are under
|
|
103
|
+
// construction, and a longjmp would skip their destructors.
|
|
98
104
|
static std::string convert_rstring(VALUE rstring) {
|
|
99
|
-
if (
|
|
105
|
+
if (!RB_TYPE_P(rstring, T_STRING)) {
|
|
100
106
|
return "";
|
|
101
|
-
} else {
|
|
102
|
-
const char *cstring = StringValueCStr(rstring);
|
|
103
|
-
return cstring;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
std::string full_label() const {
|
|
108
|
-
std::string output;
|
|
109
|
-
if (!method_name.empty()) {
|
|
110
|
-
output.append(classpath);
|
|
111
|
-
output.append(is_singleton ? "." : "#");
|
|
112
|
-
output.append(method_name);
|
|
113
|
-
} else {
|
|
114
|
-
output.append(label);
|
|
115
107
|
}
|
|
116
|
-
return
|
|
108
|
+
return std::string(RSTRING_PTR(rstring), RSTRING_LEN(rstring));
|
|
117
109
|
}
|
|
118
110
|
|
|
119
111
|
FuncInfo(VALUE frame) :
|
|
120
112
|
label(convert_rstring(rb_profile_frame_label(frame))),
|
|
121
113
|
base_label(convert_rstring(rb_profile_frame_base_label(frame))),
|
|
122
114
|
classpath(convert_rstring(rb_profile_frame_classpath(frame))),
|
|
115
|
+
path(convert_rstring(rb_profile_frame_path(frame))),
|
|
123
116
|
absolute_path(convert_rstring(rb_profile_frame_absolute_path(frame))),
|
|
124
117
|
method_name(convert_rstring(rb_profile_frame_method_name(frame))),
|
|
125
|
-
path(convert_rstring(rb_profile_frame_path(frame))),
|
|
126
118
|
first_lineno(first_lineno_int(frame)),
|
|
127
119
|
is_singleton(RTEST(rb_profile_frame_singleton_method_p(frame)))
|
|
128
120
|
{ }
|
|
@@ -171,6 +163,10 @@ class IndexMap {
|
|
|
171
163
|
};
|
|
172
164
|
|
|
173
165
|
struct StackTable {
|
|
166
|
+
StackTable() = default;
|
|
167
|
+
StackTable(const StackTable &) = delete;
|
|
168
|
+
StackTable &operator=(const StackTable &) = delete;
|
|
169
|
+
|
|
174
170
|
private:
|
|
175
171
|
|
|
176
172
|
IndexMap<Frame> frame_map;
|
|
@@ -184,10 +180,10 @@ struct StackTable {
|
|
|
184
180
|
int parent;
|
|
185
181
|
int index;
|
|
186
182
|
|
|
187
|
-
StackNode(Frame frame, int index, int parent) : frame(frame),
|
|
183
|
+
StackNode(Frame frame, int index, int parent) : frame(frame), parent(parent), index(index) {}
|
|
188
184
|
|
|
189
185
|
// root
|
|
190
|
-
StackNode() : frame(Frame{0, 0}),
|
|
186
|
+
StackNode() : frame(Frame{0, 0}), parent(-1), index(-1) {}
|
|
191
187
|
};
|
|
192
188
|
|
|
193
189
|
// This mutex guards the StackNodes only. The rest of the maps and vectors
|
|
@@ -218,9 +214,11 @@ struct StackTable {
|
|
|
218
214
|
|
|
219
215
|
public:
|
|
220
216
|
|
|
217
|
+
// Returns -1 for an empty stack. Never throws, as it may be called from
|
|
218
|
+
// C callbacks invoked by Ruby.
|
|
221
219
|
int stack_index(const RawSample &stack) {
|
|
222
220
|
if (stack.empty()) {
|
|
223
|
-
|
|
221
|
+
return -1;
|
|
224
222
|
}
|
|
225
223
|
|
|
226
224
|
const std::lock_guard<std::mutex> lock(stack_mutex);
|
|
@@ -260,8 +258,8 @@ struct StackTable {
|
|
|
260
258
|
const auto &stack_node = stack_node_list[i];
|
|
261
259
|
frame_map.index(stack_node.frame);
|
|
262
260
|
func_map.index(stack_node.frame.frame);
|
|
263
|
-
stack_node_list_finalized_idx = i;
|
|
264
261
|
}
|
|
262
|
+
stack_node_list_finalized_idx = stack_node_list.size();
|
|
265
263
|
}
|
|
266
264
|
|
|
267
265
|
for (int i = func_info_list.size(); i < func_map.size(); i++) {
|
|
@@ -274,7 +272,7 @@ struct StackTable {
|
|
|
274
272
|
void mark_frames() {
|
|
275
273
|
const std::lock_guard<std::mutex> lock(stack_mutex);
|
|
276
274
|
|
|
277
|
-
for (auto stack_node: stack_node_list) {
|
|
275
|
+
for (const auto &stack_node: stack_node_list) {
|
|
278
276
|
rb_gc_mark(stack_node.frame.frame);
|
|
279
277
|
}
|
|
280
278
|
}
|