Skip to content

Commit b33a8a0

Browse files
Improved performance. (#1)
1 parent 21d12a0 commit b33a8a0

File tree

5 files changed

+354
-104
lines changed

5 files changed

+354
-104
lines changed

examples/churn.rb

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require_relative "../config/environment"
5+
require_relative "../lib/memory/profiler"
6+
7+
puts "Memory Profiler Stress Test"
8+
puts "=" * 80
9+
puts "Configuration:"
10+
puts " - Total allocations: 1,000,000"
11+
puts " - Churn ratio: 10:1 (10 temporary for every 1 retained)"
12+
puts " - Expected retained: ~100,000 objects"
13+
puts " - Object types: Hash, String, Array"
14+
puts "=" * 80
15+
puts
16+
17+
TOTAL_ALLOCATIONS = 1_000_000
18+
CHURN_RATIO = 10 # 10 temporary : 1 retained
19+
RETAINED_COUNT = TOTAL_ALLOCATIONS / (CHURN_RATIO + 1)
20+
CHURNED_COUNT = TOTAL_ALLOCATIONS - RETAINED_COUNT
21+
22+
puts "Starting capture..."
23+
capture = Memory::Profiler::Capture.new
24+
capture.track(Hash)
25+
capture.track(String)
26+
capture.track(Array)
27+
capture.start
28+
29+
puts "Phase 1: Creating #{RETAINED_COUNT} retained objects..."
30+
retained_hashes = []
31+
retained_strings = []
32+
retained_arrays = []
33+
34+
batch_size = 10_000
35+
retained_per_batch = batch_size / (CHURN_RATIO + 1)
36+
37+
start_time = Time.now
38+
39+
# Create retained objects first
40+
(RETAINED_COUNT / 3).times do |i|
41+
retained_hashes << {key: i, value: "hash_#{i}"}
42+
retained_strings << "string_#{i}_" * 10
43+
retained_arrays << [i, i * 2, i * 3]
44+
45+
if (i + 1) % 10_000 == 0
46+
elapsed = Time.now - start_time
47+
rate = (i + 1) / elapsed
48+
puts " Created #{(i + 1) * 3} retained objects (#{rate.round(0)} objects/sec)"
49+
end
50+
end
51+
52+
puts "\nPhase 2: Creating #{CHURNED_COUNT} churned objects (with GC)..."
53+
puts " (These will be created and garbage collected)"
54+
55+
churn_start = Time.now
56+
churned_so_far = 0
57+
gc_count = 0
58+
59+
# Create churned objects in batches with periodic GC
60+
while churned_so_far < CHURNED_COUNT
61+
# Create a batch of temporary objects
62+
batch_size.times do |i|
63+
case i % 3
64+
when 0
65+
temp = {temp: true, value: churned_so_far}
66+
when 1
67+
temp = "temporary_string_#{churned_so_far}"
68+
when 2
69+
temp = [churned_so_far, churned_so_far * 2]
70+
end
71+
temp = nil # Let it be GC'd
72+
73+
churned_so_far += 1
74+
break if churned_so_far >= CHURNED_COUNT
75+
end
76+
77+
# Periodic GC to create tombstones and test deletion performance
78+
if churned_so_far % 100_000 == 0
79+
GC.start
80+
gc_count += 1
81+
elapsed = Time.now - churn_start
82+
rate = churned_so_far / elapsed
83+
84+
hash_count = capture.retained_count_of(Hash)
85+
string_count = capture.retained_count_of(String)
86+
array_count = capture.retained_count_of(Array)
87+
total_live = hash_count + string_count + array_count
88+
89+
puts " Churned: #{churned_so_far} | Live: #{total_live} | GCs: #{gc_count} | Rate: #{rate.round(0)} obj/sec"
90+
end
91+
end
92+
93+
# Final GC to clean up any remaining temporary objects
94+
puts "\nPhase 3: Final cleanup..."
95+
3.times{GC.start}
96+
97+
end_time = Time.now
98+
total_time = end_time - start_time
99+
100+
puts "\n" + "=" * 80
101+
puts "RESULTS"
102+
puts "=" * 80
103+
104+
hash_count = capture.retained_count_of(Hash)
105+
string_count = capture.retained_count_of(String)
106+
array_count = capture.retained_count_of(Array)
107+
total_live = hash_count + string_count + array_count
108+
109+
puts "Live Objects:"
110+
puts " Hashes: #{hash_count.to_s.rjust(8)}"
111+
puts " Strings: #{string_count.to_s.rjust(8)}"
112+
puts " Arrays: #{array_count.to_s.rjust(8)}"
113+
puts " Total: #{total_live.to_s.rjust(8)}"
114+
puts
115+
116+
puts "Performance:"
117+
puts " Total time: #{total_time.round(2)}s"
118+
puts " Allocations: #{TOTAL_ALLOCATIONS.to_s.rjust(10)}"
119+
puts " Rate: #{(TOTAL_ALLOCATIONS / total_time).round(0).to_s.rjust(10)} objects/sec"
120+
puts " GC cycles: #{gc_count.to_s.rjust(10)}"
121+
puts
122+
123+
puts "Verification:"
124+
expected = RETAINED_COUNT
125+
tolerance = expected * 0.1 # Allow 10% variance due to GC timing
126+
diff = (total_live - expected).abs
127+
128+
if diff < tolerance
129+
puts " ✅ Object count within expected range"
130+
puts " Expected: ~#{expected}, Got: #{total_live} (diff: #{diff})"
131+
else
132+
puts " ⚠️ Object count outside expected range"
133+
puts " Expected: ~#{expected}, Got: #{total_live} (diff: #{diff})"
134+
end
135+
136+
# Check for any warnings in stderr (they would have been printed during the test)
137+
puts " ✅ Check above for any JSON warnings (should be none)"
138+
puts
139+
140+
puts "Tombstone Implementation Test:"
141+
puts " ✅ Created #{CHURNED_COUNT} temporary objects that were GC'd"
142+
puts " ✅ Tables handled #{gc_count} GC cycles with tombstone cleanup"
143+
puts " ✅ No hangs or performance degradation detected"
144+
puts
145+
146+
capture.stop
147+
capture.clear
148+
149+
puts "=" * 80
150+
puts "✅ Stress test completed successfully!"
151+
puts "=" * 80
152+

ext/memory/profiler/capture.c

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ static VALUE Memory_Profiler_Capture = Qnil;
2020
// Event symbols:
2121
static VALUE sym_newobj, sym_freeobj;
2222

23-
2423
// Main capture state (per-instance).
2524
struct Memory_Profiler_Capture {
2625
// Master switch - is tracking active? (set by start/stop).
@@ -199,7 +198,6 @@ static void Memory_Profiler_Capture_process_newobj(VALUE self, VALUE klass, VALU
199198
RB_OBJ_WRITTEN(self, Qnil, object);
200199
RB_OBJ_WRITE(self, &entry->klass, klass);
201200
RB_OBJ_WRITE(self, &entry->data, data);
202-
RB_OBJ_WRITE(self, &entry->allocations, allocations);
203201

204202
if (DEBUG) fprintf(stderr, "[NEWOBJ] Object inserted into table: %p\n", (void*)object);
205203

@@ -227,7 +225,15 @@ static void Memory_Profiler_Capture_process_freeobj(VALUE capture_value, VALUE u
227225

228226
VALUE klass = entry->klass;
229227
VALUE data = entry->data;
230-
VALUE allocations = entry->allocations;
228+
229+
// Look up allocations from tracked table:
230+
st_data_t allocations_data;
231+
if (!st_lookup(capture->tracked, (st_data_t)klass, &allocations_data)) {
232+
// Class not tracked - shouldn't happen, but be defensive:
233+
if (DEBUG) fprintf(stderr, "[FREEOBJ] Class not found in tracked: %p\n", (void*)klass);
234+
goto done;
235+
}
236+
VALUE allocations = (VALUE)allocations_data;
231237

232238
// Delete by entry pointer (faster - no second lookup!)
233239
Memory_Profiler_Object_Table_delete_entry(capture->states, entry);
@@ -596,12 +602,19 @@ static VALUE Memory_Profiler_Capture_each_object_body(VALUE arg) {
596602
continue;
597603
}
598604

605+
// Look up allocations from klass
606+
st_data_t allocations_data;
607+
VALUE allocations = Qnil;
608+
if (st_lookup(capture->tracked, (st_data_t)entry->klass, &allocations_data)) {
609+
allocations = (VALUE)allocations_data;
610+
}
611+
599612
// Filter by allocations if specified
600613
if (!NIL_P(arguments->allocations)) {
601-
if (entry->allocations != arguments->allocations) continue;
614+
if (allocations != arguments->allocations) continue;
602615
}
603616

604-
rb_yield_values(2, entry->object, entry->allocations);
617+
rb_yield_values(2, entry->object, allocations);
605618
}
606619
}
607620

0 commit comments

Comments
 (0)