memory-profiler 1.1.4 → 1.1.6
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
- checksums.yaml.gz.sig +0 -0
- data/ext/memory/profiler/capture.c +159 -68
- data/lib/memory/profiler/call_tree.rb +75 -1
- data/lib/memory/profiler/sampler.rb +37 -2
- data/lib/memory/profiler/version.rb +1 -1
- data/readme.md +11 -1
- data/releases.md +11 -1
- data.tar.gz.sig +3 -7
- metadata +1 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ea6e765ad50aed3c3cf538d5c1a44f102f1bbe5251d5fd9c0995dd9d6890282a
|
|
4
|
+
data.tar.gz: dac87b8b0bd69f57ce3a290ef84f6eae56192ab422e554f861a2cb89b3cfa290
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc91cd05c5cf0a2680fefe8745d8be48f7d85c0e7f358326da2ee0b58a07b01363fd9238b3b00bc9f17d5df8f216da726ebbf6dfdd77242685753ebf285183e9
|
|
7
|
+
data.tar.gz: df0498c707e90556d7df5319aa22775c1912f5961c5ea4f8d60f5314a9bdc8920ac60f367ef1d29e912789974857a3275af3e680a2ad2447651366e53d5685df
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
enum {
|
|
15
15
|
DEBUG = 0,
|
|
16
|
-
|
|
16
|
+
DEBUG_EVENT_QUEUES = 0,
|
|
17
17
|
DEBUG_STATE = 0,
|
|
18
18
|
};
|
|
19
19
|
|
|
@@ -23,8 +23,20 @@ static VALUE Memory_Profiler_Capture = Qnil;
|
|
|
23
23
|
static VALUE sym_newobj;
|
|
24
24
|
static VALUE sym_freeobj;
|
|
25
25
|
|
|
26
|
-
// Queue item -
|
|
27
|
-
struct
|
|
26
|
+
// Queue item - new object data to be processed via postponed job
|
|
27
|
+
struct Memory_Profiler_Newobj_Queue_Item {
|
|
28
|
+
// The class of the new object:
|
|
29
|
+
VALUE klass;
|
|
30
|
+
|
|
31
|
+
// The Allocations wrapper:
|
|
32
|
+
VALUE allocations;
|
|
33
|
+
|
|
34
|
+
// The newly allocated object:
|
|
35
|
+
VALUE object;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Queue item - freed object data to be processed via postponed job
|
|
39
|
+
struct Memory_Profiler_Freeobj_Queue_Item {
|
|
28
40
|
// The class of the freed object:
|
|
29
41
|
VALUE klass;
|
|
30
42
|
|
|
@@ -40,13 +52,19 @@ struct Memory_Profiler_Capture {
|
|
|
40
52
|
// class => VALUE (wrapped Memory_Profiler_Capture_Allocations).
|
|
41
53
|
st_table *tracked_classes;
|
|
42
54
|
|
|
43
|
-
//
|
|
55
|
+
// Master switch - is tracking active? (set by start/stop)
|
|
56
|
+
int running;
|
|
57
|
+
|
|
58
|
+
// Internal - should we queue callbacks? (temporarily disabled during queue processing)
|
|
44
59
|
int enabled;
|
|
45
60
|
|
|
46
|
-
// Queue for
|
|
47
|
-
struct Memory_Profiler_Queue
|
|
61
|
+
// Queue for new objects (processed via postponed job):
|
|
62
|
+
struct Memory_Profiler_Queue newobj_queue;
|
|
63
|
+
|
|
64
|
+
// Queue for freed objects (processed via postponed job):
|
|
65
|
+
struct Memory_Profiler_Queue freeobj_queue;
|
|
48
66
|
|
|
49
|
-
// Handle for the postponed job
|
|
67
|
+
// Handle for the postponed job (processes both queues)
|
|
50
68
|
rb_postponed_job_handle_t postponed_job_handle;
|
|
51
69
|
};
|
|
52
70
|
|
|
@@ -75,9 +93,17 @@ static void Memory_Profiler_Capture_mark(void *ptr) {
|
|
|
75
93
|
st_foreach(capture->tracked_classes, Memory_Profiler_Capture_tracked_classes_mark, 0);
|
|
76
94
|
}
|
|
77
95
|
|
|
96
|
+
// Mark new objects in the queue:
|
|
97
|
+
for (size_t i = 0; i < capture->newobj_queue.count; i++) {
|
|
98
|
+
struct Memory_Profiler_Newobj_Queue_Item *newobj = Memory_Profiler_Queue_at(&capture->newobj_queue, i);
|
|
99
|
+
rb_gc_mark_movable(newobj->klass);
|
|
100
|
+
rb_gc_mark_movable(newobj->allocations);
|
|
101
|
+
rb_gc_mark_movable(newobj->object);
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
// Mark freed objects in the queue:
|
|
79
|
-
for (size_t i = 0; i < capture->
|
|
80
|
-
struct
|
|
105
|
+
for (size_t i = 0; i < capture->freeobj_queue.count; i++) {
|
|
106
|
+
struct Memory_Profiler_Freeobj_Queue_Item *freed = Memory_Profiler_Queue_at(&capture->freeobj_queue, i);
|
|
81
107
|
rb_gc_mark_movable(freed->klass);
|
|
82
108
|
rb_gc_mark_movable(freed->allocations);
|
|
83
109
|
|
|
@@ -95,8 +121,9 @@ static void Memory_Profiler_Capture_free(void *ptr) {
|
|
|
95
121
|
st_free_table(capture->tracked_classes);
|
|
96
122
|
}
|
|
97
123
|
|
|
98
|
-
// Free
|
|
99
|
-
Memory_Profiler_Queue_free(&capture->
|
|
124
|
+
// Free both queues (elements are stored directly, just free the queues)
|
|
125
|
+
Memory_Profiler_Queue_free(&capture->newobj_queue);
|
|
126
|
+
Memory_Profiler_Queue_free(&capture->freeobj_queue);
|
|
100
127
|
|
|
101
128
|
xfree(capture);
|
|
102
129
|
}
|
|
@@ -110,8 +137,9 @@ static size_t Memory_Profiler_Capture_memsize(const void *ptr) {
|
|
|
110
137
|
size += capture->tracked_classes->num_entries * (sizeof(st_data_t) + sizeof(struct Memory_Profiler_Capture_Allocations));
|
|
111
138
|
}
|
|
112
139
|
|
|
113
|
-
// Add size of
|
|
114
|
-
size += capture->
|
|
140
|
+
// Add size of both queues (elements stored directly)
|
|
141
|
+
size += capture->newobj_queue.capacity * capture->newobj_queue.element_size;
|
|
142
|
+
size += capture->freeobj_queue.capacity * capture->freeobj_queue.element_size;
|
|
115
143
|
|
|
116
144
|
return size;
|
|
117
145
|
}
|
|
@@ -152,9 +180,19 @@ static void Memory_Profiler_Capture_compact(void *ptr) {
|
|
|
152
180
|
}
|
|
153
181
|
}
|
|
154
182
|
|
|
183
|
+
// Update new objects in the queue
|
|
184
|
+
for (size_t i = 0; i < capture->newobj_queue.count; i++) {
|
|
185
|
+
struct Memory_Profiler_Newobj_Queue_Item *newobj = Memory_Profiler_Queue_at(&capture->newobj_queue, i);
|
|
186
|
+
|
|
187
|
+
// Update all VALUEs if they moved during compaction
|
|
188
|
+
newobj->klass = rb_gc_location(newobj->klass);
|
|
189
|
+
newobj->allocations = rb_gc_location(newobj->allocations);
|
|
190
|
+
newobj->object = rb_gc_location(newobj->object);
|
|
191
|
+
}
|
|
192
|
+
|
|
155
193
|
// Update freed objects in the queue
|
|
156
|
-
for (size_t i = 0; i < capture->
|
|
157
|
-
struct
|
|
194
|
+
for (size_t i = 0; i < capture->freeobj_queue.count; i++) {
|
|
195
|
+
struct Memory_Profiler_Freeobj_Queue_Item *freed = Memory_Profiler_Queue_at(&capture->freeobj_queue, i);
|
|
158
196
|
|
|
159
197
|
// Update all VALUEs if they moved during compaction
|
|
160
198
|
freed->klass = rb_gc_location(freed->klass);
|
|
@@ -192,18 +230,52 @@ const char *event_flag_name(rb_event_flag_t event_flag) {
|
|
|
192
230
|
}
|
|
193
231
|
}
|
|
194
232
|
|
|
195
|
-
// Postponed job callback - processes queued freed objects
|
|
196
|
-
// This runs
|
|
197
|
-
|
|
233
|
+
// Postponed job callback - processes queued new and freed objects
|
|
234
|
+
// This runs when it's safe to call Ruby code (not during allocation or GC)
|
|
235
|
+
// IMPORTANT: Process newobj queue first, then freeobj queue to maintain order
|
|
236
|
+
static void Memory_Profiler_Capture_process_queues(void *arg) {
|
|
198
237
|
VALUE self = (VALUE)arg;
|
|
199
238
|
struct Memory_Profiler_Capture *capture;
|
|
200
239
|
TypedData_Get_Struct(self, struct Memory_Profiler_Capture, &Memory_Profiler_Capture_type, capture);
|
|
201
240
|
|
|
202
|
-
if (
|
|
241
|
+
if (DEBUG_EVENT_QUEUES) fprintf(stderr, "Processing queues: %zu newobj, %zu freeobj\n",
|
|
242
|
+
capture->newobj_queue.count, capture->freeobj_queue.count);
|
|
243
|
+
|
|
244
|
+
// Disable tracking during queue processing to prevent infinite loop
|
|
245
|
+
// (rb_funcall can allocate, which would trigger more NEWOBJ events)
|
|
246
|
+
int was_enabled = capture->enabled;
|
|
247
|
+
capture->enabled = 0;
|
|
248
|
+
|
|
249
|
+
// First, process all new objects in the queue
|
|
250
|
+
for (size_t i = 0; i < capture->newobj_queue.count; i++) {
|
|
251
|
+
struct Memory_Profiler_Newobj_Queue_Item *newobj = Memory_Profiler_Queue_at(&capture->newobj_queue, i);
|
|
252
|
+
VALUE klass = newobj->klass;
|
|
253
|
+
VALUE allocations = newobj->allocations;
|
|
254
|
+
VALUE object = newobj->object;
|
|
255
|
+
|
|
256
|
+
struct Memory_Profiler_Capture_Allocations *record = Memory_Profiler_Allocations_get(allocations);
|
|
257
|
+
|
|
258
|
+
// Call the Ruby callback with (klass, :newobj, nil) - callback returns state to store
|
|
259
|
+
if (!NIL_P(record->callback)) {
|
|
260
|
+
VALUE state = rb_funcall(record->callback, rb_intern("call"), 3, klass, sym_newobj, Qnil);
|
|
261
|
+
|
|
262
|
+
// Store the state if callback returned something
|
|
263
|
+
if (!NIL_P(state)) {
|
|
264
|
+
if (!record->object_states) {
|
|
265
|
+
record->object_states = st_init_numtable();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (DEBUG_STATE) fprintf(stderr, "Memory_Profiler_Capture_process_queues: Storing state for object: %p (%s)\n",
|
|
269
|
+
(void *)object, rb_class2name(klass));
|
|
270
|
+
|
|
271
|
+
st_insert(record->object_states, (st_data_t)object, (st_data_t)state);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
203
275
|
|
|
204
|
-
//
|
|
205
|
-
for (size_t i = 0; i < capture->
|
|
206
|
-
struct
|
|
276
|
+
// Then, process all freed objects in the queue
|
|
277
|
+
for (size_t i = 0; i < capture->freeobj_queue.count; i++) {
|
|
278
|
+
struct Memory_Profiler_Freeobj_Queue_Item *freed = Memory_Profiler_Queue_at(&capture->freeobj_queue, i);
|
|
207
279
|
VALUE klass = freed->klass;
|
|
208
280
|
VALUE allocations = freed->allocations;
|
|
209
281
|
VALUE state = freed->state;
|
|
@@ -216,41 +288,46 @@ static void Memory_Profiler_Capture_process_freed_queue(void *arg) {
|
|
|
216
288
|
}
|
|
217
289
|
}
|
|
218
290
|
|
|
219
|
-
// Clear
|
|
220
|
-
Memory_Profiler_Queue_clear(&capture->
|
|
291
|
+
// Clear both queues (elements are reused on next cycle)
|
|
292
|
+
Memory_Profiler_Queue_clear(&capture->newobj_queue);
|
|
293
|
+
Memory_Profiler_Queue_clear(&capture->freeobj_queue);
|
|
294
|
+
|
|
295
|
+
// Restore tracking state
|
|
296
|
+
capture->enabled = was_enabled;
|
|
221
297
|
}
|
|
222
298
|
|
|
223
299
|
// Handler for NEWOBJ event
|
|
300
|
+
// SAFE: No longer calls Ruby code directly - queues for deferred processing
|
|
224
301
|
static void Memory_Profiler_Capture_newobj_handler(VALUE self, struct Memory_Profiler_Capture *capture, VALUE klass, VALUE object) {
|
|
225
302
|
st_data_t allocations_data;
|
|
226
303
|
if (st_lookup(capture->tracked_classes, (st_data_t)klass, &allocations_data)) {
|
|
227
304
|
VALUE allocations = (VALUE)allocations_data;
|
|
228
305
|
struct Memory_Profiler_Capture_Allocations *record = Memory_Profiler_Allocations_get(allocations);
|
|
306
|
+
|
|
307
|
+
// Always track counts (even during queue processing)
|
|
229
308
|
record->new_count++;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// Call with (klass, :newobj, nil) - callback returns state to store
|
|
240
|
-
VALUE state = rb_funcall(record->callback, rb_intern("call"), 3, klass, sym_newobj, Qnil);
|
|
241
|
-
|
|
242
|
-
// Store the state if callback returned something
|
|
243
|
-
if (!NIL_P(state)) {
|
|
244
|
-
if (!record->object_states) {
|
|
245
|
-
record->object_states = st_init_numtable();
|
|
246
|
-
}
|
|
309
|
+
|
|
310
|
+
// Only queue for callback if tracking is enabled (prevents infinite recursion)
|
|
311
|
+
if (capture->enabled && !NIL_P(record->callback)) {
|
|
312
|
+
// Push a new item onto the queue (returns pointer to write to)
|
|
313
|
+
// NOTE: realloc is safe during allocation (doesn't trigger Ruby allocation)
|
|
314
|
+
struct Memory_Profiler_Newobj_Queue_Item *newobj = Memory_Profiler_Queue_push(&capture->newobj_queue);
|
|
315
|
+
if (newobj) {
|
|
316
|
+
if (DEBUG_EVENT_QUEUES) fprintf(stderr, "Queued newobj, queue size now: %zu/%zu\n",
|
|
317
|
+
capture->newobj_queue.count, capture->newobj_queue.capacity);
|
|
247
318
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
319
|
+
// Write VALUEs with write barriers (combines write + GC notification)
|
|
320
|
+
RB_OBJ_WRITE(self, &newobj->klass, klass);
|
|
321
|
+
RB_OBJ_WRITE(self, &newobj->allocations, allocations);
|
|
322
|
+
RB_OBJ_WRITE(self, &newobj->object, object);
|
|
323
|
+
|
|
324
|
+
// Trigger postponed job to process the queue
|
|
325
|
+
if (DEBUG_EVENT_QUEUES) fprintf(stderr, "Triggering postponed job to process queues\n");
|
|
326
|
+
rb_postponed_job_trigger(capture->postponed_job_handle);
|
|
327
|
+
} else {
|
|
328
|
+
if (DEBUG_EVENT_QUEUES) fprintf(stderr, "Failed to queue newobj, out of memory\n");
|
|
253
329
|
}
|
|
330
|
+
// If push failed (out of memory), silently drop this newobj event
|
|
254
331
|
}
|
|
255
332
|
} else {
|
|
256
333
|
// Create record for this class (first time seeing it)
|
|
@@ -279,10 +356,13 @@ static void Memory_Profiler_Capture_freeobj_handler(VALUE self, struct Memory_Pr
|
|
|
279
356
|
if (st_lookup(capture->tracked_classes, (st_data_t)klass, &allocations_data)) {
|
|
280
357
|
VALUE allocations = (VALUE)allocations_data;
|
|
281
358
|
struct Memory_Profiler_Capture_Allocations *record = Memory_Profiler_Allocations_get(allocations);
|
|
359
|
+
|
|
360
|
+
// Always track counts (even during queue processing)
|
|
282
361
|
record->free_count++;
|
|
283
362
|
|
|
284
|
-
//
|
|
285
|
-
|
|
363
|
+
// Only queue for callback if tracking is enabled and we have state
|
|
364
|
+
// Note: If NEWOBJ didn't queue (enabled=0), there's no state, so this naturally skips
|
|
365
|
+
if (capture->enabled && !NIL_P(record->callback) && record->object_states) {
|
|
286
366
|
if (DEBUG_STATE) fprintf(stderr, "Memory_Profiler_Capture_freeobj_handler: Looking up state for object: %p\n", (void *)object);
|
|
287
367
|
|
|
288
368
|
// Look up state stored during NEWOBJ
|
|
@@ -293,19 +373,22 @@ static void Memory_Profiler_Capture_freeobj_handler(VALUE self, struct Memory_Pr
|
|
|
293
373
|
|
|
294
374
|
// Push a new item onto the queue (returns pointer to write to)
|
|
295
375
|
// NOTE: realloc is safe during GC (doesn't trigger Ruby allocation)
|
|
296
|
-
struct
|
|
297
|
-
if (
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
freed->klass = klass;
|
|
301
|
-
freed->allocations = allocations;
|
|
302
|
-
freed->state = state;
|
|
376
|
+
struct Memory_Profiler_Freeobj_Queue_Item *freeobj = Memory_Profiler_Queue_push(&capture->freeobj_queue);
|
|
377
|
+
if (freeobj) {
|
|
378
|
+
if (DEBUG_EVENT_QUEUES) fprintf(stderr, "Queued freed object, queue size now: %zu/%zu\n",
|
|
379
|
+
capture->freeobj_queue.count, capture->freeobj_queue.capacity);
|
|
303
380
|
|
|
304
|
-
//
|
|
305
|
-
|
|
381
|
+
// Write VALUEs with write barriers (combines write + GC notification)
|
|
382
|
+
// Note: We're during GC/FREEOBJ, but write barriers should be safe
|
|
383
|
+
RB_OBJ_WRITE(self, &freeobj->klass, klass);
|
|
384
|
+
RB_OBJ_WRITE(self, &freeobj->allocations, allocations);
|
|
385
|
+
RB_OBJ_WRITE(self, &freeobj->state, state);
|
|
386
|
+
|
|
387
|
+
// Trigger postponed job to process both queues after GC
|
|
388
|
+
if (DEBUG_EVENT_QUEUES) fprintf(stderr, "Triggering postponed job to process queues after GC\n");
|
|
306
389
|
rb_postponed_job_trigger(capture->postponed_job_handle);
|
|
307
390
|
} else {
|
|
308
|
-
if (
|
|
391
|
+
if (DEBUG_EVENT_QUEUES) fprintf(stderr, "Failed to queue freed object, out of memory\n");
|
|
309
392
|
}
|
|
310
393
|
// If push failed (out of memory), silently drop this freeobj event
|
|
311
394
|
}
|
|
@@ -356,8 +439,6 @@ static void Memory_Profiler_Capture_event_callback(VALUE data, void *ptr) {
|
|
|
356
439
|
struct Memory_Profiler_Capture *capture;
|
|
357
440
|
TypedData_Get_Struct(data, struct Memory_Profiler_Capture, &Memory_Profiler_Capture_type, capture);
|
|
358
441
|
|
|
359
|
-
if (!capture->enabled) return;
|
|
360
|
-
|
|
361
442
|
VALUE object = rb_tracearg_object(trace_arg);
|
|
362
443
|
|
|
363
444
|
// We don't want to track internal non-Object allocations:
|
|
@@ -401,16 +482,19 @@ static VALUE Memory_Profiler_Capture_alloc(VALUE klass) {
|
|
|
401
482
|
rb_raise(rb_eRuntimeError, "Failed to initialize hash table");
|
|
402
483
|
}
|
|
403
484
|
|
|
485
|
+
// Initialize state flags - not running, callbacks disabled
|
|
486
|
+
capture->running = 0;
|
|
404
487
|
capture->enabled = 0;
|
|
405
488
|
|
|
406
|
-
// Initialize
|
|
407
|
-
Memory_Profiler_Queue_initialize(&capture->
|
|
489
|
+
// Initialize both queues
|
|
490
|
+
Memory_Profiler_Queue_initialize(&capture->newobj_queue, sizeof(struct Memory_Profiler_Newobj_Queue_Item));
|
|
491
|
+
Memory_Profiler_Queue_initialize(&capture->freeobj_queue, sizeof(struct Memory_Profiler_Freeobj_Queue_Item));
|
|
408
492
|
|
|
409
|
-
// Pre-register the postponed job for processing
|
|
410
|
-
// The job will be triggered whenever we queue
|
|
493
|
+
// Pre-register the postponed job for processing both queues
|
|
494
|
+
// The job will be triggered whenever we queue newobj or freeobj events
|
|
411
495
|
capture->postponed_job_handle = rb_postponed_job_preregister(
|
|
412
496
|
0, // flags
|
|
413
|
-
|
|
497
|
+
Memory_Profiler_Capture_process_queues,
|
|
414
498
|
(void *)obj
|
|
415
499
|
);
|
|
416
500
|
|
|
@@ -431,7 +515,7 @@ static VALUE Memory_Profiler_Capture_start(VALUE self) {
|
|
|
431
515
|
struct Memory_Profiler_Capture *capture;
|
|
432
516
|
TypedData_Get_Struct(self, struct Memory_Profiler_Capture, &Memory_Profiler_Capture_type, capture);
|
|
433
517
|
|
|
434
|
-
if (capture->
|
|
518
|
+
if (capture->running) return Qfalse;
|
|
435
519
|
|
|
436
520
|
// Add event hook for NEWOBJ and FREEOBJ with RAW_ARG to get trace_arg
|
|
437
521
|
rb_add_event_hook2(
|
|
@@ -441,6 +525,8 @@ static VALUE Memory_Profiler_Capture_start(VALUE self) {
|
|
|
441
525
|
RUBY_EVENT_HOOK_FLAG_SAFE | RUBY_EVENT_HOOK_FLAG_RAW_ARG
|
|
442
526
|
);
|
|
443
527
|
|
|
528
|
+
// Set both flags - we're now running and callbacks are enabled
|
|
529
|
+
capture->running = 1;
|
|
444
530
|
capture->enabled = 1;
|
|
445
531
|
|
|
446
532
|
return Qtrue;
|
|
@@ -451,11 +537,16 @@ static VALUE Memory_Profiler_Capture_stop(VALUE self) {
|
|
|
451
537
|
struct Memory_Profiler_Capture *capture;
|
|
452
538
|
TypedData_Get_Struct(self, struct Memory_Profiler_Capture, &Memory_Profiler_Capture_type, capture);
|
|
453
539
|
|
|
454
|
-
if (!capture->
|
|
540
|
+
if (!capture->running) return Qfalse;
|
|
455
541
|
|
|
456
|
-
// Remove event hook using same data (self) we registered with
|
|
542
|
+
// Remove event hook using same data (self) we registered with. No more events will be queued after this point:
|
|
457
543
|
rb_remove_event_hook_with_data((rb_event_hook_func_t)Memory_Profiler_Capture_event_callback, self);
|
|
458
544
|
|
|
545
|
+
// Flush any pending queued events before stopping. This ensures all callbacks are invoked and object_states is properly maintained.
|
|
546
|
+
Memory_Profiler_Capture_process_queues((void *)self);
|
|
547
|
+
|
|
548
|
+
// Clear both flags - we're no longer running and callbacks are disabled
|
|
549
|
+
capture->running = 0;
|
|
459
550
|
capture->enabled = 0;
|
|
460
551
|
|
|
461
552
|
return Qtrue;
|
|
@@ -52,6 +52,63 @@ module Memory
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Prune this node's children, keeping only the top N by retained count.
|
|
56
|
+
# Prunes current level first, then recursively prunes retained children (top-down).
|
|
57
|
+
#
|
|
58
|
+
# @parameter limit [Integer] Number of children to keep.
|
|
59
|
+
# @returns [Integer] Total number of nodes pruned (discarded).
|
|
60
|
+
def prune!(limit)
|
|
61
|
+
return 0 if @children.nil?
|
|
62
|
+
|
|
63
|
+
pruned_count = 0
|
|
64
|
+
|
|
65
|
+
# Prune at this level first - keep only top N children by retained count
|
|
66
|
+
if @children.size > limit
|
|
67
|
+
sorted = @children.sort_by do |_location, child|
|
|
68
|
+
-child.retained_count # Sort descending
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Detach and count discarded subtrees before we discard them:
|
|
72
|
+
discarded = sorted.drop(limit)
|
|
73
|
+
discarded.each do |_location, child|
|
|
74
|
+
# detach! breaks references to aid GC and returns node count
|
|
75
|
+
pruned_count += child.detach!
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@children = sorted.first(limit).to_h
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Now recursively prune the retained children (avoid pruning nodes we just discarded)
|
|
82
|
+
@children.each_value {|child| pruned_count += child.prune!(limit)}
|
|
83
|
+
|
|
84
|
+
# Clean up if we ended up with no children
|
|
85
|
+
@children = nil if @children.empty?
|
|
86
|
+
|
|
87
|
+
pruned_count
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Detach this node from the tree, breaking parent/child relationships.
|
|
91
|
+
# This helps GC collect pruned nodes that might be retained in object_states.
|
|
92
|
+
#
|
|
93
|
+
# Recursively detaches all descendants and returns total nodes detached.
|
|
94
|
+
#
|
|
95
|
+
# @returns [Integer] Number of nodes detached (including self).
|
|
96
|
+
def detach!
|
|
97
|
+
count = 1 # Self
|
|
98
|
+
|
|
99
|
+
# Recursively detach all children first and sum their counts
|
|
100
|
+
if @children
|
|
101
|
+
@children.each_value {|child| count += child.detach!}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Break all references
|
|
105
|
+
@parent = nil
|
|
106
|
+
@children = nil
|
|
107
|
+
@location = nil
|
|
108
|
+
|
|
109
|
+
return count
|
|
110
|
+
end
|
|
111
|
+
|
|
55
112
|
# Check if this node is a leaf (end of a call path).
|
|
56
113
|
#
|
|
57
114
|
# @returns [Boolean] True if this node has no children.
|
|
@@ -95,8 +152,12 @@ module Memory
|
|
|
95
152
|
# Create a new call tree for tracking allocation paths.
|
|
96
153
|
def initialize
|
|
97
154
|
@root = Node.new
|
|
155
|
+
@insertion_count = 0
|
|
98
156
|
end
|
|
99
157
|
|
|
158
|
+
# @attribute [Integer] Number of insertions (allocations) recorded in this tree.
|
|
159
|
+
attr_accessor :insertion_count
|
|
160
|
+
|
|
100
161
|
# Record an allocation with the given caller locations.
|
|
101
162
|
#
|
|
102
163
|
# @parameter caller_locations [Array<Thread::Backtrace::Location>] The call stack.
|
|
@@ -114,6 +175,9 @@ module Memory
|
|
|
114
175
|
# Increment counts for entire path (from leaf back to root):
|
|
115
176
|
current.increment_path!
|
|
116
177
|
|
|
178
|
+
# Track total insertions
|
|
179
|
+
@insertion_count += 1
|
|
180
|
+
|
|
117
181
|
# Return leaf node for object tracking:
|
|
118
182
|
current
|
|
119
183
|
end
|
|
@@ -122,7 +186,7 @@ module Memory
|
|
|
122
186
|
#
|
|
123
187
|
# @parameter limit [Integer] Maximum number of paths to return.
|
|
124
188
|
# @parameter by [Symbol] Sort by :total or :retained count.
|
|
125
|
-
# @returns [Array
|
|
189
|
+
# @returns [Array(Array)] Array of [locations, total_count, retained_count].
|
|
126
190
|
def top_paths(limit = 10, by: :retained)
|
|
127
191
|
paths = []
|
|
128
192
|
|
|
@@ -169,6 +233,16 @@ module Memory
|
|
|
169
233
|
# Clear all tracking data
|
|
170
234
|
def clear!
|
|
171
235
|
@root = Node.new
|
|
236
|
+
@insertion_count = 0
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Prune the tree to keep only the top N children at each level.
|
|
240
|
+
# This controls memory usage by removing low-retained branches.
|
|
241
|
+
#
|
|
242
|
+
# @parameter limit [Integer] Number of children to keep per node (default: 5).
|
|
243
|
+
# @returns [Integer] Total number of nodes pruned (discarded).
|
|
244
|
+
def prune!(limit = 5)
|
|
245
|
+
@root.prune!(limit)
|
|
172
246
|
end
|
|
173
247
|
|
|
174
248
|
private
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
require "console"
|
|
7
|
+
|
|
6
8
|
require_relative "capture"
|
|
7
9
|
require_relative "call_tree"
|
|
8
10
|
|
|
@@ -90,10 +92,14 @@ module Memory
|
|
|
90
92
|
# @parameter depth [Integer] Number of stack frames to capture for call path analysis.
|
|
91
93
|
# @parameter filter [Proc] Optional filter to exclude frames from call paths.
|
|
92
94
|
# @parameter increases_threshold [Integer] Number of increases before enabling detailed tracking.
|
|
93
|
-
|
|
95
|
+
# @parameter prune_limit [Integer] Keep only top N children per node during pruning (default: 5).
|
|
96
|
+
# @parameter prune_threshold [Integer] Number of insertions before auto-pruning (nil = no auto-pruning).
|
|
97
|
+
def initialize(depth: 4, filter: nil, increases_threshold: 10, prune_limit: 5, prune_threshold: nil)
|
|
94
98
|
@depth = depth
|
|
95
99
|
@filter = filter || default_filter
|
|
96
100
|
@increases_threshold = increases_threshold
|
|
101
|
+
@prune_limit = prune_limit
|
|
102
|
+
@prune_threshold = prune_threshold
|
|
97
103
|
@capture = Capture.new
|
|
98
104
|
@call_trees = {}
|
|
99
105
|
@samples = {}
|
|
@@ -148,6 +154,9 @@ module Memory
|
|
|
148
154
|
yield sample if block_given?
|
|
149
155
|
end
|
|
150
156
|
end
|
|
157
|
+
|
|
158
|
+
# Prune call trees to control memory usage
|
|
159
|
+
prune_call_trees!
|
|
151
160
|
end
|
|
152
161
|
|
|
153
162
|
# Start tracking with call path analysis.
|
|
@@ -256,11 +265,37 @@ module Memory
|
|
|
256
265
|
@call_trees.clear
|
|
257
266
|
end
|
|
258
267
|
|
|
259
|
-
|
|
268
|
+
private
|
|
260
269
|
|
|
261
270
|
def default_filter
|
|
262
271
|
->(location) {!location.path.match?(%r{/(gems|ruby)/|\A\(eval\)})}
|
|
263
272
|
end
|
|
273
|
+
|
|
274
|
+
def prune_call_trees!
|
|
275
|
+
return if @prune_threshold.nil?
|
|
276
|
+
|
|
277
|
+
@call_trees.each do |klass, tree|
|
|
278
|
+
# Only prune if insertions exceed threshold:
|
|
279
|
+
insertions = tree.insertion_count
|
|
280
|
+
next if insertions < @prune_threshold
|
|
281
|
+
|
|
282
|
+
# Prune the tree
|
|
283
|
+
pruned_count = tree.prune!(@prune_limit)
|
|
284
|
+
|
|
285
|
+
# Reset insertion counter after pruning
|
|
286
|
+
tree.insertion_count = 0
|
|
287
|
+
|
|
288
|
+
# Log pruning activity for visibility
|
|
289
|
+
if pruned_count > 0 && defined?(Console)
|
|
290
|
+
Console.debug(klass, "Pruned call tree:",
|
|
291
|
+
pruned_nodes: pruned_count,
|
|
292
|
+
insertions_since_last_prune: insertions,
|
|
293
|
+
total: tree.total_allocations,
|
|
294
|
+
retained: tree.retained_allocations
|
|
295
|
+
)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
264
299
|
end
|
|
265
300
|
end
|
|
266
301
|
end
|
data/readme.md
CHANGED
|
@@ -22,9 +22,19 @@ Please see the [project documentation](https://socketry.github.io/memory-profile
|
|
|
22
22
|
|
|
23
23
|
Please see the [project releases](https://socketry.github.io/memory-profiler/releases/index) for all releases.
|
|
24
24
|
|
|
25
|
+
### v1.1.6
|
|
26
|
+
|
|
27
|
+
- Write barriers all the things.
|
|
28
|
+
- Better state handling and object increment/decrement counting.
|
|
29
|
+
- Better call tree handling - including support for `prune!`.
|
|
30
|
+
|
|
31
|
+
### v1.1.5
|
|
32
|
+
|
|
33
|
+
- Use queue for `newobj` too to avoid invoking user code during object allocation.
|
|
34
|
+
|
|
25
35
|
### v1.1.2
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
- Fix handling of GC compaction (I hope).
|
|
28
38
|
|
|
29
39
|
### v0.1.0
|
|
30
40
|
|
data/releases.md
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v1.1.6
|
|
4
|
+
|
|
5
|
+
- Write barriers all the things.
|
|
6
|
+
- Better state handling and object increment/decrement counting.
|
|
7
|
+
- Better call tree handling - including support for `prune!`.
|
|
8
|
+
|
|
9
|
+
## v1.1.5
|
|
10
|
+
|
|
11
|
+
- Use queue for `newobj` too to avoid invoking user code during object allocation.
|
|
12
|
+
|
|
3
13
|
## v1.1.2
|
|
4
14
|
|
|
5
|
-
|
|
15
|
+
- Fix handling of GC compaction (I hope).
|
|
6
16
|
|
|
7
17
|
## v0.1.0
|
|
8
18
|
|
data.tar.gz.sig
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
�
|
|
4
|
-
�|L�y�8��I�/D�gƎ@pm��1ɪL�٦����bm!�ח��uu��PYx�ǚ]�k/9�A=�Ɋ
|
|
5
|
-
�5u�
|
|
6
|
-
�9!�2G�����*����u\d�!.�O_.��� %�̠����
|
|
7
|
-
�1ܧ��~��S?�ywX,Z�\>e�J�nM���퉦ja�Յ/�Qk���L���B��Ne6���سa��TE�����'�J$ke�`e��p7���n������Et �͝�2 l����M;�d��D>$,�����%Yd��9]L���{
|
|
1
|
+
5�Ԝ��(�8��x��^i;��^hC��o���V�����9���WBK�Lu�s�ҕ��g3��Ι���'@�P�
|
|
2
|
+
�
|
|
3
|
+
Sb'ɻ�mao�E�^D���ѫ�-�g����W� ��W����'�����>��E��AK������a��I�+�0٢<+�|�IЂ�%��'x|��!?�if)e*�vf���+���f����Py,#��z����6}��!0\*+*v���&����yL�����x^�})kt)�-+̠����YZ�enM��`葉�������.�j�Ǫ��Fiϕ�=���q.�f���G_��$w8��벡q9��O�N�P��:��>���ɯYO�\�N��㯴
|
metadata
CHANGED
metadata.gz.sig
CHANGED
|
Binary file
|