-
Notifications
You must be signed in to change notification settings - Fork 46
Open
Description
Im noticing a memory leak over time when creating new animations. For my project, the way I would like to do animations is to replace the existing animation that was previously running. Its not an AnimationSequence, because I'd like to stop running one animation for another. The animation selected is from an external event (a message from i2c).
Here is an example load test script to demonstrate the issue. It seems like something is not internally cleaning up the animation object. I'm using an Adafruit RP2040 SCORPIO with the 8 channel neopixel support:
"""
Memory Leak Test for Adafruit LED Animation Library
Demonstrates memory consumption and leaks when creating/destroying animation objects
This script creates Solid animations with random colors, runs them for 10 seconds,
then creates new animations with different parameters. Memory usage is tracked
to demonstrate the memory leak issue.
Expected behavior: Memory should return to baseline after gc.collect()
Actual behavior: Memory continues to increase with each animation creation
"""
import time
import board
import gc
import random
import supervisor
from adafruit_led_animation.animation.solid import Solid
from adafruit_led_animation.color import RED, GREEN, BLUE, WHITE, YELLOW, PURPLE, ORANGE
from adafruit_neopxl8 import NeoPxl8
# Test configuration
STRAND_COUNT = 8
PIXELS_PER_STRAND = 50
TOTAL_PIXELS = STRAND_COUNT * PIXELS_PER_STRAND
TEST_CYCLES = 20 # Number of animation create/destroy cycles
ANIMATION_DURATION = 1 # Seconds per animation
# Available colors for random selection
COLORS = [RED, GREEN, BLUE, WHITE, YELLOW, PURPLE, ORANGE]
def log_memory(label=""):
"""Log detailed memory usage information"""
if not supervisor.runtime.serial_connected:
return
try:
# Force garbage collection before measurement
gc.collect()
free = gc.mem_free()
alloc = gc.mem_alloc()
total = free + alloc
percent_used = (alloc * 100) // total
print(f"MEMORY [{label}]: Free={free:,}B, Used={alloc:,}B, Total={total:,}B, %Used={percent_used}%")
return percent_used, alloc, total
except Exception as e:
print(f"MEMORY [{label}]: Error - {e}")
return None, None, None
def create_test_pixels():
"""Create the LED pixel object"""
print(f"Creating {STRAND_COUNT} strands with {PIXELS_PER_STRAND} pixels each")
pixels = NeoPxl8(
board.NEOPIXEL0,
TOTAL_PIXELS,
num_strands=STRAND_COUNT,
auto_write=False,
brightness=0.3,
)
# Turn off all pixels initially
pixels.fill((0, 0, 0))
pixels.show()
return pixels
def run_memory_leak_test():
"""Main test function that demonstrates the memory leak"""
print("=" * 60)
print("ADAFRUIT LED ANIMATION LIBRARY MEMORY LEAK TEST")
print("=" * 60)
print(f"Test Configuration:")
print(f" - Strands: {STRAND_COUNT}")
print(f" - Pixels per strand: {PIXELS_PER_STRAND}")
print(f" - Total pixels: {TOTAL_PIXELS}")
print(f" - Test cycles: {TEST_CYCLES}")
print(f" - Animation duration: {ANIMATION_DURATION}s per cycle")
print("=" * 60)
# Initial memory measurement
print("\n1. BASELINE MEMORY MEASUREMENT")
baseline_percent, baseline_used, baseline_total = log_memory("BASELINE")
# Create pixel object
print("\n2. CREATING PIXEL OBJECT")
pixels = create_test_pixels()
pixel_percent, pixel_used, pixel_total = log_memory("AFTER PIXEL CREATION")
pixel_overhead = pixel_used - baseline_used if baseline_used else 0
print(f"Pixel object overhead: {pixel_overhead:,} bytes")
# Track memory usage over test cycles
memory_history = []
print(f"\n3. STARTING {TEST_CYCLES} ANIMATION CYCLES")
print("Each cycle: Create animation -> Run 10s -> Destroy -> GC")
print("-" * 60)
for cycle in range(TEST_CYCLES):
cycle_start_time = time.monotonic()
print(f"\nCycle {cycle + 1}/{TEST_CYCLES}")
# Memory before creating animation
pre_percent, pre_used, pre_total = log_memory(f"CYCLE {cycle + 1} - BEFORE CREATE")
# Create a new Solid animation with random color
random_color = random.choice(COLORS)
print(f" Creating Solid animation with color {random_color}")
try:
animation = Solid(pixels, color=random_color)
post_create_percent, post_create_used, post_create_total = log_memory(f"CYCLE {cycle + 1} - AFTER CREATE")
# Calculate animation object overhead
animation_overhead = post_create_used - pre_used if pre_used else 0
print(f" Animation object overhead: {animation_overhead:,} bytes")
# Run animation for specified duration
print(f" Running animation for {ANIMATION_DURATION} seconds...")
end_time = time.monotonic() + ANIMATION_DURATION
while time.monotonic() < end_time:
animation.animate()
time.sleep(0.1) # Small delay to prevent overwhelming the system
# Memory after running animation
post_run_percent, post_run_used, post_run_total = log_memory(f"CYCLE {cycle + 1} - AFTER RUN")
# Destroy animation object
print(" Destroying animation object...")
animation = None
# Force garbage collection
print(" Running garbage collection...")
gc.collect()
gc.collect() # Double GC to ensure cleanup
# Memory after cleanup
post_gc_percent, post_gc_used, post_gc_total = log_memory(f"CYCLE {cycle + 1} - AFTER GC")
# Calculate memory leak
if baseline_used and post_gc_used:
leak_bytes = post_gc_used - baseline_used - pixel_overhead
print(f" Memory leak vs baseline: {leak_bytes:,} bytes")
# Store memory data for summary
memory_history.append({
'cycle': cycle + 1,
'baseline': baseline_used,
'after_create': post_create_used,
'after_run': post_run_used,
'after_gc': post_gc_used,
'animation_overhead': animation_overhead,
'total_leak': leak_bytes,
'percent_used': post_gc_percent
})
except MemoryError as e:
print(f" MEMORY ERROR in cycle {cycle + 1}: {e}")
log_memory(f"CYCLE {cycle + 1} - MEMORY ERROR")
break
except Exception as e:
print(f" ERROR in cycle {cycle + 1}: {e}")
continue
# Brief pause between cycles
time.sleep(1)
# Final summary
print("\n" + "=" * 60)
print("MEMORY LEAK TEST SUMMARY")
print("=" * 60)
if memory_history:
print(f"{'Cycle':<6} {'%Used':<6} {'Leak(B)':<10} {'AnimObj(B)':<12} {'Status'}")
print("-" * 50)
for data in memory_history:
status = "LEAK" if data['total_leak'] > 1000 else "OK"
print(f"{data['cycle']:<6} {data['percent_used']:<6} {data['total_leak']:<10,} {data['animation_overhead']:<12,} {status}")
# Calculate total leak
final_leak = memory_history[-1]['total_leak'] if memory_history else 0
avg_animation_overhead = sum(d['animation_overhead'] for d in memory_history) // len(memory_history)
print("-" * 50)
print(f"Final memory leak: {final_leak:,} bytes")
print(f"Average animation overhead: {avg_animation_overhead:,} bytes per object")
print(f"Expected memory after GC: ~{baseline_used + pixel_overhead:,} bytes")
print(f"Actual memory after GC: ~{memory_history[-1]['after_gc']:,} bytes")
if final_leak > 1000:
print("\n🚨 MEMORY LEAK DETECTED!")
print("Animation objects are not being properly garbage collected.")
print("Each animation creation leaves memory that is never freed.")
else:
print("\n✅ No significant memory leak detected.")
print("\n" + "=" * 60)
print("TEST COMPLETE - Please include this output in your bug report")
print("=" * 60)
# Run the test
if __name__ == "__main__":
try:
run_memory_leak_test()
except KeyboardInterrupt:
print("\nTest interrupted by user")
except Exception as e:
print(f"\nTest failed with error: {e}")
log_memory("ERROR STATE")
Results:
============================================================
MEMORY LEAK TEST SUMMARY
============================================================
Cycle %Used Leak(B) AnimObj(B) Status
--------------------------------------------------
1 21 80 352 OK
2 21 160 352 OK
3 21 240 352 OK
4 21 320 336 OK
5 21 400 352 OK
6 22 496 352 OK
7 22 576 336 OK
8 22 656 320 OK
9 22 736 352 OK
10 22 848 352 OK
11 22 928 336 OK
12 22 1,008 352 LEAK
13 22 1,088 352 LEAK
14 22 1,168 352 LEAK
15 22 1,248 336 LEAK
16 22 1,328 320 LEAK
17 22 1,408 352 LEAK
18 22 1,552 352 LEAK
19 22 1,632 352 LEAK
20 22 1,712 352 LEAK
21 22 1,792 352 LEAK
22 22 1,872 320 LEAK
23 22 1,952 352 LEAK
24 22 2,032 352 LEAK
25 22 2,112 352 LEAK
26 23 2,192 352 LEAK
27 23 2,272 352 LEAK
28 23 2,352 352 LEAK
29 23 2,432 352 LEAK
30 23 2,512 352 LEAK
31 23 2,592 352 LEAK
32 23 2,672 352 LEAK
33 23 2,752 352 LEAK
34 23 2,960 352 LEAK
35 23 3,040 352 LEAK
36 23 3,120 352 LEAK
37 23 3,200 352 LEAK
38 23 3,280 352 LEAK
39 23 3,360 352 LEAK
40 23 3,440 352 LEAK
41 23 3,520 352 LEAK
42 23 3,600 352 LEAK
43 23 3,680 352 LEAK
44 23 3,760 352 LEAK
45 23 3,840 352 LEAK
46 24 3,920 352 LEAK
47 24 4,000 352 LEAK
48 24 4,080 352 LEAK
49 24 4,160 352 LEAK
50 24 4,240 352 LEAK
51 24 4,320 352 LEAK
52 24 4,400 352 LEAK
53 24 4,480 352 LEAK
54 24 4,560 352 LEAK
55 24 4,640 352 LEAK
56 24 4,720 352 LEAK
57 24 4,800 352 LEAK
58 24 4,880 352 LEAK
59 24 4,960 352 LEAK
60 24 5,040 352 LEAK
61 24 5,120 352 LEAK
62 24 5,200 352 LEAK
63 24 5,280 352 LEAK
64 24 5,360 352 LEAK
65 24 5,440 352 LEAK
66 25 5,776 352 LEAK
67 25 5,856 352 LEAK
68 25 5,936 320 LEAK
69 25 6,016 352 LEAK
70 25 6,096 352 LEAK
71 25 6,176 352 LEAK
72 25 6,256 352 LEAK
73 25 6,336 352 LEAK
74 25 6,416 352 LEAK
75 25 6,496 352 LEAK
76 25 6,576 352 LEAK
77 25 6,656 352 LEAK
78 25 6,736 352 LEAK
79 25 6,816 352 LEAK
80 25 6,896 352 LEAK
81 25 6,976 352 LEAK
82 25 7,056 352 LEAK
83 25 7,136 352 LEAK
84 25 7,216 352 LEAK
85 26 7,296 352 LEAK
86 26 7,376 352 LEAK
87 26 7,456 352 LEAK
88 26 7,536 352 LEAK
89 26 7,616 352 LEAK
90 26 7,696 352 LEAK
91 26 7,776 352 LEAK
92 26 7,856 352 LEAK
93 26 7,936 352 LEAK
94 26 8,016 352 LEAK
95 26 8,096 352 LEAK
96 26 8,176 352 LEAK
97 26 8,256 352 LEAK
98 26 8,336 352 LEAK
99 26 8,416 352 LEAK
100 26 8,496 352 LEAK
--------------------------------------------------
Final memory leak: 8,496 bytes
Average animation overhead: 350 bytes per object
Expected memory after GC: ~36,976 bytes
Actual memory after GC: ~45,472 bytes
🚨 MEMORY LEAK DETECTED!
Animation objects are not being properly garbage collected.
Each animation creation leaves memory that is never freed.
============================================================
TEST COMPLETE
============================================================
Metadata
Metadata
Assignees
Labels
No labels