vernier 1.8.1 → 1.9.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/.ruby-version +1 -1
- data/Gemfile +1 -0
- data/README.md +65 -0
- data/examples/custom_hook.rb +37 -0
- data/ext/vernier/heap_tracker.cc +277 -0
- data/ext/vernier/memory.cc +1 -1
- data/ext/vernier/stack_table.cc +290 -0
- data/ext/vernier/stack_table.hh +314 -0
- data/ext/vernier/vernier.cc +67 -791
- data/ext/vernier/vernier.hh +7 -0
- data/lib/vernier/collector.rb +112 -2
- data/lib/vernier/heap_tracker.rb +47 -0
- data/lib/vernier/memory_leak_detector.rb +40 -0
- data/lib/vernier/result.rb +37 -22
- data/lib/vernier/stack_table_helpers.rb +24 -10
- data/lib/vernier/version.rb +1 -1
- data/lib/vernier.rb +2 -6
- data/vernier.gemspec +39 -0
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3aa6d3665db78499efc979f38af3d3a4f633e105ccd3a6a17b9e11ad19556cf
|
|
4
|
+
data.tar.gz: eb752d610602a89417c894a360ea46f7727ce9a9bd11a13a2f608d35d07d4680
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a96e0d3fa78b72e3a54c05eb871af65d405c32c44e0c0d8bc5efbeffaad59bb8490ab5c599e1a435d755093927c623e813965cda31a9bbeda43e3c2c41945cc
|
|
7
|
+
data.tar.gz: 32355e62e58124b426899dc4c167139521081d8be2bcf454aa3b0fb4916fb92334464a5685cccf5b712ee2d3d23a328a804b96b12a79e85e590f30d31d28fc66
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.4.
|
|
1
|
+
3.4.7
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -126,6 +126,71 @@ ruby -r vernier -e 'Vernier.trace_retained(out: "irb_profile.json") { require "i
|
|
|
126
126
|
> [!NOTE]
|
|
127
127
|
> Retained-memory flamegraphs must be interpreted a little differently than a typical profiling flamegraph. In a retained-memory flamegraph, the x-axis represents a proportion of memory in bytes, _not time or samples_ The topmost boxes on the y-axis represent the retained objects, with their stacktrace below; their width represents the percentage of overall retained memory each object occupies.
|
|
128
128
|
|
|
129
|
+
### Hooks
|
|
130
|
+
|
|
131
|
+
Hooks automatically add markers to profiles based on application events:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
Vernier.profile(hooks: [:activesupport, MyHook]) do
|
|
135
|
+
# code to profile
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Built-in hooks
|
|
140
|
+
|
|
141
|
+
- `:activesupport` (`:rails`) - ActiveSupport notifications
|
|
142
|
+
- `:memory_usage` - Memory usage tracking
|
|
143
|
+
|
|
144
|
+
#### Custom hooks
|
|
145
|
+
|
|
146
|
+
Define a class with `initialize(collector)`, `enable`, and `disable` methods:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
class MyHook
|
|
150
|
+
def initialize(collector)
|
|
151
|
+
@collector = collector
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def enable
|
|
155
|
+
# Set up event listeners
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def disable
|
|
159
|
+
# Clean up
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
For Firefox profiler integration, add `firefox_marker_schema` or `firefox_counters` methods (see [Firefox profiler docs](https://profiler.firefox.com/docs/#/) for format details):
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
def firefox_marker_schema
|
|
168
|
+
[{
|
|
169
|
+
name: "my_event",
|
|
170
|
+
display: ["marker-chart", "marker-table"],
|
|
171
|
+
tooltipLabel: "{marker.data.name}",
|
|
172
|
+
data: [
|
|
173
|
+
{ key: "name", format: "string" }
|
|
174
|
+
]
|
|
175
|
+
}]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def firefox_counters
|
|
179
|
+
{
|
|
180
|
+
name: "my_counter",
|
|
181
|
+
category: "Custom",
|
|
182
|
+
description: "My custom counter",
|
|
183
|
+
samples: {
|
|
184
|
+
time: [0, 1000, 2000],
|
|
185
|
+
count: [10, 20, 30],
|
|
186
|
+
length: 3
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
See [`examples/custom_hook.rb`](examples/custom_hook.rb) for a complete example.
|
|
193
|
+
|
|
129
194
|
### Options
|
|
130
195
|
|
|
131
196
|
| Option | Middleware Param | Description | Default (Middleware Default) |
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "vernier"
|
|
4
|
+
|
|
5
|
+
class CustomHook
|
|
6
|
+
def initialize(collector)
|
|
7
|
+
@collector = collector
|
|
8
|
+
@events = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def enable
|
|
12
|
+
# Simulate subscribing to application events
|
|
13
|
+
@thread = Thread.new do
|
|
14
|
+
3.times do |i|
|
|
15
|
+
sleep 0.03
|
|
16
|
+
start_time = @collector.current_time
|
|
17
|
+
sleep 0.01 # Simulate work
|
|
18
|
+
@collector.add_marker(
|
|
19
|
+
name: "custom_event",
|
|
20
|
+
start: start_time,
|
|
21
|
+
finish: @collector.current_time,
|
|
22
|
+
data: { type: "custom_event", event_id: i + 1 }
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def disable
|
|
29
|
+
@thread&.join
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
result = Vernier.profile(hooks: [CustomHook]) do
|
|
34
|
+
sleep 0.15
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
puts "Profile complete with custom events"
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#include "vernier.hh"
|
|
2
|
+
#include "stack_table.hh"
|
|
3
|
+
|
|
4
|
+
static VALUE rb_cHeapTracker;
|
|
5
|
+
|
|
6
|
+
static void heap_tracker_mark(void *data);
|
|
7
|
+
static void heap_tracker_free(void *data);
|
|
8
|
+
static size_t heap_tracker_memsize(const void *data);
|
|
9
|
+
static void heap_tracker_compact(void *data);
|
|
10
|
+
|
|
11
|
+
static const rb_data_type_t rb_heap_tracker_type = {
|
|
12
|
+
.wrap_struct_name = "vernier/heap_tracker",
|
|
13
|
+
.function = {
|
|
14
|
+
.dmark = heap_tracker_mark,
|
|
15
|
+
.dfree = heap_tracker_free,
|
|
16
|
+
.dsize = heap_tracker_memsize,
|
|
17
|
+
.dcompact = heap_tracker_compact,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
class HeapTracker {
|
|
22
|
+
public:
|
|
23
|
+
VALUE stack_table_value;
|
|
24
|
+
StackTable *stack_table;
|
|
25
|
+
|
|
26
|
+
unsigned long long objects_freed = 0;
|
|
27
|
+
unsigned long long objects_allocated = 0;
|
|
28
|
+
unsigned long long tombstones = 0;
|
|
29
|
+
|
|
30
|
+
std::unordered_map<VALUE, int> object_index;
|
|
31
|
+
std::vector<VALUE> object_list;
|
|
32
|
+
std::vector<int> frame_list;
|
|
33
|
+
|
|
34
|
+
static VALUE rb_new(VALUE self, VALUE stack_table_value) {
|
|
35
|
+
HeapTracker *heap_tracker = new HeapTracker();
|
|
36
|
+
heap_tracker->stack_table_value = stack_table_value;
|
|
37
|
+
heap_tracker->stack_table = get_stack_table(stack_table_value);
|
|
38
|
+
VALUE obj = TypedData_Wrap_Struct(rb_cHeapTracker, &rb_heap_tracker_type, heap_tracker);
|
|
39
|
+
rb_ivar_set(obj, rb_intern("@stack_table"), stack_table_value);
|
|
40
|
+
return obj;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static HeapTracker *get(VALUE obj) {
|
|
44
|
+
HeapTracker *heap_tracker;
|
|
45
|
+
TypedData_Get_Struct(obj, HeapTracker, &rb_heap_tracker_type, heap_tracker);
|
|
46
|
+
return heap_tracker;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
void record_newobj(VALUE obj) {
|
|
50
|
+
objects_allocated++;
|
|
51
|
+
|
|
52
|
+
RawSample sample;
|
|
53
|
+
sample.sample();
|
|
54
|
+
if (sample.empty()) {
|
|
55
|
+
// During thread allocation we allocate one object without a frame
|
|
56
|
+
// (as of Ruby 3.3)
|
|
57
|
+
// Ideally we'd allow empty samples to be represented
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
int stack_index = stack_table->stack_index(sample);
|
|
61
|
+
|
|
62
|
+
int idx = object_list.size();
|
|
63
|
+
object_list.push_back(obj);
|
|
64
|
+
frame_list.push_back(stack_index);
|
|
65
|
+
object_index.emplace(obj, idx);
|
|
66
|
+
|
|
67
|
+
assert(objects_allocated == frame_list.size());
|
|
68
|
+
assert(objects_allocated == object_list.size());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
void rebuild() {
|
|
72
|
+
object_index.clear();
|
|
73
|
+
|
|
74
|
+
size_t j = 0;
|
|
75
|
+
for (size_t i = 0; i < object_list.size(); i++) {
|
|
76
|
+
VALUE obj = object_list[i];
|
|
77
|
+
if (obj != Qfalse) {
|
|
78
|
+
object_list[j] = obj;
|
|
79
|
+
frame_list[j] = frame_list[i];
|
|
80
|
+
object_index.emplace(obj, j);
|
|
81
|
+
j++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
object_list.resize(j);
|
|
86
|
+
frame_list.resize(j);
|
|
87
|
+
tombstones = 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
void record_freeobj(VALUE obj) {
|
|
91
|
+
auto it = object_index.find(obj);
|
|
92
|
+
if (it != object_index.end()) {
|
|
93
|
+
int index = it->second;
|
|
94
|
+
object_list[index] = Qfalse;
|
|
95
|
+
objects_freed++;
|
|
96
|
+
tombstones++;
|
|
97
|
+
object_index.erase(it);
|
|
98
|
+
|
|
99
|
+
if (tombstones * 2 > object_list.size()) {
|
|
100
|
+
rebuild();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static void newobj_i(VALUE tpval, void *data) {
|
|
106
|
+
HeapTracker *tracer = static_cast<HeapTracker *>(data);
|
|
107
|
+
rb_trace_arg_t *tparg = rb_tracearg_from_tracepoint(tpval);
|
|
108
|
+
VALUE obj = rb_tracearg_object(tparg);
|
|
109
|
+
|
|
110
|
+
tracer->record_newobj(obj);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static void freeobj_i(VALUE tpval, void *data) {
|
|
114
|
+
HeapTracker *tracer = static_cast<HeapTracker *>(data);
|
|
115
|
+
rb_trace_arg_t *tparg = rb_tracearg_from_tracepoint(tpval);
|
|
116
|
+
VALUE obj = rb_tracearg_object(tparg);
|
|
117
|
+
|
|
118
|
+
tracer->record_freeobj(obj);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
bool stopped = false;
|
|
122
|
+
VALUE tp_newobj = Qnil;
|
|
123
|
+
VALUE tp_freeobj = Qnil;
|
|
124
|
+
|
|
125
|
+
void collect() {
|
|
126
|
+
if (!RTEST(tp_newobj)) {
|
|
127
|
+
tp_newobj = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, newobj_i, this);
|
|
128
|
+
tp_freeobj = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_FREEOBJ, freeobj_i, this);
|
|
129
|
+
|
|
130
|
+
rb_tracepoint_enable(tp_newobj);
|
|
131
|
+
rb_tracepoint_enable(tp_freeobj);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
static VALUE collect(VALUE self) {
|
|
135
|
+
get(self)->collect();
|
|
136
|
+
return self;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
void drain() {
|
|
140
|
+
if (RTEST(tp_newobj)) {
|
|
141
|
+
rb_tracepoint_disable(tp_newobj);
|
|
142
|
+
tp_newobj = Qnil;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static VALUE drain(VALUE self) {
|
|
147
|
+
get(self)->drain();
|
|
148
|
+
return self;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
void lock() {
|
|
152
|
+
drain();
|
|
153
|
+
if (RTEST(tp_freeobj)) {
|
|
154
|
+
rb_tracepoint_disable(tp_freeobj);
|
|
155
|
+
tp_freeobj = Qnil;
|
|
156
|
+
}
|
|
157
|
+
stopped = true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static VALUE lock(VALUE self) {
|
|
161
|
+
get(self)->lock();
|
|
162
|
+
return self;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
static VALUE stack_idx(VALUE self, VALUE obj) {
|
|
166
|
+
auto tracer = get(self);
|
|
167
|
+
auto iter = tracer->object_index.find(obj);
|
|
168
|
+
if (iter == tracer->object_index.end()) {
|
|
169
|
+
return Qnil;
|
|
170
|
+
} else {
|
|
171
|
+
int index = iter->second;
|
|
172
|
+
return INT2NUM(tracer->frame_list[index]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
VALUE data() {
|
|
177
|
+
assert(stopped);
|
|
178
|
+
VALUE hash = rb_hash_new();
|
|
179
|
+
VALUE samples = rb_ary_new();
|
|
180
|
+
rb_hash_aset(hash, sym("samples"), samples);
|
|
181
|
+
VALUE weights = rb_ary_new();
|
|
182
|
+
rb_hash_aset(hash, sym("weights"), weights);
|
|
183
|
+
|
|
184
|
+
for (int i = 0; i < object_list.size(); i++) {
|
|
185
|
+
VALUE obj = object_list[i];
|
|
186
|
+
if (obj == Qfalse) continue;
|
|
187
|
+
VALUE stack_index = frame_list[i];
|
|
188
|
+
|
|
189
|
+
rb_ary_push(samples, INT2NUM(stack_index));
|
|
190
|
+
rb_ary_push(weights, INT2NUM(rb_obj_memsize_of(obj)));
|
|
191
|
+
}
|
|
192
|
+
return hash;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static VALUE data(VALUE self) {
|
|
196
|
+
return get(self)->data();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static VALUE allocated_objects(VALUE self) {
|
|
200
|
+
return ULL2NUM(get(self)->objects_allocated);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static VALUE freed_objects(VALUE self) {
|
|
204
|
+
return ULL2NUM(get(self)->objects_freed);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
void mark() {
|
|
208
|
+
rb_gc_mark(stack_table_value);
|
|
209
|
+
|
|
210
|
+
rb_gc_mark(tp_newobj);
|
|
211
|
+
rb_gc_mark(tp_freeobj);
|
|
212
|
+
|
|
213
|
+
if (stopped) {
|
|
214
|
+
for (auto obj: object_list) {
|
|
215
|
+
rb_gc_mark_movable(obj);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
void compact() {
|
|
221
|
+
object_index.clear();
|
|
222
|
+
for (int i = 0; i < object_list.size(); i++) {
|
|
223
|
+
VALUE obj = object_list[i];
|
|
224
|
+
VALUE reloc_obj = rb_gc_location(obj);
|
|
225
|
+
|
|
226
|
+
object_list[i] = reloc_obj;
|
|
227
|
+
object_index.emplace(reloc_obj, i);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
static void
|
|
233
|
+
heap_tracker_mark(void *data) {
|
|
234
|
+
HeapTracker *heap_tracker = static_cast<HeapTracker *>(data);
|
|
235
|
+
heap_tracker->mark();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
static void
|
|
239
|
+
heap_tracker_free(void *data) {
|
|
240
|
+
HeapTracker *heap_tracker = static_cast<HeapTracker *>(data);
|
|
241
|
+
delete heap_tracker;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
static size_t
|
|
245
|
+
heap_tracker_memsize(const void *data) {
|
|
246
|
+
const HeapTracker *heap_tracker = static_cast<const HeapTracker *>(data);
|
|
247
|
+
size_t size = sizeof(HeapTracker);
|
|
248
|
+
|
|
249
|
+
size += heap_tracker->object_index.bucket_count() * sizeof(void*);
|
|
250
|
+
size += heap_tracker->object_index.size() * (sizeof(VALUE) + sizeof(int) + sizeof(void*));
|
|
251
|
+
|
|
252
|
+
size += heap_tracker->object_list.capacity() * sizeof(VALUE);
|
|
253
|
+
size += heap_tracker->frame_list.capacity() * sizeof(int);
|
|
254
|
+
|
|
255
|
+
return size;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
static void
|
|
259
|
+
heap_tracker_compact(void *data) {
|
|
260
|
+
HeapTracker *heap_tracker = static_cast<HeapTracker *>(data);
|
|
261
|
+
heap_tracker->compact();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
void
|
|
265
|
+
Init_heap_tracker() {
|
|
266
|
+
rb_cHeapTracker = rb_define_class_under(rb_mVernier, "HeapTracker", rb_cObject);
|
|
267
|
+
rb_define_method(rb_cHeapTracker, "collect", HeapTracker::collect, 0);
|
|
268
|
+
rb_define_method(rb_cHeapTracker, "drain", HeapTracker::drain, 0);
|
|
269
|
+
rb_define_method(rb_cHeapTracker, "lock", HeapTracker::lock, 0);
|
|
270
|
+
rb_define_method(rb_cHeapTracker, "data", HeapTracker::data, 0);
|
|
271
|
+
rb_define_method(rb_cHeapTracker, "stack_idx", HeapTracker::stack_idx, 1);
|
|
272
|
+
rb_undef_alloc_func(rb_cHeapTracker);
|
|
273
|
+
rb_define_singleton_method(rb_cHeapTracker, "_new", HeapTracker::rb_new, 1);
|
|
274
|
+
|
|
275
|
+
rb_define_method(rb_cHeapTracker, "allocated_objects", HeapTracker::allocated_objects, 0);
|
|
276
|
+
rb_define_method(rb_cHeapTracker, "freed_objects", HeapTracker::freed_objects, 0);
|
|
277
|
+
}
|
data/ext/vernier/memory.cc
CHANGED
|
@@ -85,9 +85,9 @@ class MemoryTracker : public PeriodicThread {
|
|
|
85
85
|
static const rb_data_type_t rb_memory_tracker_type = {
|
|
86
86
|
.wrap_struct_name = "vernier/memory_tracker",
|
|
87
87
|
.function = {
|
|
88
|
-
//.dmemsize = memory_tracker_memsize,
|
|
89
88
|
//.dmark = memory_tracker_mark,
|
|
90
89
|
//.dfree = memory_tracker_free,
|
|
90
|
+
//.dsize = memory_tracker_memsize,
|
|
91
91
|
},
|
|
92
92
|
};
|
|
93
93
|
|