memory-profiler 1.4.0 → 1.5.1
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/extconf.rb +1 -1
- data/ext/memory/profiler/capture.c +133 -199
- data/ext/memory/profiler/events.c +10 -4
- data/ext/memory/profiler/events.h +7 -5
- data/ext/memory/profiler/profiler.c +17 -6
- data/ext/memory/profiler/table.c +429 -0
- data/ext/memory/profiler/table.h +72 -0
- data/lib/memory/profiler/sampler.rb +24 -31
- data/lib/memory/profiler/version.rb +1 -1
- data/lib/memory/profiler.rb +0 -1
- data/readme.md +12 -13
- data/releases.md +12 -0
- data.tar.gz.sig +0 -0
- metadata +3 -2
- metadata.gz.sig +0 -0
- data/lib/memory/profiler/graph.rb +0 -369
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// Released under the MIT License.
|
|
2
|
+
// Copyright, 2025, by Samuel Williams.
|
|
3
|
+
|
|
4
|
+
#include "table.h"
|
|
5
|
+
#include <stdlib.h>
|
|
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
|
+
};
|
|
20
|
+
|
|
21
|
+
// Use the Entry struct from header
|
|
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
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
VALUE TOMBSTONE = Qnil;
|
|
28
|
+
|
|
29
|
+
// Create a new table
|
|
30
|
+
struct Memory_Profiler_Object_Table* Memory_Profiler_Object_Table_new(size_t initial_capacity) {
|
|
31
|
+
struct Memory_Profiler_Object_Table *table = malloc(sizeof(struct Memory_Profiler_Object_Table));
|
|
32
|
+
|
|
33
|
+
if (!table) {
|
|
34
|
+
return NULL;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
table->capacity = initial_capacity > 0 ? initial_capacity : INITIAL_CAPACITY;
|
|
38
|
+
table->count = 0;
|
|
39
|
+
table->tombstones = 0;
|
|
40
|
+
table->strong = 0; // Start as weak table (strong == 0 means weak)
|
|
41
|
+
|
|
42
|
+
// Use calloc to zero out entries (0 = empty slot)
|
|
43
|
+
table->entries = calloc(table->capacity, sizeof(struct Memory_Profiler_Object_Table_Entry));
|
|
44
|
+
|
|
45
|
+
if (!table->entries) {
|
|
46
|
+
free(table);
|
|
47
|
+
return NULL;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return table;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Free the table
|
|
54
|
+
void Memory_Profiler_Object_Table_free(struct Memory_Profiler_Object_Table *table) {
|
|
55
|
+
if (table) {
|
|
56
|
+
free(table->entries);
|
|
57
|
+
free(table);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Hash function for object addresses
|
|
62
|
+
// Uses multiplicative hashing with bit mixing to reduce clustering
|
|
63
|
+
static inline size_t hash_object(VALUE object, size_t capacity) {
|
|
64
|
+
size_t hash = (size_t)object;
|
|
65
|
+
|
|
66
|
+
// Remove alignment bits (objects are typically 8-byte aligned)
|
|
67
|
+
hash >>= 3;
|
|
68
|
+
|
|
69
|
+
// Multiplicative hashing (Knuth's golden ratio method)
|
|
70
|
+
// This helps distribute consecutive addresses across the table
|
|
71
|
+
hash *= 2654435761UL; // 2^32 / phi (golden ratio)
|
|
72
|
+
|
|
73
|
+
// Mix high bits into low bits for better distribution
|
|
74
|
+
hash ^= (hash >> 16);
|
|
75
|
+
hash *= 0x85ebca6b;
|
|
76
|
+
hash ^= (hash >> 13);
|
|
77
|
+
hash *= 0xc2b2ae35;
|
|
78
|
+
hash ^= (hash >> 16);
|
|
79
|
+
|
|
80
|
+
return hash % capacity;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Find entry index for an object (linear probing)
|
|
84
|
+
// Returns index if found, or index of empty slot if not found
|
|
85
|
+
// If table is provided (not NULL), logs statistics when probe length is excessive
|
|
86
|
+
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) {
|
|
87
|
+
size_t index = hash_object(object, capacity);
|
|
88
|
+
size_t start = index;
|
|
89
|
+
size_t probe_count = 0;
|
|
90
|
+
|
|
91
|
+
*found = 0;
|
|
92
|
+
|
|
93
|
+
do {
|
|
94
|
+
probe_count++;
|
|
95
|
+
|
|
96
|
+
// Safety check - prevent infinite loops
|
|
97
|
+
if (probe_count > MAX_PROBE_LENGTH) {
|
|
98
|
+
if (DEBUG && table) {
|
|
99
|
+
double load = (double)table->count / capacity;
|
|
100
|
+
double tomb_ratio = (double)table->tombstones / capacity;
|
|
101
|
+
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",
|
|
102
|
+
operation, probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
|
|
103
|
+
} else if (DEBUG) {
|
|
104
|
+
fprintf(stderr, "{\"subject\":\"Memory::Profiler::ObjectTable\",\"level\":\"critical\",\"operation\":\"%s\",\"event\":\"max_probes_exceeded\",\"probe_count\":%zu,\"capacity\":%zu}\n",
|
|
105
|
+
operation, probe_count, capacity);
|
|
106
|
+
}
|
|
107
|
+
return index;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Log warning for excessive probing
|
|
111
|
+
if (DEBUG && probe_count == WARN_PROBE_LENGTH && table) {
|
|
112
|
+
double load = (double)table->count / capacity;
|
|
113
|
+
double tomb_ratio = (double)table->tombstones / capacity;
|
|
114
|
+
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",
|
|
115
|
+
operation, probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (entries[index].object == 0) {
|
|
119
|
+
return index;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (entries[index].object != TOMBSTONE && entries[index].object == object) {
|
|
123
|
+
*found = 1;
|
|
124
|
+
return index;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
index = (index + 1) % capacity;
|
|
128
|
+
} while (index != start);
|
|
129
|
+
|
|
130
|
+
// Table is full
|
|
131
|
+
if (DEBUG && table) {
|
|
132
|
+
double load = (double)table->count / capacity;
|
|
133
|
+
double tomb_ratio = (double)table->tombstones / capacity;
|
|
134
|
+
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",
|
|
135
|
+
operation, probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
|
|
136
|
+
}
|
|
137
|
+
return index;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Find slot for inserting an object (linear probing)
|
|
141
|
+
// Returns index to insert at - reuses tombstone slots if found
|
|
142
|
+
// If object exists, returns its index with found=1
|
|
143
|
+
static size_t find_insert_slot(struct Memory_Profiler_Object_Table *table, VALUE object, int *found) {
|
|
144
|
+
struct Memory_Profiler_Object_Table_Entry *entries = table->entries;
|
|
145
|
+
size_t capacity = table->capacity;
|
|
146
|
+
size_t index = hash_object(object, capacity);
|
|
147
|
+
size_t start = index;
|
|
148
|
+
size_t first_tombstone = SIZE_MAX; // Track first tombstone we encounter
|
|
149
|
+
size_t probe_count = 0;
|
|
150
|
+
|
|
151
|
+
*found = 0;
|
|
152
|
+
|
|
153
|
+
do {
|
|
154
|
+
probe_count++;
|
|
155
|
+
|
|
156
|
+
// Safety check - prevent infinite loops
|
|
157
|
+
if (probe_count > MAX_PROBE_LENGTH) {
|
|
158
|
+
if (DEBUG) {
|
|
159
|
+
double load = (double)table->count / capacity;
|
|
160
|
+
double tomb_ratio = (double)table->tombstones / capacity;
|
|
161
|
+
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",
|
|
162
|
+
probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
|
|
163
|
+
}
|
|
164
|
+
// Return tombstone if we found one, otherwise current position
|
|
165
|
+
return (first_tombstone != SIZE_MAX) ? first_tombstone : index;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Log warning for excessive probing
|
|
169
|
+
if (DEBUG && probe_count == WARN_PROBE_LENGTH) {
|
|
170
|
+
double load = (double)table->count / capacity;
|
|
171
|
+
double tomb_ratio = (double)table->tombstones / capacity;
|
|
172
|
+
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",
|
|
173
|
+
probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (entries[index].object == 0) {
|
|
177
|
+
// Empty slot - use tombstone if we found one, otherwise this slot
|
|
178
|
+
return (first_tombstone != SIZE_MAX) ? first_tombstone : index;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (entries[index].object == TOMBSTONE) {
|
|
182
|
+
// Remember first tombstone (but keep searching for existing object)
|
|
183
|
+
if (first_tombstone == SIZE_MAX) {
|
|
184
|
+
first_tombstone = index;
|
|
185
|
+
}
|
|
186
|
+
} else if (entries[index].object == object) {
|
|
187
|
+
// Found existing entry
|
|
188
|
+
*found = 1;
|
|
189
|
+
return index;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
index = (index + 1) % capacity;
|
|
193
|
+
} while (index != start);
|
|
194
|
+
|
|
195
|
+
// Table is full
|
|
196
|
+
if (DEBUG) {
|
|
197
|
+
double load = (double)table->count / capacity;
|
|
198
|
+
double tomb_ratio = (double)table->tombstones / capacity;
|
|
199
|
+
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",
|
|
200
|
+
probe_count, capacity, table->count, table->tombstones, load, tomb_ratio);
|
|
201
|
+
}
|
|
202
|
+
// Use tombstone slot if we found one
|
|
203
|
+
return (first_tombstone != SIZE_MAX) ? first_tombstone : index;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Resize the table (only called from insert, not during GC)
|
|
207
|
+
// This clears all tombstones
|
|
208
|
+
static void resize_table(struct Memory_Profiler_Object_Table *table) {
|
|
209
|
+
size_t old_capacity = table->capacity;
|
|
210
|
+
struct Memory_Profiler_Object_Table_Entry *old_entries = table->entries;
|
|
211
|
+
|
|
212
|
+
// Double capacity
|
|
213
|
+
table->capacity = old_capacity * 2;
|
|
214
|
+
table->count = 0;
|
|
215
|
+
table->tombstones = 0; // Reset tombstones
|
|
216
|
+
table->entries = calloc(table->capacity, sizeof(struct Memory_Profiler_Object_Table_Entry));
|
|
217
|
+
|
|
218
|
+
if (!table->entries) {
|
|
219
|
+
// Resize failed - restore old state
|
|
220
|
+
table->capacity = old_capacity;
|
|
221
|
+
table->entries = old_entries;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Rehash all non-tombstone entries
|
|
226
|
+
for (size_t i = 0; i < old_capacity; i++) {
|
|
227
|
+
// Skip empty slots and tombstones
|
|
228
|
+
if (old_entries[i].object != 0 && old_entries[i].object != TOMBSTONE) {
|
|
229
|
+
int found;
|
|
230
|
+
size_t new_index = find_entry(table->entries, table->capacity, old_entries[i].object, &found, NULL, "resize");
|
|
231
|
+
table->entries[new_index] = old_entries[i];
|
|
232
|
+
table->count++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
free(old_entries);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Insert object, returns pointer to entry for caller to fill
|
|
240
|
+
struct Memory_Profiler_Object_Table_Entry* Memory_Profiler_Object_Table_insert(struct Memory_Profiler_Object_Table *table, VALUE object) {
|
|
241
|
+
// Resize if load factor exceeded (count + tombstones)
|
|
242
|
+
// This clears tombstones and gives us fresh space
|
|
243
|
+
if ((double)(table->count + table->tombstones) / table->capacity > LOAD_FACTOR) {
|
|
244
|
+
resize_table(table);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
int found;
|
|
248
|
+
size_t index = find_insert_slot(table, object, &found);
|
|
249
|
+
|
|
250
|
+
if (!found) {
|
|
251
|
+
// New entry - check if we're reusing a tombstone slot
|
|
252
|
+
if (table->entries[index].object == TOMBSTONE) {
|
|
253
|
+
table->tombstones--; // Reusing tombstone
|
|
254
|
+
}
|
|
255
|
+
table->count++;
|
|
256
|
+
// Zero out the entry
|
|
257
|
+
table->entries[index].object = object;
|
|
258
|
+
table->entries[index].klass = 0;
|
|
259
|
+
table->entries[index].data = 0;
|
|
260
|
+
} else {
|
|
261
|
+
// Updating existing entry
|
|
262
|
+
table->entries[index].object = object;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Return pointer for caller to fill fields
|
|
266
|
+
return &table->entries[index];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Lookup entry for object - returns pointer or NULL
|
|
270
|
+
struct Memory_Profiler_Object_Table_Entry* Memory_Profiler_Object_Table_lookup(struct Memory_Profiler_Object_Table *table, VALUE object) {
|
|
271
|
+
int found;
|
|
272
|
+
size_t index = find_entry(table->entries, table->capacity, object, &found, table, "lookup");
|
|
273
|
+
|
|
274
|
+
if (found) {
|
|
275
|
+
return &table->entries[index];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return NULL;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Delete object from table
|
|
282
|
+
void Memory_Profiler_Object_Table_delete(struct Memory_Profiler_Object_Table *table, VALUE object) {
|
|
283
|
+
int found;
|
|
284
|
+
size_t index = find_entry(table->entries, table->capacity, object, &found, table, "delete");
|
|
285
|
+
|
|
286
|
+
if (!found) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Mark as tombstone - no rehashing needed!
|
|
291
|
+
table->entries[index].object = TOMBSTONE;
|
|
292
|
+
table->entries[index].klass = 0;
|
|
293
|
+
table->entries[index].data = 0;
|
|
294
|
+
table->count--;
|
|
295
|
+
table->tombstones++;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Mark all entries for GC
|
|
299
|
+
void Memory_Profiler_Object_Table_mark(struct Memory_Profiler_Object_Table *table) {
|
|
300
|
+
if (!table) return;
|
|
301
|
+
|
|
302
|
+
for (size_t i = 0; i < table->capacity; i++) {
|
|
303
|
+
struct Memory_Profiler_Object_Table_Entry *entry = &table->entries[i];
|
|
304
|
+
// Skip empty slots and tombstones
|
|
305
|
+
if (entry->object != 0 && entry->object != TOMBSTONE) {
|
|
306
|
+
// Mark object key if table is strong (strong > 0)
|
|
307
|
+
// When weak (strong == 0), object keys can be GC'd (that's how we detect frees)
|
|
308
|
+
if (table->strong > 0) {
|
|
309
|
+
rb_gc_mark_movable(entry->object);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Always mark the other fields (klass, data) - we own these
|
|
313
|
+
if (entry->klass) rb_gc_mark_movable(entry->klass);
|
|
314
|
+
if (entry->data) rb_gc_mark_movable(entry->data);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Update object pointers during compaction
|
|
320
|
+
void Memory_Profiler_Object_Table_compact(struct Memory_Profiler_Object_Table *table) {
|
|
321
|
+
if (!table || table->count == 0) return;
|
|
322
|
+
|
|
323
|
+
// First pass: check if any objects moved
|
|
324
|
+
int any_moved = 0;
|
|
325
|
+
for (size_t i = 0; i < table->capacity; i++) {
|
|
326
|
+
// Skip empty slots and tombstones
|
|
327
|
+
if (table->entries[i].object != 0 && table->entries[i].object != TOMBSTONE) {
|
|
328
|
+
VALUE new_loc = rb_gc_location(table->entries[i].object);
|
|
329
|
+
if (new_loc != table->entries[i].object) {
|
|
330
|
+
any_moved = 1;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// If nothing moved, just update VALUE fields and we're done
|
|
337
|
+
if (!any_moved) {
|
|
338
|
+
for (size_t i = 0; i < table->capacity; i++) {
|
|
339
|
+
// Skip empty slots and tombstones
|
|
340
|
+
if (table->entries[i].object != 0 && table->entries[i].object != TOMBSTONE) {
|
|
341
|
+
// Update VALUE fields if they moved
|
|
342
|
+
table->entries[i].klass = rb_gc_location(table->entries[i].klass);
|
|
343
|
+
table->entries[i].data = rb_gc_location(table->entries[i].data);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Something moved - need to rehash entire table
|
|
350
|
+
// Collect all entries into temporary array (use system malloc, not Ruby's)
|
|
351
|
+
struct Memory_Profiler_Object_Table_Entry *temp_entries = malloc(table->count * sizeof(struct Memory_Profiler_Object_Table_Entry));
|
|
352
|
+
if (!temp_entries) {
|
|
353
|
+
// Allocation failed - this is bad, but can't do much during GC
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
size_t temp_count = 0;
|
|
358
|
+
for (size_t i = 0; i < table->capacity; i++) {
|
|
359
|
+
// Skip empty slots and tombstones
|
|
360
|
+
if (table->entries[i].object != 0 && table->entries[i].object != TOMBSTONE) {
|
|
361
|
+
// Update all pointers first
|
|
362
|
+
temp_entries[temp_count].object = rb_gc_location(table->entries[i].object);
|
|
363
|
+
temp_entries[temp_count].klass = rb_gc_location(table->entries[i].klass);
|
|
364
|
+
temp_entries[temp_count].data = rb_gc_location(table->entries[i].data);
|
|
365
|
+
temp_count++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Clear the table (zero out all entries, clears tombstones too)
|
|
370
|
+
memset(table->entries, 0, table->capacity * sizeof(struct Memory_Profiler_Object_Table_Entry));
|
|
371
|
+
table->count = 0;
|
|
372
|
+
table->tombstones = 0; // Compaction clears tombstones
|
|
373
|
+
|
|
374
|
+
// Reinsert all entries with new hash values
|
|
375
|
+
for (size_t i = 0; i < temp_count; i++) {
|
|
376
|
+
int found;
|
|
377
|
+
size_t index = find_entry(table->entries, table->capacity, temp_entries[i].object, &found, NULL, "compact");
|
|
378
|
+
|
|
379
|
+
// Insert at new location
|
|
380
|
+
table->entries[index] = temp_entries[i];
|
|
381
|
+
table->count++;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Free temporary array
|
|
385
|
+
free(temp_entries);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Delete by entry pointer (faster - avoids second lookup)
|
|
389
|
+
void Memory_Profiler_Object_Table_delete_entry(struct Memory_Profiler_Object_Table *table, struct Memory_Profiler_Object_Table_Entry *entry) {
|
|
390
|
+
// Calculate index from pointer
|
|
391
|
+
size_t index = entry - table->entries;
|
|
392
|
+
|
|
393
|
+
// Validate it's within our table
|
|
394
|
+
if (index >= table->capacity) {
|
|
395
|
+
return; // Invalid pointer
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Check if entry is actually occupied (not empty or tombstone)
|
|
399
|
+
if (entry->object == 0 || entry->object == TOMBSTONE) {
|
|
400
|
+
return; // Already deleted or empty
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Mark as tombstone - no rehashing needed!
|
|
404
|
+
entry->object = TOMBSTONE;
|
|
405
|
+
entry->klass = 0;
|
|
406
|
+
entry->data = 0;
|
|
407
|
+
table->count--;
|
|
408
|
+
table->tombstones++;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Get current size
|
|
412
|
+
size_t Memory_Profiler_Object_Table_size(struct Memory_Profiler_Object_Table *table) {
|
|
413
|
+
return table->count;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Increment strong reference count (makes table strong when > 0)
|
|
417
|
+
void Memory_Profiler_Object_Table_increment_strong(struct Memory_Profiler_Object_Table *table) {
|
|
418
|
+
if (table) {
|
|
419
|
+
table->strong++;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Decrement strong reference count (makes table weak when == 0)
|
|
424
|
+
void Memory_Profiler_Object_Table_decrement_strong(struct Memory_Profiler_Object_Table *table) {
|
|
425
|
+
if (table && table->strong > 0) {
|
|
426
|
+
table->strong--;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Released under the MIT License.
|
|
2
|
+
// Copyright, 2025, by Samuel Williams.
|
|
3
|
+
|
|
4
|
+
#pragma once
|
|
5
|
+
|
|
6
|
+
#include <ruby.h>
|
|
7
|
+
#include <stddef.h>
|
|
8
|
+
|
|
9
|
+
// Entry in the object table
|
|
10
|
+
struct Memory_Profiler_Object_Table_Entry {
|
|
11
|
+
// Object pointer (key):
|
|
12
|
+
VALUE object;
|
|
13
|
+
// The class of the allocated object:
|
|
14
|
+
VALUE klass;
|
|
15
|
+
// User-defined state from callback:
|
|
16
|
+
VALUE data;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Custom object table for tracking allocations during GC.
|
|
20
|
+
// Uses system malloc/free (not ruby_xmalloc) to be safe during GC compaction.
|
|
21
|
+
// Keys are object addresses (updated during compaction).
|
|
22
|
+
struct Memory_Profiler_Object_Table {
|
|
23
|
+
// Strong reference count: 0 = weak (don't mark keys), >0 = strong (mark keys)
|
|
24
|
+
int strong;
|
|
25
|
+
|
|
26
|
+
size_t capacity; // Total slots
|
|
27
|
+
size_t count; // Used slots (occupied entries)
|
|
28
|
+
size_t tombstones; // Deleted slots (tombstone markers)
|
|
29
|
+
struct Memory_Profiler_Object_Table_Entry *entries; // System malloc'd array
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Create a new object table with initial capacity
|
|
33
|
+
struct Memory_Profiler_Object_Table* Memory_Profiler_Object_Table_new(size_t initial_capacity);
|
|
34
|
+
|
|
35
|
+
// Free the table and all its memory
|
|
36
|
+
void Memory_Profiler_Object_Table_free(struct Memory_Profiler_Object_Table *table);
|
|
37
|
+
|
|
38
|
+
// Insert an object, returns pointer to entry for caller to fill fields.
|
|
39
|
+
// Safe to call from postponed job (not during GC).
|
|
40
|
+
struct Memory_Profiler_Object_Table_Entry* Memory_Profiler_Object_Table_insert(struct Memory_Profiler_Object_Table *table, VALUE object);
|
|
41
|
+
|
|
42
|
+
// Lookup entry for an object. Returns pointer to entry or NULL if not found.
|
|
43
|
+
// Safe to call during FREEOBJ event handler (no allocation) - READ ONLY!
|
|
44
|
+
struct Memory_Profiler_Object_Table_Entry* Memory_Profiler_Object_Table_lookup(struct Memory_Profiler_Object_Table *table, VALUE object);
|
|
45
|
+
|
|
46
|
+
// Delete an object. Safe to call from postponed job (not during GC).
|
|
47
|
+
void Memory_Profiler_Object_Table_delete(struct Memory_Profiler_Object_Table *table, VALUE object);
|
|
48
|
+
|
|
49
|
+
// Delete by entry pointer (faster - no second lookup needed).
|
|
50
|
+
// Safe to call from postponed job (not during GC).
|
|
51
|
+
// entry must be a valid pointer from Object_Table_lookup.
|
|
52
|
+
void Memory_Profiler_Object_Table_delete_entry(struct Memory_Profiler_Object_Table *table, struct Memory_Profiler_Object_Table_Entry *entry);
|
|
53
|
+
|
|
54
|
+
// Mark all entries for GC.
|
|
55
|
+
// Must be called from dmark callback.
|
|
56
|
+
void Memory_Profiler_Object_Table_mark(struct Memory_Profiler_Object_Table *table);
|
|
57
|
+
|
|
58
|
+
// Update object pointers after compaction.
|
|
59
|
+
// Must be called from dcompact callback.
|
|
60
|
+
void Memory_Profiler_Object_Table_compact(struct Memory_Profiler_Object_Table *table);
|
|
61
|
+
|
|
62
|
+
// Get current size
|
|
63
|
+
size_t Memory_Profiler_Object_Table_size(struct Memory_Profiler_Object_Table *table);
|
|
64
|
+
|
|
65
|
+
// Increment strong reference count
|
|
66
|
+
// When strong > 0, table is strong and will mark object keys during GC
|
|
67
|
+
void Memory_Profiler_Object_Table_increment_strong(struct Memory_Profiler_Object_Table *table);
|
|
68
|
+
|
|
69
|
+
// Decrement strong reference count
|
|
70
|
+
// When strong == 0, table is weak and will not mark object keys during GC
|
|
71
|
+
void Memory_Profiler_Object_Table_decrement_strong(struct Memory_Profiler_Object_Table *table);
|
|
72
|
+
|
|
@@ -9,7 +9,6 @@ require "objspace"
|
|
|
9
9
|
require_relative "capture"
|
|
10
10
|
require_relative "allocations"
|
|
11
11
|
require_relative "call_tree"
|
|
12
|
-
require_relative "graph"
|
|
13
12
|
|
|
14
13
|
module Memory
|
|
15
14
|
module Profiler
|
|
@@ -142,7 +141,7 @@ module Memory
|
|
|
142
141
|
def stop
|
|
143
142
|
@capture.stop
|
|
144
143
|
end
|
|
145
|
-
|
|
144
|
+
|
|
146
145
|
# Clear tracking data for a class.
|
|
147
146
|
def clear(klass)
|
|
148
147
|
tree = @call_trees[klass]
|
|
@@ -280,52 +279,46 @@ module Memory
|
|
|
280
279
|
#
|
|
281
280
|
# @parameter klass [Class] The class to get statistics for.
|
|
282
281
|
# @parameter allocation_roots [Boolean] Include call tree showing where allocations occurred (default: true if available).
|
|
283
|
-
# @parameter retained_roots [Boolean] Compute object graph showing what's retaining allocations (default:
|
|
284
|
-
# @parameter
|
|
285
|
-
# @returns [Hash] Statistics including allocations, allocation_roots (call tree),
|
|
286
|
-
def analyze(klass, allocation_roots: true,
|
|
287
|
-
|
|
288
|
-
|
|
282
|
+
# @parameter retained_roots [Boolean] Compute object graph showing what's retaining allocations (default: false, can be slow for large graphs).
|
|
283
|
+
# @parameter retained_addresses [Boolean | Integer] Include memory addresses of retained objects for correlation with heap dumps (default: 1000).
|
|
284
|
+
# @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: 100, retained_minimum: 100)
|
|
286
|
+
unless allocations = @capture[klass]
|
|
287
|
+
return nil
|
|
288
|
+
end
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
if retained_minimum && allocations.retained_count < retained_minimum
|
|
291
|
+
return nil
|
|
292
|
+
end
|
|
291
293
|
|
|
292
294
|
result = {
|
|
293
295
|
allocations: allocations&.as_json,
|
|
294
296
|
}
|
|
295
297
|
|
|
296
|
-
if allocation_roots
|
|
297
|
-
|
|
298
|
+
if allocation_roots
|
|
299
|
+
if call_tree_data = @call_trees[klass]
|
|
300
|
+
result[:allocation_roots] = call_tree_data.as_json
|
|
301
|
+
end
|
|
298
302
|
end
|
|
299
303
|
|
|
300
|
-
if
|
|
301
|
-
|
|
304
|
+
if retained_addresses
|
|
305
|
+
addresses = []
|
|
306
|
+
@capture.each_object(klass) do |object, state|
|
|
307
|
+
addresses << Memory::Profiler.address_of(object)
|
|
308
|
+
break if retained_addresses.is_a?(Integer) && addresses.size >= retained_addresses
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
result[:retained_addresses] = addresses
|
|
302
312
|
end
|
|
303
313
|
|
|
304
314
|
result
|
|
305
315
|
end
|
|
306
316
|
|
|
307
317
|
private
|
|
308
|
-
|
|
309
|
-
# Compute retaining roots for a class's allocations
|
|
310
|
-
def compute_roots(klass)
|
|
311
|
-
graph = Graph.new
|
|
312
|
-
|
|
313
|
-
# Add all tracked objects to the graph
|
|
314
|
-
# NOTE: States table is now at Capture level, so we use capture.each_object
|
|
315
|
-
@capture.each_object(klass) do |object, state|
|
|
316
|
-
graph.add(object)
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
# Build parent relationships
|
|
320
|
-
graph.update!
|
|
321
|
-
|
|
322
|
-
# Return roots analysis
|
|
323
|
-
graph.roots
|
|
324
|
-
end
|
|
325
318
|
|
|
326
319
|
# Default filter to include all locations.
|
|
327
320
|
def default_filter
|
|
328
|
-
->(location)
|
|
321
|
+
->(location){true}
|
|
329
322
|
end
|
|
330
323
|
|
|
331
324
|
def prune_call_trees!
|
data/lib/memory/profiler.rb
CHANGED
data/readme.md
CHANGED
|
@@ -22,6 +22,18 @@ 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
|
+
|
|
29
|
+
### v1.5.0
|
|
30
|
+
|
|
31
|
+
- Add `Capture#each_object` for getting all retained objects.
|
|
32
|
+
- Add `retained_addresses:` option to `Sampler#analyze` to capture addresses.
|
|
33
|
+
- Add `Sampler#analyze(retained_minimum: 100)` - if the retained\_size is less than this, the analyse won't proceed.
|
|
34
|
+
- Remove `Memory::Profiler::Graph` - it's too slow for practical use.
|
|
35
|
+
- Add `Memory::Profiler.address_of(object)` to get the memory address of an object.
|
|
36
|
+
|
|
25
37
|
### v1.4.0
|
|
26
38
|
|
|
27
39
|
- Implement [Cooper-Harvey-Kennedy](https://www.cs.tufts.edu/~nr/cs257/archive/keith-cooper/dom14.pdf) algorithm for finding root objects in memory leaks.
|
|
@@ -67,19 +79,6 @@ Please see the [project releases](https://socketry.github.io/memory-profiler/rel
|
|
|
67
79
|
|
|
68
80
|
- Double buffer shared events queues to fix queue corruption.
|
|
69
81
|
|
|
70
|
-
### v1.1.10
|
|
71
|
-
|
|
72
|
-
- Added `Capture#new_count` - returns total number of allocations tracked across all classes.
|
|
73
|
-
- Added `Capture#free_count` - returns total number of objects freed across all classes.
|
|
74
|
-
- Added `Capture#retained_count` - returns retained object count (new\_count - free\_count).
|
|
75
|
-
- **Critical:** Fixed GC crash during compaction caused by missing write barriers in event queue.
|
|
76
|
-
- Fixed allocation/deallocation counts being inaccurate when objects are allocated during callbacks or freed after compaction.
|
|
77
|
-
- `Capture#clear` now raises `RuntimeError` if called while capture is running. Call `stop()` before `clear()`.
|
|
78
|
-
|
|
79
|
-
### v1.1.9
|
|
80
|
-
|
|
81
|
-
- More write barriers...
|
|
82
|
-
|
|
83
82
|
## Contributing
|
|
84
83
|
|
|
85
84
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v1.5.1
|
|
4
|
+
|
|
5
|
+
- Improve performance of object table.
|
|
6
|
+
|
|
7
|
+
## v1.5.0
|
|
8
|
+
|
|
9
|
+
- Add `Capture#each_object` for getting all retained objects.
|
|
10
|
+
- Add `retained_addresses:` option to `Sampler#analyze` to capture addresses.
|
|
11
|
+
- Add `Sampler#analyze(retained_minimum: 100)` - if the retained\_size is less than this, the analyse won't proceed.
|
|
12
|
+
- Remove `Memory::Profiler::Graph` - it's too slow for practical use.
|
|
13
|
+
- Add `Memory::Profiler.address_of(object)` to get the memory address of an object.
|
|
14
|
+
|
|
3
15
|
## v1.4.0
|
|
4
16
|
|
|
5
17
|
- Implement [Cooper-Harvey-Kennedy](https://www.cs.tufts.edu/~nr/cs257/archive/keith-cooper/dom14.pdf) algorithm for finding root objects in memory leaks.
|
data.tar.gz.sig
CHANGED
|
Binary file
|