Skip to content

Commit c666f8b

Browse files
authored
fix: prevent memory leak in IOReportIterator by releasing delta CFDictionaryRef (#104)
The IOReportIterator was not releasing the delta CFDictionaryRef that was passed to it from get_sample(). Each API call created a new delta dictionary via IOReportCreateSamplesDelta() but it was never released, causing memory to grow at ~300KB/second during continuous API queries. Changes: - Add `sample` field to IOReportIterator to store the CFDictionaryRef - Implement Drop trait to call CFRelease on the sample when iterator is dropped - Add memory leak test script for future regression testing Test results (3 minutes, ~1,225 queries): - Before: +53,472 KB memory increase - After: +288 KB memory increase (99.5% reduction)
1 parent 2ccaebc commit c666f8b

File tree

2 files changed

+160
-1
lines changed

2 files changed

+160
-1
lines changed

src/device/macos_native/ioreport.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,15 +475,36 @@ impl IOReportChannelItem {
475475
}
476476

477477
/// Iterator over IOReport sample channels
478+
///
479+
/// This struct takes ownership of the sample CFDictionaryRef and releases it
480+
/// when the iterator is dropped, preventing memory leaks.
478481
pub struct IOReportIterator {
482+
/// The sample CFDictionary that owns the channel data.
483+
/// Must be released when the iterator is dropped.
484+
sample: CFDictionaryRef,
479485
channels: Vec<CFDictionaryRef>,
480486
index: usize,
481487
}
482488

483489
impl IOReportIterator {
484490
fn new(sample: CFDictionaryRef) -> Self {
485491
let channels = get_io_channels(sample);
486-
Self { channels, index: 0 }
492+
Self {
493+
sample,
494+
channels,
495+
index: 0,
496+
}
497+
}
498+
}
499+
500+
impl Drop for IOReportIterator {
501+
fn drop(&mut self) {
502+
// Release the sample dictionary to prevent memory leaks
503+
if !self.sample.is_null() {
504+
unsafe {
505+
CFRelease(self.sample as *const c_void);
506+
}
507+
}
487508
}
488509
}
489510

tests/memory-leak-test.sh

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/bin/bash
2+
3+
# Memory Leak Test Script for all-smi API mode
4+
#
5+
# This script monitors memory usage while repeatedly querying the API endpoint.
6+
# It helps detect memory leaks by tracking RSS growth over time.
7+
#
8+
# Usage:
9+
# ./tests/memory-leak-test.sh [PORT] [DURATION_SECONDS]
10+
#
11+
# Example:
12+
# ./target/release/all-smi api --port 19090 &
13+
# ./tests/memory-leak-test.sh 19090 60
14+
15+
PORT=${1:-19090}
16+
DURATION=${2:-60}
17+
QUERY_INTERVAL=0.1
18+
MONITOR_INTERVAL=1
19+
20+
PID=$(pgrep -f "all-smi api" | head -1)
21+
22+
if [ -z "$PID" ]; then
23+
echo "Error: all-smi api process not found"
24+
echo "Please start all-smi in API mode first:"
25+
echo " ./target/release/all-smi api --port $PORT &"
26+
exit 1
27+
fi
28+
29+
echo "=========================================="
30+
echo "Memory Leak Test for all-smi API mode"
31+
echo "=========================================="
32+
echo "PID: $PID"
33+
echo "Port: $PORT"
34+
echo "Duration: ${DURATION}s"
35+
echo "=========================================="
36+
37+
MEMORY_LOG=$(mktemp)
38+
QUERY_LOG=$(mktemp)
39+
40+
INITIAL_RSS=$(ps -o rss= -p $PID | tr -d ' ')
41+
echo "Initial RSS: ${INITIAL_RSS} KB"
42+
echo ""
43+
44+
# Memory monitoring
45+
(
46+
START_TIME=$(date +%s)
47+
while true; do
48+
CURRENT_TIME=$(date +%s)
49+
ELAPSED=$((CURRENT_TIME - START_TIME))
50+
[ $ELAPSED -ge $DURATION ] && break
51+
RSS=$(ps -o rss= -p $PID 2>/dev/null | tr -d ' ')
52+
[ -n "$RSS" ] && echo "$ELAPSED $RSS" >> "$MEMORY_LOG"
53+
sleep $MONITOR_INTERVAL
54+
done
55+
) &
56+
MONITOR_PID=$!
57+
58+
# Query loop
59+
(
60+
QUERY_COUNT=0
61+
START_TIME=$(date +%s)
62+
while true; do
63+
CURRENT_TIME=$(date +%s)
64+
ELAPSED=$((CURRENT_TIME - START_TIME))
65+
[ $ELAPSED -ge $DURATION ] && break
66+
curl -s "http://localhost:$PORT/metrics" > /dev/null 2>&1
67+
QUERY_COUNT=$((QUERY_COUNT + 1))
68+
[ $((QUERY_COUNT % 100)) -eq 0 ] && echo "Queries: $QUERY_COUNT, Elapsed: ${ELAPSED}s" >> "$QUERY_LOG"
69+
sleep $QUERY_INTERVAL
70+
done
71+
echo "Total queries: $QUERY_COUNT" >> "$QUERY_LOG"
72+
) &
73+
QUERY_PID=$!
74+
75+
echo "Running test..."
76+
for i in $(seq 1 $DURATION); do
77+
sleep 1
78+
RSS=$(ps -o rss= -p $PID 2>/dev/null | tr -d ' ')
79+
[ -n "$RSS" ] && printf "\r[%3d/%3ds] RSS: %d KB (D %+d KB)" $i $DURATION $RSS $((RSS - INITIAL_RSS))
80+
done
81+
echo ""
82+
echo ""
83+
84+
wait $MONITOR_PID 2>/dev/null
85+
wait $QUERY_PID 2>/dev/null
86+
87+
FINAL_RSS=$(ps -o rss= -p $PID | tr -d ' ')
88+
89+
echo "=========================================="
90+
echo "Test Results"
91+
echo "=========================================="
92+
echo "Initial RSS: ${INITIAL_RSS} KB"
93+
echo "Final RSS: ${FINAL_RSS} KB"
94+
echo "Difference: $((FINAL_RSS - INITIAL_RSS)) KB"
95+
echo ""
96+
97+
if [ -f "$MEMORY_LOG" ]; then
98+
MIN_RSS=$(awk '{print $2}' "$MEMORY_LOG" | sort -n | head -1)
99+
MAX_RSS=$(awk '{print $2}' "$MEMORY_LOG" | sort -n | tail -1)
100+
AVG_RSS=$(awk '{sum+=$2; count++} END {printf "%.0f", sum/count}' "$MEMORY_LOG")
101+
echo "Statistics:"
102+
echo " Min RSS: ${MIN_RSS} KB"
103+
echo " Max RSS: ${MAX_RSS} KB"
104+
echo " Avg RSS: ${AVG_RSS} KB"
105+
echo " Range: $((MAX_RSS - MIN_RSS)) KB"
106+
echo ""
107+
fi
108+
109+
cat "$QUERY_LOG"
110+
echo ""
111+
112+
FIRST_AVG=$(head -5 "$MEMORY_LOG" | awk '{sum+=$2; count++} END {printf "%.0f", sum/count}')
113+
LAST_AVG=$(tail -5 "$MEMORY_LOG" | awk '{sum+=$2; count++} END {printf "%.0f", sum/count}')
114+
TREND=$((LAST_AVG - FIRST_AVG))
115+
116+
echo "=========================================="
117+
echo "Trend Analysis"
118+
echo "=========================================="
119+
echo "First 5 samples avg: ${FIRST_AVG} KB"
120+
echo "Last 5 samples avg: ${LAST_AVG} KB"
121+
echo "Trend: ${TREND} KB"
122+
echo ""
123+
124+
if [ $TREND -gt 1000 ]; then
125+
echo "WARNING: Memory appears to be increasing significantly!"
126+
echo "This could indicate a memory leak."
127+
EXIT_CODE=1
128+
elif [ $TREND -gt 100 ]; then
129+
echo "NOTICE: Memory shows some increase."
130+
echo "Consider running a longer test to confirm stability."
131+
EXIT_CODE=0
132+
else
133+
echo "Memory appears stable."
134+
EXIT_CODE=0
135+
fi
136+
137+
rm -f "$MEMORY_LOG" "$QUERY_LOG"
138+
exit $EXIT_CODE

0 commit comments

Comments
 (0)