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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c70385bac57de768e1f6eb3ca9044942a357d77c97967b30636acd89b1f051f4
4
- data.tar.gz: 8c7ae2a117684e61d34e09e9b2ebb4a99ab440b1b31fee364d2a92bc6caf5cac
3
+ metadata.gz: ea6e765ad50aed3c3cf538d5c1a44f102f1bbe5251d5fd9c0995dd9d6890282a
4
+ data.tar.gz: dac87b8b0bd69f57ce3a290ef84f6eae56192ab422e554f861a2cb89b3cfa290
5
5
  SHA512:
6
- metadata.gz: 50d848d974700ab480c805c82decf0fa58806be6f087ab4a199fa6b88963c303e459ca7a3ea2dabfea9d05bc55972edc1912917f49b46dc2752f1ea8f6d23f4b
7
- data.tar.gz: 248a8fded6707058ccea2f9e1ee8eef074af4f8afeb5abfa9e786ef0e8c05efbd12fc8139eb887a156a84eaa05aba35f4dec1907426153ba935d0339198a5532
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
- DEBUG_FREED_QUEUE = 0,
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 - freed object data to be processed after GC
27
- struct Memory_Profiler_Queue_Item {
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
- // Is tracking enabled (via start/stop):
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 freed objects (processed after GC via postponed job)
47
- struct Memory_Profiler_Queue freed_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->freed_queue.count; i++) {
80
- struct Memory_Profiler_Queue_Item *freed = Memory_Profiler_Queue_at(&capture->freed_queue, i);
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 the queue (elements are stored directly, just free the queue)
99
- Memory_Profiler_Queue_free(&capture->freed_queue);
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 freed queue (elements stored directly)
114
- size += capture->freed_queue.capacity * capture->freed_queue.element_size;
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->freed_queue.count; i++) {
157
- struct Memory_Profiler_Queue_Item *freed = Memory_Profiler_Queue_at(&capture->freed_queue, i);
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 after GC completes, when it's safe to call Ruby code
197
- static void Memory_Profiler_Capture_process_freed_queue(void *arg) {
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 (DEBUG_FREED_QUEUE) fprintf(stderr, "Processing freed queue with %zu entries\n", capture->freed_queue.count);
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
- // Process all freed objects in the queue
205
- for (size_t i = 0; i < capture->freed_queue.count; i++) {
206
- struct Memory_Profiler_Queue_Item *freed = Memory_Profiler_Queue_at(&capture->freed_queue, i);
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 the queue (elements are reused on next cycle)
220
- Memory_Profiler_Queue_clear(&capture->freed_queue);
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
- if (!NIL_P(record->callback)) {
231
- // Invoke callback - runs during NEWOBJ with GC disabled
232
- // CRITICAL CALLBACK REQUIREMENTS:
233
- // - Must be FAST (runs on EVERY allocation)
234
- // - Must NOT call GC.start (will deadlock)
235
- // - Must NOT block/sleep (stalls all allocations system-wide)
236
- // - Should NOT raise exceptions (will propagate to allocating code)
237
- // - Avoid allocating objects (causes re-entry)
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
- if (DEBUG_STATE) fprintf(stderr, "Memory_Profiler_Capture_newobj_handler: Inserting state for object: %p (%s)\n", (void *)object, rb_class2name(klass));
249
-
250
- st_insert(record->object_states, (st_data_t)object, (st_data_t)state);
251
- // Notify GC about the state VALUE stored in the table
252
- RB_OBJ_WRITTEN(self, Qnil, state);
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
- // If we have a callback and detailed tracking, queue the freeobj for later processing
285
- if (!NIL_P(record->callback) && record->object_states) {
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 Memory_Profiler_Queue_Item *freed = Memory_Profiler_Queue_push(&capture->freed_queue);
297
- if (freed) {
298
- if (DEBUG_FREED_QUEUE) fprintf(stderr, "Queued freed object, queue size now: %zu/%zu\n", capture->freed_queue.count, capture->freed_queue.capacity);
299
- // Write directly to the allocated space
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
- // Trigger postponed job to process the queue after GC
305
- if (DEBUG_FREED_QUEUE) fprintf(stderr, "Triggering postponed job to process the queue after GC\n");
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 (DEBUG_FREED_QUEUE) fprintf(stderr, "Failed to queue freed object, out of memory\n");
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 the freed object queue
407
- Memory_Profiler_Queue_initialize(&capture->freed_queue, sizeof(struct Memory_Profiler_Queue_Item));
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 freed objects
410
- // The job will be triggered whenever we queue freed objects during GC
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
- Memory_Profiler_Capture_process_freed_queue,
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->enabled) return Qfalse;
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->enabled) return Qfalse;
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<Array>] Array of [locations, total_count, retained_count].
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
- def initialize(depth: 10, filter: nil, increases_threshold: 10)
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
- private
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
@@ -7,7 +7,7 @@
7
7
  module Memory
8
8
  # @namespace
9
9
  module Profiler
10
- VERSION = "1.1.4"
10
+ VERSION = "1.1.6"
11
11
  end
12
12
  end
13
13
 
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
- - Fix handling of GC compaction (I hope).
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
- - Fix handling of GC compaction (I hope).
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
- (�Q"�� D���# >� t&ކN
2
- ��ӧ�6ʲW����~hELJ�dܺ}�����ZJг�dӇ �{Dy�)-�\&��'ԩ�}R��7`E�5n�s
3
- t�~
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'ɻ�maoE�^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
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memory-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 1.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file