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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/ext/memory/profiler/capture.c +63 -24
- data/ext/memory/profiler/table.c +180 -114
- data/ext/memory/profiler/table.h +4 -15
- data/lib/memory/profiler/sampler.rb +3 -2
- data/lib/memory/profiler/version.rb +1 -1
- data/readme.md +4 -9
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +1 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a65991a6612d28ede2c4ab5652bcf410e7edf92a0f98e8bdeebd921b64232d6
|
|
4
|
+
data.tar.gz: 230a10b0d5ed42f74d19007534dbae1ee49df3c67d0507fcf42a0fc67b1cf5e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
601
|
+
// Cleanup function to re-enable GC
|
|
570
602
|
static VALUE Memory_Profiler_Capture_each_object_ensure(VALUE arg) {
|
|
571
|
-
|
|
572
|
-
|
|
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 (
|
|
636
|
+
if (allocations != arguments->allocations) continue;
|
|
602
637
|
}
|
|
603
638
|
|
|
604
|
-
rb_yield_values(2, entry->object,
|
|
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
|
-
//
|
|
630
|
-
|
|
664
|
+
// Disable GC to prevent objects from being collected during iteration
|
|
665
|
+
rb_gc_disable();
|
|
631
666
|
|
|
632
|
-
// Process all pending events
|
|
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
|
-
|
|
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
|
data/ext/memory/profiler/table.c
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
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->
|
|
39
|
+
table->tombstones = 0;
|
|
25
40
|
|
|
26
|
-
// Use calloc to zero out entries (
|
|
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
|
-
//
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
157
|
-
table->entries[index].object =
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
293
|
-
entry->object =
|
|
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
|
-
|
data/ext/memory/profiler/table.h
CHANGED
|
@@ -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
|
-
//
|
|
26
|
-
|
|
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:
|
|
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)
|
|
322
|
+
->(location){true}
|
|
322
323
|
end
|
|
323
324
|
|
|
324
325
|
def prune_call_trees!
|
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
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
metadata.gz.sig
CHANGED
|
Binary file
|