Skip to content

Commit d9c952e

Browse files
Fix FREEOBJ queue handling using postponed job.
1 parent 686a729 commit d9c952e

File tree

4 files changed

+52
-50
lines changed

4 files changed

+52
-50
lines changed

examples/basic_tracking.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
puts "-" * 60
4949

5050
sampler = Memory::Profiler::Sampler.new(depth: 10)
51-
sampler.track_with_analysis(Hash)
51+
sampler.track(Hash)
5252
sampler.start
5353

5454
puts "Tracking Hash with call path analysis enabled"

ext/memory/profiler/allocations.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ static VALUE Memory_Profiler_Allocations = Qnil;
1212

1313
// Helper to mark object_states table values
1414
static int Memory_Profiler_Allocations_object_states_mark(st_data_t key, st_data_t value, st_data_t arg) {
15-
VALUE object = (VALUE)key;
16-
rb_gc_mark_movable(object);
15+
// Don't mark the object, we use it as a key but we never use it as an object, and we don't want to retain it.
16+
// VALUE object = (VALUE)key;
17+
// rb_gc_mark_movable(object);
1718

1819
VALUE state = (VALUE)value;
1920
if (!NIL_P(state)) {

ext/memory/profiler/capture.c

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
enum {
1515
DEBUG = 0,
1616
DEBUG_FREED_QUEUE = 0,
17+
DEBUG_STATE = 0,
1718
};
1819

1920
static VALUE Memory_Profiler_Capture = Qnil;
@@ -198,9 +199,7 @@ static void Memory_Profiler_Capture_process_freed_queue(void *arg) {
198199
struct Memory_Profiler_Capture *capture;
199200
TypedData_Get_Struct(self, struct Memory_Profiler_Capture, &Memory_Profiler_Capture_type, capture);
200201

201-
if (DEBUG_FREED_QUEUE) {
202-
fprintf(stderr, "Processing freed queue with %zu entries\n", capture->freed_queue.count);
203-
}
202+
if (DEBUG_FREED_QUEUE) fprintf(stderr, "Processing freed queue with %zu entries\n", capture->freed_queue.count);
204203

205204
// Process all freed objects in the queue
206205
for (size_t i = 0; i < capture->freed_queue.count; i++) {
@@ -245,6 +244,9 @@ static void Memory_Profiler_Capture_newobj_handler(VALUE self, struct Memory_Pro
245244
if (!record->object_states) {
246245
record->object_states = st_init_numtable();
247246
}
247+
248+
if (DEBUG_STATE) fprintf(stderr, "Memory_Profiler_Capture_newobj_handler: Inserting state for object: %p (%s)\n", (void *)object, rb_class2name(klass));
249+
248250
st_insert(record->object_states, (st_data_t)object, (st_data_t)state);
249251
// Notify GC about the state VALUE stored in the table
250252
RB_OBJ_WRITTEN(self, Qnil, state);
@@ -281,22 +283,29 @@ static void Memory_Profiler_Capture_freeobj_handler(VALUE self, struct Memory_Pr
281283

282284
// If we have a callback and detailed tracking, queue the freeobj for later processing
283285
if (!NIL_P(record->callback) && record->object_states) {
286+
if (DEBUG_STATE) fprintf(stderr, "Memory_Profiler_Capture_freeobj_handler: Looking up state for object: %p\n", (void *)object);
287+
284288
// Look up state stored during NEWOBJ
285289
st_data_t state_data;
286290
if (st_delete(record->object_states, (st_data_t *)&object, &state_data)) {
291+
if (DEBUG_STATE) fprintf(stderr, "Found state for object: %p\n", (void *)object);
287292
VALUE state = (VALUE)state_data;
288293

289294
// Push a new item onto the queue (returns pointer to write to)
290295
// NOTE: realloc is safe during GC (doesn't trigger Ruby allocation)
291296
struct Memory_Profiler_Queue_Item *freed = Memory_Profiler_Queue_push(&capture->freed_queue);
292297
if (freed) {
298+
if (DEBUG_FREED_QUEUE) fprintf(stderr, "Queued freed object, queue size now: %zu/%zu\n", capture->freed_queue.count, capture->freed_queue.capacity);
293299
// Write directly to the allocated space
294300
freed->klass = klass;
295301
freed->allocations = allocations;
296302
freed->state = state;
297303

298304
// Trigger postponed job to process the queue after GC
305+
if (DEBUG_FREED_QUEUE) fprintf(stderr, "Triggering postponed job to process the queue after GC\n");
299306
rb_postponed_job_trigger(capture->postponed_job_handle);
307+
} else {
308+
if (DEBUG_FREED_QUEUE) fprintf(stderr, "Failed to queue freed object, out of memory\n");
300309
}
301310
// If push failed (out of memory), silently drop this freeobj event
302311
}
@@ -455,6 +464,7 @@ static VALUE Memory_Profiler_Capture_stop(VALUE self) {
455464
// Add a class to track with optional callback
456465
// Usage: track(klass) or track(klass) { |obj, klass| ... }
457466
// Callback can call caller_locations with desired depth
467+
// Returns the Allocations object for the tracked class
458468
static VALUE Memory_Profiler_Capture_track(int argc, VALUE *argv, VALUE self) {
459469
struct Memory_Profiler_Capture *capture;
460470
TypedData_Get_Struct(self, struct Memory_Profiler_Capture, &Memory_Profiler_Capture_type, capture);
@@ -463,8 +473,10 @@ static VALUE Memory_Profiler_Capture_track(int argc, VALUE *argv, VALUE self) {
463473
rb_scan_args(argc, argv, "1&", &klass, &callback);
464474

465475
st_data_t allocations_data;
476+
VALUE allocations;
477+
466478
if (st_lookup(capture->tracked_classes, (st_data_t)klass, &allocations_data)) {
467-
VALUE allocations = (VALUE)allocations_data;
479+
allocations = (VALUE)allocations_data;
468480
struct Memory_Profiler_Capture_Allocations *record = Memory_Profiler_Allocations_get(allocations);
469481
RB_OBJ_WRITE(self, &record->callback, callback);
470482
} else {
@@ -475,7 +487,7 @@ static VALUE Memory_Profiler_Capture_track(int argc, VALUE *argv, VALUE self) {
475487
record->object_states = NULL;
476488

477489
// Wrap the record in a VALUE
478-
VALUE allocations = Memory_Profiler_Allocations_wrap(record);
490+
allocations = Memory_Profiler_Allocations_wrap(record);
479491

480492
st_insert(capture->tracked_classes, (st_data_t)klass, (st_data_t)allocations);
481493
// Notify GC about the class VALUE stored as key in the table
@@ -488,7 +500,7 @@ static VALUE Memory_Profiler_Capture_track(int argc, VALUE *argv, VALUE self) {
488500
}
489501
}
490502

491-
return self;
503+
return allocations;
492504
}
493505

494506
// Stop tracking a class
@@ -578,6 +590,19 @@ static VALUE Memory_Profiler_Capture_each(VALUE self) {
578590
return self;
579591
}
580592

593+
// Get allocations for a specific class
594+
static VALUE Memory_Profiler_Capture_aref(VALUE self, VALUE klass) {
595+
struct Memory_Profiler_Capture *capture;
596+
TypedData_Get_Struct(self, struct Memory_Profiler_Capture, &Memory_Profiler_Capture_type, capture);
597+
598+
st_data_t allocations_data;
599+
if (st_lookup(capture->tracked_classes, (st_data_t)klass, &allocations_data)) {
600+
return (VALUE)allocations_data;
601+
}
602+
603+
return Qnil;
604+
}
605+
581606
void Init_Memory_Profiler_Capture(VALUE Memory_Profiler)
582607
{
583608
// Initialize event symbols
@@ -597,6 +622,7 @@ void Init_Memory_Profiler_Capture(VALUE Memory_Profiler)
597622
rb_define_method(Memory_Profiler_Capture, "tracking?", Memory_Profiler_Capture_tracking_p, 1);
598623
rb_define_method(Memory_Profiler_Capture, "count_for", Memory_Profiler_Capture_count_for, 1);
599624
rb_define_method(Memory_Profiler_Capture, "each", Memory_Profiler_Capture_each, 0);
625+
rb_define_method(Memory_Profiler_Capture, "[]", Memory_Profiler_Capture_aref, 1);
600626
rb_define_method(Memory_Profiler_Capture, "clear", Memory_Profiler_Capture_clear, 0);
601627

602628
// Initialize Allocations class

lib/memory/profiler/sampler.rb

Lines changed: 16 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def sample!
141141
if sample.sample!(count)
142142
# Check if we should enable detailed tracking
143143
if sample.increases >= @increases_threshold && !@call_trees.key?(klass)
144-
track_with_analysis_internal(klass, allocations)
144+
track(klass, allocations)
145145
end
146146

147147
# Notify about growth if block given
@@ -150,13 +150,19 @@ def sample!
150150
end
151151
end
152152

153-
# Internal: Enable tracking with analysis using allocations object
154-
private def track_with_analysis_internal(klass, allocations)
153+
# Start tracking with call path analysis.
154+
#
155+
# @parameter klass [Class] The class to track with detailed analysis.
156+
def track(klass, allocations = nil)
157+
# Track the class and get the allocations object
158+
allocations ||= @capture.track(klass)
159+
160+
# Set up call tree for this class
155161
tree = @call_trees[klass] = CallTree.new
156162
depth = @depth
157163
filter = @filter
158164

159-
# Register callback on allocations object with new signature:
165+
# Register callback on allocations object:
160166
# - On :newobj - returns state (leaf node) which C extension stores
161167
# - On :freeobj - receives state back from C extension
162168
allocations.track do |klass, event, state|
@@ -166,43 +172,16 @@ def sample!
166172
locations = caller_locations(1, depth)
167173
filtered = locations.select(&filter)
168174
unless filtered.empty?
169-
# Record returns the leaf node - return it so C can store it
175+
# Record returns the leaf node - return it so C can store it:
170176
tree.record(filtered)
171177
end
172-
# Return nil or the node - C will store whatever we return
178+
# Return nil or the node - C will store whatever we return.
173179
when :freeobj
174-
# Decrement using the state (leaf node) passed back from C
175-
if state
176-
state.decrement_path!
177-
end
180+
# Decrement using the state (leaf node) passed back from then native extension:
181+
state&.decrement_path!
178182
end
179183
rescue Exception => error
180-
warn "Error in track_with_analysis_internal: #{error.message}\n#{error.backtrace.join("\n")}"
181-
end
182-
end
183-
184-
# Start tracking allocations for a class (count only).
185-
def track(klass)
186-
return if @capture.tracking?(klass)
187-
188-
@capture.track(klass)
189-
end
190-
191-
# Start tracking with call path analysis.
192-
#
193-
# @parameter klass [Class] The class to track with detailed analysis.
194-
def track_with_analysis(klass)
195-
# Track the class if not already tracked
196-
unless @capture.tracking?(klass)
197-
@capture.track(klass)
198-
end
199-
200-
# Enable analysis by setting callback on the allocations object
201-
@capture.each do |tracked_klass, allocations|
202-
if tracked_klass == klass
203-
track_with_analysis_internal(klass, allocations)
204-
break
205-
end
184+
warn "Error in allocation tracking: #{error.message}\n#{error.backtrace.join("\n")}"
206185
end
207186
end
208187

@@ -280,11 +259,7 @@ def stop!
280259
private
281260

282261
def default_filter
283-
->(location) {path = location.path
284-
!path.include?("/gems/") &&
285-
!path.include?("/ruby/") &&
286-
!path.start_with?("(eval)")
287-
}
262+
->(location) {!location.path.match?(%r{/(gems|ruby)/|\A\(eval\)})}
288263
end
289264
end
290265
end

0 commit comments

Comments
 (0)