memory-profiler 1.5.0 → 1.6.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: b936617787e483bcf790aee25bb224bf10a199f431dbb8ac51eff005504a44d4
4
- data.tar.gz: 587f38cef39c639c9d9e2efbffdd325cffdfbd3000dddbca46f5aba895a66a21
3
+ metadata.gz: 8a65991a6612d28ede2c4ab5652bcf410e7edf92a0f98e8bdeebd921b64232d6
4
+ data.tar.gz: 230a10b0d5ed42f74d19007534dbae1ee49df3c67d0507fcf42a0fc67b1cf5e3
5
5
  SHA512:
6
- metadata.gz: c483112cf1dd1c82ea679afe3b86007bf20b2674558ac676fc2c66cd07f651787a09198b9f49fb3dd28f1f9b64865dfac9cc249d91b6b819e3849e0cc9c9ab0b
7
- data.tar.gz: 0de0b5d28eb0f5af2b72ec0aa44ceaf8d0401822949ee4512f922b6caa730e1d69a6b2997fac633140168b3b8de93bdbf4c76223ae6ef789520ae5566ef23f08
6
+ metadata.gz: 7b64a8a6b0a500f84c52689aa5affad466d19e3ddc723db2014af3ef784fe35b7d6704aa2bf363f27c23d8fe75e27bdef5c7e969736a2bedcfc925467dd3e16a
7
+ data.tar.gz: da48ff77a1915aaa978ce162b6c44d310b6b4a77af59b6df18c3e6fc92b19aa40df99473bb461dafd0a160ded3c5721681cce3e22dece393d484c0b2c51e67b4
checksums.yaml.gz.sig CHANGED
Binary file
@@ -20,7 +20,6 @@ static VALUE Memory_Profiler_Capture = Qnil;
20
20
  // Event symbols:
21
21
  static VALUE sym_newobj, sym_freeobj;
22
22
 
23
-
24
23
  // Main capture state (per-instance).
25
24
  struct Memory_Profiler_Capture {
26
25
  // Master switch - is tracking active? (set by start/stop).
@@ -29,6 +28,9 @@ struct Memory_Profiler_Capture {
29
28
  // Should we queue callbacks? (temporarily disabled during queue processing).
30
29
  int paused;
31
30
 
31
+ // Should we automatically track all classes? (if false, only explicitly tracked classes are tracked).
32
+ int track_all;
33
+
32
34
  // Tracked classes: class => VALUE (wrapped Memory_Profiler_Capture_Allocations).
33
35
  st_table *tracked;
34
36
 
@@ -164,21 +166,18 @@ static void Memory_Profiler_Capture_process_newobj(VALUE self, VALUE klass, VALU
164
166
  // Pause the capture to prevent infinite loop:
165
167
  capture->paused += 1;
166
168
 
167
- // Increment global new count:
168
- capture->new_count++;
169
-
170
- // Look up or create allocations record for this class:
169
+ // Look up allocations record for this class:
171
170
  st_data_t allocations_data;
172
171
  VALUE allocations;
173
172
  struct Memory_Profiler_Capture_Allocations *record;
174
173
 
175
174
  if (st_lookup(capture->tracked, (st_data_t)klass, &allocations_data)) {
176
- // Existing record
175
+ // Existing record - class is explicitly tracked
177
176
  allocations = (VALUE)allocations_data;
178
177
  record = Memory_Profiler_Allocations_get(allocations);
179
178
  record->new_count++;
180
- } else {
181
- // First time seeing this class, create record automatically
179
+ } else if (capture->track_all) {
180
+ // First time seeing this class, create record automatically (if track_all is enabled)
182
181
  record = ALLOC(struct Memory_Profiler_Capture_Allocations);
183
182
  record->callback = Qnil;
184
183
  record->new_count = 1;
@@ -188,8 +187,15 @@ static void Memory_Profiler_Capture_process_newobj(VALUE self, VALUE klass, VALU
188
187
  st_insert(capture->tracked, (st_data_t)klass, (st_data_t)allocations);
189
188
  RB_OBJ_WRITTEN(self, Qnil, klass);
190
189
  RB_OBJ_WRITTEN(self, Qnil, allocations);
190
+ } else {
191
+ // track_all disabled and class not explicitly tracked - skip this allocation entirely
192
+ capture->paused -= 1;
193
+ return;
191
194
  }
192
195
 
196
+ // Increment global new count (only if we're tracking this class):
197
+ capture->new_count++;
198
+
193
199
  VALUE data = Qnil;
194
200
  if (!NIL_P(record->callback)) {
195
201
  data = rb_funcall(record->callback, rb_intern("call"), 3, klass, sym_newobj, Qnil);
@@ -199,7 +205,6 @@ static void Memory_Profiler_Capture_process_newobj(VALUE self, VALUE klass, VALU
199
205
  RB_OBJ_WRITTEN(self, Qnil, object);
200
206
  RB_OBJ_WRITE(self, &entry->klass, klass);
201
207
  RB_OBJ_WRITE(self, &entry->data, data);
202
- RB_OBJ_WRITE(self, &entry->allocations, allocations);
203
208
 
204
209
  if (DEBUG) fprintf(stderr, "[NEWOBJ] Object inserted into table: %p\n", (void*)object);
205
210
 
@@ -227,7 +232,15 @@ static void Memory_Profiler_Capture_process_freeobj(VALUE capture_value, VALUE u
227
232
 
228
233
  VALUE klass = entry->klass;
229
234
  VALUE data = entry->data;
230
- VALUE allocations = entry->allocations;
235
+
236
+ // Look up allocations from tracked table:
237
+ st_data_t allocations_data;
238
+ if (!st_lookup(capture->tracked, (st_data_t)klass, &allocations_data)) {
239
+ // Class not tracked - shouldn't happen, but be defensive:
240
+ if (DEBUG) fprintf(stderr, "[FREEOBJ] Class not found in tracked: %p\n", (void*)klass);
241
+ goto done;
242
+ }
243
+ VALUE allocations = (VALUE)allocations_data;
231
244
 
232
245
  // Delete by entry pointer (faster - no second lookup!)
233
246
  Memory_Profiler_Object_Table_delete_entry(capture->states, entry);
@@ -361,9 +374,10 @@ static VALUE Memory_Profiler_Capture_alloc(VALUE klass) {
361
374
  capture->new_count = 0;
362
375
  capture->free_count = 0;
363
376
 
364
- // Initialize state flags - not running, callbacks disabled
377
+ // Initialize state flags - not running, callbacks disabled, track_all disabled by default
365
378
  capture->running = 0;
366
379
  capture->paused = 0;
380
+ capture->track_all = 0;
367
381
 
368
382
  // Global event queue system will auto-initialize on first use (lazy initialization)
369
383
 
@@ -375,6 +389,24 @@ static VALUE Memory_Profiler_Capture_initialize(VALUE self) {
375
389
  return self;
376
390
  }
377
391
 
392
+ // Get track_all setting
393
+ static VALUE Memory_Profiler_Capture_track_all_get(VALUE self) {
394
+ struct Memory_Profiler_Capture *capture;
395
+ TypedData_Get_Struct(self, struct Memory_Profiler_Capture, &Memory_Profiler_Capture_type, capture);
396
+
397
+ return capture->track_all ? Qtrue : Qfalse;
398
+ }
399
+
400
+ // Set track_all setting
401
+ static VALUE Memory_Profiler_Capture_track_all_set(VALUE self, VALUE value) {
402
+ struct Memory_Profiler_Capture *capture;
403
+ TypedData_Get_Struct(self, struct Memory_Profiler_Capture, &Memory_Profiler_Capture_type, capture);
404
+
405
+ capture->track_all = RTEST(value) ? 1 : 0;
406
+
407
+ return value;
408
+ }
409
+
378
410
  // Start capturing allocations
379
411
  static VALUE Memory_Profiler_Capture_start(VALUE self) {
380
412
  struct Memory_Profiler_Capture *capture;
@@ -566,14 +598,10 @@ struct Memory_Profiler_Each_Object_Arguments {
566
598
  VALUE allocations;
567
599
  };
568
600
 
569
- // Cleanup function to ensure table is made weak again
601
+ // Cleanup function to re-enable GC
570
602
  static VALUE Memory_Profiler_Capture_each_object_ensure(VALUE arg) {
571
- struct Memory_Profiler_Each_Object_Arguments *arguments = (struct Memory_Profiler_Each_Object_Arguments *)arg;
572
- struct Memory_Profiler_Capture *capture;
573
- TypedData_Get_Struct(arguments->self, struct Memory_Profiler_Capture, &Memory_Profiler_Capture_type, capture);
574
-
575
- // Make table weak again
576
- Memory_Profiler_Object_Table_decrement_strong(capture->states);
603
+ // Re-enable GC (rb_gc_enable returns previous state, but we don't need it)
604
+ rb_gc_enable();
577
605
 
578
606
  return Qnil;
579
607
  }
@@ -596,12 +624,19 @@ static VALUE Memory_Profiler_Capture_each_object_body(VALUE arg) {
596
624
  continue;
597
625
  }
598
626
 
627
+ // Look up allocations from klass
628
+ st_data_t allocations_data;
629
+ VALUE allocations = Qnil;
630
+ if (st_lookup(capture->tracked, (st_data_t)entry->klass, &allocations_data)) {
631
+ allocations = (VALUE)allocations_data;
632
+ }
633
+
599
634
  // Filter by allocations if specified
600
635
  if (!NIL_P(arguments->allocations)) {
601
- if (entry->allocations != arguments->allocations) continue;
636
+ if (allocations != arguments->allocations) continue;
602
637
  }
603
638
 
604
- rb_yield_values(2, entry->object, entry->allocations);
639
+ rb_yield_values(2, entry->object, allocations);
605
640
  }
606
641
  }
607
642
 
@@ -626,10 +661,11 @@ static VALUE Memory_Profiler_Capture_each_object(int argc, VALUE *argv, VALUE se
626
661
 
627
662
  RETURN_ENUMERATOR(self, argc, argv);
628
663
 
629
- // Make table strong so objects won't be collected during iteration
630
- Memory_Profiler_Object_Table_increment_strong(capture->states);
664
+ // Disable GC to prevent objects from being collected during iteration
665
+ rb_gc_disable();
631
666
 
632
- // Process all pending events and run GC to clean up stale objects:
667
+ // Process all pending events to clean up stale entries
668
+ // At this point, all remaining objects in the table should be valid
633
669
  Memory_Profiler_Events_process_all();
634
670
 
635
671
  // If class provided, look up its allocations wrapper
@@ -640,7 +676,8 @@ static VALUE Memory_Profiler_Capture_each_object(int argc, VALUE *argv, VALUE se
640
676
  allocations = (VALUE)allocations_data;
641
677
  } else {
642
678
  // Class not tracked - nothing to iterate
643
- Memory_Profiler_Object_Table_decrement_strong(capture->states);
679
+ // Re-enable GC before returning
680
+ rb_gc_enable();
644
681
  return self;
645
682
  }
646
683
  }
@@ -733,6 +770,8 @@ void Init_Memory_Profiler_Capture(VALUE Memory_Profiler)
733
770
  rb_define_alloc_func(Memory_Profiler_Capture, Memory_Profiler_Capture_alloc);
734
771
 
735
772
  rb_define_method(Memory_Profiler_Capture, "initialize", Memory_Profiler_Capture_initialize, 0);
773
+ rb_define_method(Memory_Profiler_Capture, "track_all", Memory_Profiler_Capture_track_all_get, 0);
774
+ rb_define_method(Memory_Profiler_Capture, "track_all=", Memory_Profiler_Capture_track_all_set, 1);
736
775
  rb_define_method(Memory_Profiler_Capture, "start", Memory_Profiler_Capture_start, 0);
737
776
  rb_define_method(Memory_Profiler_Capture, "stop", Memory_Profiler_Capture_stop, 0);
738
777
  rb_define_method(Memory_Profiler_Capture, "track", Memory_Profiler_Capture_track, -1); // -1 to accept block
@@ -4,12 +4,27 @@
4
4
  #include "table.h"
5
5
  #include <stdlib.h>
6
6
  #include <string.h>
7
+ #include <stdio.h>
8
+
9
+ enum {
10
+ DEBUG = 1,
11
+
12
+ // Performance monitoring thresholds
13
+
14
+ // Log warning if probe chain exceeds this
15
+ WARN_PROBE_LENGTH = 100,
16
+
17
+ // Safety limit - abort search if exceeded
18
+ MAX_PROBE_LENGTH = 10000,
19
+ };
7
20
 
8
21
  // Use the Entry struct from header
9
22
  // (No local definition needed)
23
+ const size_t INITIAL_CAPACITY = 1024;
24
+ const float LOAD_FACTOR = 0.50; // Reduced from 0.75 to avoid clustering
10
25
 
11
- #define INITIAL_CAPACITY 1024
12
- #define LOAD_FACTOR 0.75
26
+
27
+ VALUE TOMBSTONE = Qnil;
13
28
 
14
29
  // Create a new table
15
30
  struct Memory_Profiler_Object_Table* Memory_Profiler_Object_Table_new(size_t initial_capacity) {
@@ -21,9 +36,9 @@ struct Memory_Profiler_Object_Table* Memory_Profiler_Object_Table_new(size_t ini
21
36
 
22
37
  table->capacity = initial_capacity > 0 ? initial_capacity : INITIAL_CAPACITY;
23
38
  table->count = 0;
24
- table->strong = 0; // Start as weak table (strong == 0 means weak)
39
+ table->tombstones = 0;
25
40
 
26
- // Use calloc to zero out entries (Qnil = 0)
41
+ // Use calloc to zero out entries (0 = empty slot)
27
42
  table->entries = calloc(table->capacity, sizeof(struct Memory_Profiler_Object_Table_Entry));
28
43
 
29
44
  if (!table->entries) {
@@ -42,41 +57,153 @@ void Memory_Profiler_Object_Table_free(struct Memory_Profiler_Object_Table *tabl
42
57
  }
43
58
  }
44
59
 
45
- // Simple hash function for object addresses
60
+ // Hash function for object addresses
61
+ // Uses multiplicative hashing with bit mixing to reduce clustering
46
62
  static inline size_t hash_object(VALUE object, size_t capacity) {
47
- // Use address bits, shift right to ignore alignment
48
- return ((size_t)object >> 3) % capacity;
63
+ size_t hash = (size_t)object;
64
+
65
+ // Remove alignment bits (objects are typically 8-byte aligned)
66
+ hash >>= 3;
67
+
68
+ // Multiplicative hashing (Knuth's golden ratio method)
69
+ // This helps distribute consecutive addresses across the table
70
+ hash *= 2654435761UL; // 2^32 / phi (golden ratio)
71
+
72
+ // Mix high bits into low bits for better distribution
73
+ hash ^= (hash >> 16);
74
+ hash *= 0x85ebca6b;
75
+ hash ^= (hash >> 13);
76
+ hash *= 0xc2b2ae35;
77
+ hash ^= (hash >> 16);
78
+
79
+ return hash % capacity;
49
80
  }
50
81
 
51
82
  // Find entry index for an object (linear probing)
52
83
  // Returns index if found, or index of empty slot if not found
53
- static size_t find_entry(struct Memory_Profiler_Object_Table_Entry *entries, size_t capacity, VALUE object, int *found) {
84
+ // If table is provided (not NULL), logs statistics when probe length is excessive
85
+ static size_t find_entry(struct Memory_Profiler_Object_Table_Entry *entries, size_t capacity, VALUE object, int *found, struct Memory_Profiler_Object_Table *table, const char *operation) {
54
86
  size_t index = hash_object(object, capacity);
55
87
  size_t start = index;
88
+ size_t probe_count = 0;
56
89
 
57
90
  *found = 0;
58
91
 
59
92
  do {
93
+ probe_count++;
94
+
95
+ // Safety check - prevent infinite loops
96
+ if (probe_count > MAX_PROBE_LENGTH) {
97
+ if (DEBUG && table) {
98
+ double load = (double)table->count / capacity;
99
+ double tomb_ratio = (double)table->tombstones / capacity;
100
+ fprintf(stderr, "{\"subject\":\"Memory::Profiler::ObjectTable\",\"level\":\"critical\",\"operation\":\"%s\",\"event\":\"max_probes_exceeded\",\"probe_count\":%zu,\"capacity\":%zu,\"count\":%zu,\"tombstones\":%zu,\"load_factor\":%.3f,\"tombstone_ratio\":%.3f}\n",
101
+ operation, probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
102
+ } else if (DEBUG) {
103
+ fprintf(stderr, "{\"subject\":\"Memory::Profiler::ObjectTable\",\"level\":\"critical\",\"operation\":\"%s\",\"event\":\"max_probes_exceeded\",\"probe_count\":%zu,\"capacity\":%zu}\n",
104
+ operation, probe_count, capacity);
105
+ }
106
+ return index;
107
+ }
108
+
109
+ // Log warning for excessive probing
110
+ if (DEBUG && probe_count == WARN_PROBE_LENGTH && table) {
111
+ double load = (double)table->count / capacity;
112
+ double tomb_ratio = (double)table->tombstones / capacity;
113
+ fprintf(stderr, "{\"subject\":\"Memory::Profiler::ObjectTable\",\"level\":\"warning\",\"operation\":\"%s\",\"event\":\"long_probe_chain\",\"probe_count\":%zu,\"capacity\":%zu,\"count\":%zu,\"tombstones\":%zu,\"load_factor\":%.3f,\"tombstone_ratio\":%.3f}\n",
114
+ operation, probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
115
+ }
116
+
60
117
  if (entries[index].object == 0) {
61
- // Empty slot (calloc zeros memory)
62
118
  return index;
63
119
  }
64
120
 
65
- if (entries[index].object == object) {
66
- // Found it
121
+ if (entries[index].object != TOMBSTONE && entries[index].object == object) {
67
122
  *found = 1;
68
123
  return index;
69
124
  }
70
125
 
71
- // Linear probe
72
126
  index = (index + 1) % capacity;
73
127
  } while (index != start);
74
128
 
75
- // Table is full (shouldn't happen with load factor)
129
+ // Table is full
130
+ if (DEBUG && table) {
131
+ double load = (double)table->count / capacity;
132
+ double tomb_ratio = (double)table->tombstones / capacity;
133
+ fprintf(stderr, "{\"subject\":\"Memory::Profiler::ObjectTable\",\"level\":\"error\",\"operation\":\"%s\",\"event\":\"table_full\",\"probe_count\":%zu,\"capacity\":%zu,\"count\":%zu,\"tombstones\":%zu,\"load_factor\":%.3f,\"tombstone_ratio\":%.3f}\n",
134
+ operation, probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
135
+ }
76
136
  return index;
77
137
  }
78
138
 
139
+ // Find slot for inserting an object (linear probing)
140
+ // Returns index to insert at - reuses tombstone slots if found
141
+ // If object exists, returns its index with found=1
142
+ static size_t find_insert_slot(struct Memory_Profiler_Object_Table *table, VALUE object, int *found) {
143
+ struct Memory_Profiler_Object_Table_Entry *entries = table->entries;
144
+ size_t capacity = table->capacity;
145
+ size_t index = hash_object(object, capacity);
146
+ size_t start = index;
147
+ size_t first_tombstone = SIZE_MAX; // Track first tombstone we encounter
148
+ size_t probe_count = 0;
149
+
150
+ *found = 0;
151
+
152
+ do {
153
+ probe_count++;
154
+
155
+ // Safety check - prevent infinite loops
156
+ if (probe_count > MAX_PROBE_LENGTH) {
157
+ if (DEBUG) {
158
+ double load = (double)table->count / capacity;
159
+ double tomb_ratio = (double)table->tombstones / capacity;
160
+ fprintf(stderr, "{\"subject\":\"Memory::Profiler::ObjectTable\",\"level\":\"critical\",\"operation\":\"insert\",\"event\":\"max_probes_exceeded\",\"probe_count\":%zu,\"capacity\":%zu,\"count\":%zu,\"tombstones\":%zu,\"load_factor\":%.3f,\"tombstone_ratio\":%.3f}\n",
161
+ probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
162
+ }
163
+ // Return tombstone if we found one, otherwise current position
164
+ return (first_tombstone != SIZE_MAX) ? first_tombstone : index;
165
+ }
166
+
167
+ // Log warning for excessive probing
168
+ if (DEBUG && probe_count == WARN_PROBE_LENGTH) {
169
+ double load = (double)table->count / capacity;
170
+ double tomb_ratio = (double)table->tombstones / capacity;
171
+ fprintf(stderr, "{\"subject\":\"Memory::Profiler::ObjectTable\",\"level\":\"warning\",\"operation\":\"insert\",\"event\":\"long_probe_chain\",\"probe_count\":%zu,\"capacity\":%zu,\"count\":%zu,\"tombstones\":%zu,\"load_factor\":%.3f,\"tombstone_ratio\":%.3f}\n",
172
+ probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
173
+ }
174
+
175
+ if (entries[index].object == 0) {
176
+ // Empty slot - use tombstone if we found one, otherwise this slot
177
+ return (first_tombstone != SIZE_MAX) ? first_tombstone : index;
178
+ }
179
+
180
+ if (entries[index].object == TOMBSTONE) {
181
+ // Remember first tombstone (but keep searching for existing object)
182
+ if (first_tombstone == SIZE_MAX) {
183
+ first_tombstone = index;
184
+ }
185
+ } else if (entries[index].object == object) {
186
+ // Found existing entry
187
+ *found = 1;
188
+ return index;
189
+ }
190
+
191
+ index = (index + 1) % capacity;
192
+ } while (index != start);
193
+
194
+ // Table is full
195
+ if (DEBUG) {
196
+ double load = (double)table->count / capacity;
197
+ double tomb_ratio = (double)table->tombstones / capacity;
198
+ fprintf(stderr, "{\"subject\":\"Memory::Profiler::ObjectTable\",\"level\":\"error\",\"operation\":\"insert\",\"event\":\"table_full\",\"probe_count\":%zu,\"capacity\":%zu,\"count\":%zu,\"tombstones\":%zu,\"load_factor\":%.3f,\"tombstone_ratio\":%.3f}\n",
199
+ probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
200
+ }
201
+ // Use tombstone slot if we found one
202
+ return (first_tombstone != SIZE_MAX) ? first_tombstone : index;
203
+ }
204
+
79
205
  // Resize the table (only called from insert, not during GC)
206
+ // This clears all tombstones
80
207
  static void resize_table(struct Memory_Profiler_Object_Table *table) {
81
208
  size_t old_capacity = table->capacity;
82
209
  struct Memory_Profiler_Object_Table_Entry *old_entries = table->entries;
@@ -84,6 +211,7 @@ static void resize_table(struct Memory_Profiler_Object_Table *table) {
84
211
  // Double capacity
85
212
  table->capacity = old_capacity * 2;
86
213
  table->count = 0;
214
+ table->tombstones = 0; // Reset tombstones
87
215
  table->entries = calloc(table->capacity, sizeof(struct Memory_Profiler_Object_Table_Entry));
88
216
 
89
217
  if (!table->entries) {
@@ -93,11 +221,12 @@ static void resize_table(struct Memory_Profiler_Object_Table *table) {
93
221
  return;
94
222
  }
95
223
 
96
- // Rehash all entries
224
+ // Rehash all non-tombstone entries
97
225
  for (size_t i = 0; i < old_capacity; i++) {
98
- if (old_entries[i].object != Qnil) {
226
+ // Skip empty slots and tombstones
227
+ if (old_entries[i].object != 0 && old_entries[i].object != TOMBSTONE) {
99
228
  int found;
100
- size_t new_index = find_entry(table->entries, table->capacity, old_entries[i].object, &found);
229
+ size_t new_index = find_entry(table->entries, table->capacity, old_entries[i].object, &found, NULL, "resize");
101
230
  table->entries[new_index] = old_entries[i];
102
231
  table->count++;
103
232
  }
@@ -108,26 +237,30 @@ static void resize_table(struct Memory_Profiler_Object_Table *table) {
108
237
 
109
238
  // Insert object, returns pointer to entry for caller to fill
110
239
  struct Memory_Profiler_Object_Table_Entry* Memory_Profiler_Object_Table_insert(struct Memory_Profiler_Object_Table *table, VALUE object) {
111
- // Resize if load factor exceeded
112
- if ((double)table->count / table->capacity > LOAD_FACTOR) {
240
+ // Resize if load factor exceeded (count + tombstones)
241
+ // This clears tombstones and gives us fresh space
242
+ if ((double)(table->count + table->tombstones) / table->capacity > LOAD_FACTOR) {
113
243
  resize_table(table);
114
244
  }
115
245
 
116
246
  int found;
117
- size_t index = find_entry(table->entries, table->capacity, object, &found);
247
+ size_t index = find_insert_slot(table, object, &found);
118
248
 
119
249
  if (!found) {
250
+ // New entry - check if we're reusing a tombstone slot
251
+ if (table->entries[index].object == TOMBSTONE) {
252
+ table->tombstones--; // Reusing tombstone
253
+ }
120
254
  table->count++;
121
255
  // Zero out the entry
122
256
  table->entries[index].object = object;
123
257
  table->entries[index].klass = 0;
124
258
  table->entries[index].data = 0;
125
- table->entries[index].allocations = 0;
259
+ } else {
260
+ // Updating existing entry
261
+ table->entries[index].object = object;
126
262
  }
127
263
 
128
- // Set object (might be updating existing entry)
129
- table->entries[index].object = object;
130
-
131
264
  // Return pointer for caller to fill fields
132
265
  return &table->entries[index];
133
266
  }
@@ -135,7 +268,7 @@ struct Memory_Profiler_Object_Table_Entry* Memory_Profiler_Object_Table_insert(s
135
268
  // Lookup entry for object - returns pointer or NULL
136
269
  struct Memory_Profiler_Object_Table_Entry* Memory_Profiler_Object_Table_lookup(struct Memory_Profiler_Object_Table *table, VALUE object) {
137
270
  int found;
138
- size_t index = find_entry(table->entries, table->capacity, object, &found);
271
+ size_t index = find_entry(table->entries, table->capacity, object, &found, table, "lookup");
139
272
 
140
273
  if (found) {
141
274
  return &table->entries[index];
@@ -147,43 +280,18 @@ struct Memory_Profiler_Object_Table_Entry* Memory_Profiler_Object_Table_lookup(s
147
280
  // Delete object from table
148
281
  void Memory_Profiler_Object_Table_delete(struct Memory_Profiler_Object_Table *table, VALUE object) {
149
282
  int found;
150
- size_t index = find_entry(table->entries, table->capacity, object, &found);
283
+ size_t index = find_entry(table->entries, table->capacity, object, &found, table, "delete");
151
284
 
152
285
  if (!found) {
153
286
  return;
154
287
  }
155
288
 
156
- // Mark as deleted (set to 0/NULL)
157
- table->entries[index].object = 0;
289
+ // Mark as tombstone - no rehashing needed!
290
+ table->entries[index].object = TOMBSTONE;
158
291
  table->entries[index].klass = 0;
159
292
  table->entries[index].data = 0;
160
- table->entries[index].allocations = 0;
161
293
  table->count--;
162
-
163
- // Rehash following entries to fix probe chains
164
- size_t next = (index + 1) % table->capacity;
165
- while (table->entries[next].object != 0) {
166
- // Save entry values
167
- VALUE obj = table->entries[next].object;
168
- VALUE k = table->entries[next].klass;
169
- VALUE d = table->entries[next].data;
170
- VALUE a = table->entries[next].allocations;
171
-
172
- // Remove this entry (set to 0/NULL)
173
- table->entries[next].object = 0;
174
- table->entries[next].klass = 0;
175
- table->entries[next].data = 0;
176
- table->entries[next].allocations = 0;
177
- table->count--;
178
-
179
- // Reinsert (will find correct spot and fill fields)
180
- struct Memory_Profiler_Object_Table_Entry *new_entry = Memory_Profiler_Object_Table_insert(table, obj);
181
- new_entry->klass = k;
182
- new_entry->data = d;
183
- new_entry->allocations = a;
184
-
185
- next = (next + 1) % table->capacity;
186
- }
294
+ table->tombstones++;
187
295
  }
188
296
 
189
297
  // Mark all entries for GC
@@ -192,17 +300,12 @@ void Memory_Profiler_Object_Table_mark(struct Memory_Profiler_Object_Table *tabl
192
300
 
193
301
  for (size_t i = 0; i < table->capacity; i++) {
194
302
  struct Memory_Profiler_Object_Table_Entry *entry = &table->entries[i];
195
- if (entry->object != 0) {
196
- // Mark object key if table is strong (strong > 0)
197
- // When weak (strong == 0), object keys can be GC'd (that's how we detect frees)
198
- if (table->strong > 0) {
199
- rb_gc_mark_movable(entry->object);
200
- }
201
-
202
- // Always mark the other fields (klass, data, allocations) - we own these
303
+ // Skip empty slots and tombstones
304
+ if (entry->object != 0 && entry->object != TOMBSTONE) {
305
+ // Don't mark object keys - table is weak (object keys can be GC'd, that's how we detect frees)
306
+ // Always mark the other fields (klass, data) - we own these
203
307
  if (entry->klass) rb_gc_mark_movable(entry->klass);
204
308
  if (entry->data) rb_gc_mark_movable(entry->data);
205
- if (entry->allocations) rb_gc_mark_movable(entry->allocations);
206
309
  }
207
310
  }
208
311
  }
@@ -214,7 +317,8 @@ void Memory_Profiler_Object_Table_compact(struct Memory_Profiler_Object_Table *t
214
317
  // First pass: check if any objects moved
215
318
  int any_moved = 0;
216
319
  for (size_t i = 0; i < table->capacity; i++) {
217
- if (table->entries[i].object != 0) {
320
+ // Skip empty slots and tombstones
321
+ if (table->entries[i].object != 0 && table->entries[i].object != TOMBSTONE) {
218
322
  VALUE new_loc = rb_gc_location(table->entries[i].object);
219
323
  if (new_loc != table->entries[i].object) {
220
324
  any_moved = 1;
@@ -226,11 +330,11 @@ void Memory_Profiler_Object_Table_compact(struct Memory_Profiler_Object_Table *t
226
330
  // If nothing moved, just update VALUE fields and we're done
227
331
  if (!any_moved) {
228
332
  for (size_t i = 0; i < table->capacity; i++) {
229
- if (table->entries[i].object != 0) {
333
+ // Skip empty slots and tombstones
334
+ if (table->entries[i].object != 0 && table->entries[i].object != TOMBSTONE) {
230
335
  // Update VALUE fields if they moved
231
336
  table->entries[i].klass = rb_gc_location(table->entries[i].klass);
232
337
  table->entries[i].data = rb_gc_location(table->entries[i].data);
233
- table->entries[i].allocations = rb_gc_location(table->entries[i].allocations);
234
338
  }
235
339
  }
236
340
  return;
@@ -246,24 +350,25 @@ void Memory_Profiler_Object_Table_compact(struct Memory_Profiler_Object_Table *t
246
350
 
247
351
  size_t temp_count = 0;
248
352
  for (size_t i = 0; i < table->capacity; i++) {
249
- if (table->entries[i].object != 0) {
353
+ // Skip empty slots and tombstones
354
+ if (table->entries[i].object != 0 && table->entries[i].object != TOMBSTONE) {
250
355
  // Update all pointers first
251
356
  temp_entries[temp_count].object = rb_gc_location(table->entries[i].object);
252
357
  temp_entries[temp_count].klass = rb_gc_location(table->entries[i].klass);
253
358
  temp_entries[temp_count].data = rb_gc_location(table->entries[i].data);
254
- temp_entries[temp_count].allocations = rb_gc_location(table->entries[i].allocations);
255
359
  temp_count++;
256
360
  }
257
361
  }
258
362
 
259
- // Clear the table (zero out all entries)
363
+ // Clear the table (zero out all entries, clears tombstones too)
260
364
  memset(table->entries, 0, table->capacity * sizeof(struct Memory_Profiler_Object_Table_Entry));
261
365
  table->count = 0;
366
+ table->tombstones = 0; // Compaction clears tombstones
262
367
 
263
368
  // Reinsert all entries with new hash values
264
369
  for (size_t i = 0; i < temp_count; i++) {
265
370
  int found;
266
- size_t index = find_entry(table->entries, table->capacity, temp_entries[i].object, &found);
371
+ size_t index = find_entry(table->entries, table->capacity, temp_entries[i].object, &found, NULL, "compact");
267
372
 
268
373
  // Insert at new location
269
374
  table->entries[index] = temp_entries[i];
@@ -284,42 +389,17 @@ void Memory_Profiler_Object_Table_delete_entry(struct Memory_Profiler_Object_Tab
284
389
  return; // Invalid pointer
285
390
  }
286
391
 
287
- // Check if entry is actually occupied
288
- if (entry->object == 0) {
289
- return; // Already deleted
392
+ // Check if entry is actually occupied (not empty or tombstone)
393
+ if (entry->object == 0 || entry->object == TOMBSTONE) {
394
+ return; // Already deleted or empty
290
395
  }
291
396
 
292
- // Mark as deleted (set to 0/NULL)
293
- entry->object = 0;
397
+ // Mark as tombstone - no rehashing needed!
398
+ entry->object = TOMBSTONE;
294
399
  entry->klass = 0;
295
400
  entry->data = 0;
296
- entry->allocations = 0;
297
401
  table->count--;
298
-
299
- // Rehash following entries to fix probe chains
300
- size_t next = (index + 1) % table->capacity;
301
- while (table->entries[next].object != 0) {
302
- // Save entry values
303
- VALUE obj = table->entries[next].object;
304
- VALUE k = table->entries[next].klass;
305
- VALUE d = table->entries[next].data;
306
- VALUE a = table->entries[next].allocations;
307
-
308
- // Remove this entry (set to 0/NULL)
309
- table->entries[next].object = 0;
310
- table->entries[next].klass = 0;
311
- table->entries[next].data = 0;
312
- table->entries[next].allocations = 0;
313
- table->count--;
314
-
315
- // Reinsert (will find correct spot and fill fields)
316
- struct Memory_Profiler_Object_Table_Entry *new_entry = Memory_Profiler_Object_Table_insert(table, obj);
317
- new_entry->klass = k;
318
- new_entry->data = d;
319
- new_entry->allocations = a;
320
-
321
- next = (next + 1) % table->capacity;
322
- }
402
+ table->tombstones++;
323
403
  }
324
404
 
325
405
  // Get current size
@@ -327,17 +407,3 @@ size_t Memory_Profiler_Object_Table_size(struct Memory_Profiler_Object_Table *ta
327
407
  return table->count;
328
408
  }
329
409
 
330
- // Increment strong reference count (makes table strong when > 0)
331
- void Memory_Profiler_Object_Table_increment_strong(struct Memory_Profiler_Object_Table *table) {
332
- if (table) {
333
- table->strong++;
334
- }
335
- }
336
-
337
- // Decrement strong reference count (makes table weak when == 0)
338
- void Memory_Profiler_Object_Table_decrement_strong(struct Memory_Profiler_Object_Table *table) {
339
- if (table && table->strong > 0) {
340
- table->strong--;
341
- }
342
- }
343
-
@@ -14,19 +14,16 @@ struct Memory_Profiler_Object_Table_Entry {
14
14
  VALUE klass;
15
15
  // User-defined state from callback:
16
16
  VALUE data;
17
- // The Allocations wrapper for this class:
18
- VALUE allocations;
19
17
  };
20
18
 
21
19
  // Custom object table for tracking allocations during GC.
22
20
  // Uses system malloc/free (not ruby_xmalloc) to be safe during GC compaction.
23
21
  // Keys are object addresses (updated during compaction).
22
+ // Table is always weak - object keys are not marked, allowing GC to collect them.
24
23
  struct Memory_Profiler_Object_Table {
25
- // Strong reference count: 0 = weak (don't mark keys), >0 = strong (mark keys)
26
- int strong;
27
-
28
- size_t capacity; // Total slots
29
- size_t count; // Used slots
24
+ size_t capacity; // Total slots
25
+ size_t count; // Used slots (occupied entries)
26
+ size_t tombstones; // Deleted slots (tombstone markers)
30
27
  struct Memory_Profiler_Object_Table_Entry *entries; // System malloc'd array
31
28
  };
32
29
 
@@ -63,11 +60,3 @@ void Memory_Profiler_Object_Table_compact(struct Memory_Profiler_Object_Table *t
63
60
  // Get current size
64
61
  size_t Memory_Profiler_Object_Table_size(struct Memory_Profiler_Object_Table *table);
65
62
 
66
- // Increment strong reference count
67
- // When strong > 0, table is strong and will mark object keys during GC
68
- void Memory_Profiler_Object_Table_increment_strong(struct Memory_Profiler_Object_Table *table);
69
-
70
- // Decrement strong reference count
71
- // When strong == 0, table is weak and will not mark object keys during GC
72
- void Memory_Profiler_Object_Table_decrement_strong(struct Memory_Profiler_Object_Table *table);
73
-
@@ -104,6 +104,7 @@ module Memory
104
104
  @gc = gc
105
105
 
106
106
  @capture = Capture.new
107
+ @capture.track_all = true
107
108
  @call_trees = {}
108
109
  @samples = {}
109
110
  end
@@ -282,7 +283,7 @@ module Memory
282
283
  # @parameter retained_roots [Boolean] Compute object graph showing what's retaining allocations (default: false, can be slow for large graphs).
283
284
  # @parameter retained_addresses [Boolean | Integer] Include memory addresses of retained objects for correlation with heap dumps (default: 1000).
284
285
  # @returns [Hash] Statistics including allocations, allocation_roots (call tree), retained_roots (object graph), and retained_addresses (array of memory addresses) .
285
- def analyze(klass, allocation_roots: true, retained_addresses: 1000, retained_minimum: 100)
286
+ def analyze(klass, allocation_roots: true, retained_addresses: 100, retained_minimum: 100)
286
287
  unless allocations = @capture[klass]
287
288
  return nil
288
289
  end
@@ -318,7 +319,7 @@ module Memory
318
319
 
319
320
  # Default filter to include all locations.
320
321
  def default_filter
321
- ->(location) {true}
322
+ ->(location){true}
322
323
  end
323
324
 
324
325
  def prune_call_trees!
@@ -7,7 +7,7 @@
7
7
  module Memory
8
8
  # @namespace
9
9
  module Profiler
10
- VERSION = "1.5.0"
10
+ VERSION = "1.6.0"
11
11
  end
12
12
  end
13
13
 
data/readme.md CHANGED
@@ -22,6 +22,10 @@ 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.5.1
26
+
27
+ - Improve performance of object table.
28
+
25
29
  ### v1.5.0
26
30
 
27
31
  - Add `Capture#each_object` for getting all retained objects.
@@ -75,15 +79,6 @@ Please see the [project releases](https://socketry.github.io/memory-profiler/rel
75
79
 
76
80
  - Double buffer shared events queues to fix queue corruption.
77
81
 
78
- ### v1.1.10
79
-
80
- - Added `Capture#new_count` - returns total number of allocations tracked across all classes.
81
- - Added `Capture#free_count` - returns total number of objects freed across all classes.
82
- - Added `Capture#retained_count` - returns retained object count (new\_count - free\_count).
83
- - **Critical:** Fixed GC crash during compaction caused by missing write barriers in event queue.
84
- - Fixed allocation/deallocation counts being inaccurate when objects are allocated during callbacks or freed after compaction.
85
- - `Capture#clear` now raises `RuntimeError` if called while capture is running. Call `stop()` before `clear()`.
86
-
87
82
  ## Contributing
88
83
 
89
84
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v1.5.1
4
+
5
+ - Improve performance of object table.
6
+
3
7
  ## v1.5.0
4
8
 
5
9
  - Add `Capture#each_object` for getting all retained objects.
data.tar.gz.sig CHANGED
Binary file
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.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file