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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4be5edc549ea93934096d98e46c4ffcabc140d06e27303f6ec6b01de8201983c
4
- data.tar.gz: a6322324723a2f31f15a0b7e98a6de72bf5c555e29e21951b7f466466ecedd6f
3
+ metadata.gz: c3aa6d3665db78499efc979f38af3d3a4f633e105ccd3a6a17b9e11ad19556cf
4
+ data.tar.gz: eb752d610602a89417c894a360ea46f7727ce9a9bd11a13a2f608d35d07d4680
5
5
  SHA512:
6
- metadata.gz: 92377006ae54cc5cc89934cc3c393fdfe02746035235b507196ef9d360b6d843c50db4382e0b53ec253f1d08f10f78e28d7daf10979f26187bb7c925933dec56
7
- data.tar.gz: 0e963a838d3c319d93d73ae224c9e11caa51562583ef37f2d5fff40781281d30ee714273a9ce021c9e8285942825a3356388d2f68b51315fb5275c5bb96b49a6
6
+ metadata.gz: 3a96e0d3fa78b72e3a54c05eb871af65d405c32c44e0c0d8bc5efbeffaad59bb8490ab5c599e1a435d755093927c623e813965cda31a9bbeda43e3c2c41945cc
7
+ data.tar.gz: 32355e62e58124b426899dc4c167139521081d8be2bcf454aa3b0fb4916fb92334464a5685cccf5b712ee2d3d23a328a804b96b12a79e85e590f30d31d28fc66
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.6
1
+ 3.4.7
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
+ gem "cgi"
9
10
 
10
11
  gem "rake-compiler"
11
12
  gem "benchmark-ips"
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
+ }
@@ -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