Hopefully finally tracked down and fix the high memory alert on text based books.
This commit is contained in:
+64
-12
@@ -194,6 +194,8 @@ class BookReader:
|
|||||||
|
|
||||||
# Audio buffering for seamless playback
|
# Audio buffering for seamless playback
|
||||||
self.bufferedAudio = None # Pre-generated next paragraph
|
self.bufferedAudio = None # Pre-generated next paragraph
|
||||||
|
self.bufferedChapter = -1 # Which chapter this buffer is for
|
||||||
|
self.bufferedParagraph = -1 # Which paragraph this buffer is for
|
||||||
self.bufferThread = None
|
self.bufferThread = None
|
||||||
self.cancelBuffer = False
|
self.cancelBuffer = False
|
||||||
self.bufferLock = threading.Lock()
|
self.bufferLock = threading.Lock()
|
||||||
@@ -822,16 +824,42 @@ class BookReader:
|
|||||||
gc.collect(generation=0) # Fast collection at 10 seconds
|
gc.collect(generation=0) # Fast collection at 10 seconds
|
||||||
# Don't reset - let it reach 600 for full GC
|
# Don't reset - let it reach 600 for full GC
|
||||||
|
|
||||||
# Memory watchdog: warn if exceeding 2GB (50% on Pi 4GB)
|
# Memory watchdog: warn if using 50% of available system memory
|
||||||
try:
|
try:
|
||||||
|
# Get CURRENT memory usage (not peak) from /proc/self/status
|
||||||
|
currentMemMb = None
|
||||||
|
try:
|
||||||
|
with open('/proc/self/status', 'r') as statusFile:
|
||||||
|
for line in statusFile:
|
||||||
|
if line.startswith('VmRSS:'):
|
||||||
|
# VmRSS is in kB
|
||||||
|
currentMemMb = int(line.split()[1]) / 1024
|
||||||
|
break
|
||||||
|
except (FileNotFoundError, IOError):
|
||||||
|
# Fall back to resource module (gives peak, not current)
|
||||||
import resource
|
import resource
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
memUsage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024 # MB
|
currentMemMb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
||||||
|
|
||||||
# More aggressive memory warnings and cleanup
|
# Get total system memory from /proc/meminfo
|
||||||
if memUsage > 1536: # 1.5GB threshold
|
totalMemMb = 8192 # Default 8GB if can't read
|
||||||
|
try:
|
||||||
|
with open('/proc/meminfo', 'r') as meminfoFile:
|
||||||
|
for line in meminfoFile:
|
||||||
|
if line.startswith('MemTotal:'):
|
||||||
|
# MemTotal is in kB
|
||||||
|
totalMemMb = int(line.split()[1]) / 1024
|
||||||
|
break
|
||||||
|
except (FileNotFoundError, IOError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Calculate thresholds as percentages of total memory
|
||||||
|
cleanupThreshold = totalMemMb * 0.4 # 40% triggers cleanup
|
||||||
|
warningThreshold = totalMemMb * 0.5 # 50% triggers warning
|
||||||
|
|
||||||
|
if currentMemMb and currentMemMb > cleanupThreshold:
|
||||||
if not memoryWarningShown:
|
if not memoryWarningShown:
|
||||||
print(f"Memory usage: {memUsage:.0f}MB - performing aggressive cleanup")
|
print(f"Memory usage: {currentMemMb:.0f}MB ({currentMemMb/totalMemMb*100:.1f}% of {totalMemMb:.0f}MB) - performing aggressive cleanup")
|
||||||
# Force aggressive cleanup
|
# Force aggressive cleanup
|
||||||
if hasattr(self, 'parser') and self.parser:
|
if hasattr(self, 'parser') and self.parser:
|
||||||
self.parser.cleanup()
|
self.parser.cleanup()
|
||||||
@@ -847,7 +875,7 @@ class BookReader:
|
|||||||
gc.collect()
|
gc.collect()
|
||||||
gc.collect() # Second pass
|
gc.collect() # Second pass
|
||||||
|
|
||||||
if memUsage > 2048 and not memoryWarningShown:
|
if currentMemMb > warningThreshold and not memoryWarningShown:
|
||||||
memoryWarningShown = True
|
memoryWarningShown = True
|
||||||
self.speechEngine.speak("Warning: High memory usage detected. Consider restarting BookStorm soon.")
|
self.speechEngine.speak("Warning: High memory usage detected. Consider restarting BookStorm soon.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -2303,13 +2331,26 @@ class BookReader:
|
|||||||
if not self.brailleOutput.muteVoice:
|
if not self.brailleOutput.muteVoice:
|
||||||
wavData = None
|
wavData = None
|
||||||
try:
|
try:
|
||||||
# Check if we have buffered audio ready
|
# Check if we have buffered audio ready for THIS paragraph
|
||||||
with self.bufferLock:
|
with self.bufferLock:
|
||||||
if self.bufferedAudio is not None:
|
if (self.bufferedAudio is not None and
|
||||||
# Use pre-generated audio
|
self.bufferedChapter == self.currentChapter and
|
||||||
|
self.bufferedParagraph == self.currentParagraph):
|
||||||
|
# Use pre-generated audio (matches current position)
|
||||||
wavData = self.bufferedAudio
|
wavData = self.bufferedAudio
|
||||||
self.bufferedAudio = None
|
self.bufferedAudio = None
|
||||||
|
self.bufferedChapter = -1
|
||||||
|
self.bufferedParagraph = -1
|
||||||
else:
|
else:
|
||||||
|
# Discard stale buffer if present (wrong paragraph)
|
||||||
|
if self.bufferedAudio is not None:
|
||||||
|
print(f"Discarding stale buffer (expected ch{self.currentChapter}p{self.currentParagraph}, got ch{self.bufferedChapter}p{self.bufferedParagraph})")
|
||||||
|
del self.bufferedAudio
|
||||||
|
self.bufferedAudio = None
|
||||||
|
self.bufferedChapter = -1
|
||||||
|
self.bufferedParagraph = -1
|
||||||
|
# Force GC to reclaim buffer memory immediately
|
||||||
|
gc.collect()
|
||||||
# Generate audio now (first paragraph or after navigation)
|
# Generate audio now (first paragraph or after navigation)
|
||||||
wavData = self.ttsEngine.text_to_wav_data(paragraph)
|
wavData = self.ttsEngine.text_to_wav_data(paragraph)
|
||||||
|
|
||||||
@@ -2422,12 +2463,17 @@ class BookReader:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# CRITICAL: Clear any stale buffered audio before starting new thread
|
# CRITICAL: Clear any stale buffered audio before starting new thread
|
||||||
# This happens when buffer thread finishes AFTER we already generated audio synchronously
|
# This shouldn't normally happen now that we track buffer position
|
||||||
with self.bufferLock:
|
with self.bufferLock:
|
||||||
if self.bufferedAudio is not None:
|
if self.bufferedAudio is not None:
|
||||||
print("Warning: Discarding stale buffered audio (orphaned buffer)")
|
# This is now a safety check - should rarely trigger
|
||||||
|
print(f"Warning: Clearing existing buffer before buffering next (ch{self.bufferedChapter}p{self.bufferedParagraph})")
|
||||||
del self.bufferedAudio
|
del self.bufferedAudio
|
||||||
self.bufferedAudio = None
|
self.bufferedAudio = None
|
||||||
|
self.bufferedChapter = -1
|
||||||
|
self.bufferedParagraph = -1
|
||||||
|
# Force immediate garbage collection
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
# Calculate next paragraph position
|
# Calculate next paragraph position
|
||||||
nextChapter = self.currentChapter
|
nextChapter = self.currentChapter
|
||||||
@@ -2464,15 +2510,19 @@ class BookReader:
|
|||||||
_wavData_to_cleanup = None
|
_wavData_to_cleanup = None
|
||||||
return
|
return
|
||||||
|
|
||||||
# Store buffered audio
|
# Store buffered audio with position tracking
|
||||||
with self.bufferLock:
|
with self.bufferLock:
|
||||||
if not self.cancelBuffer:
|
if not self.cancelBuffer:
|
||||||
self.bufferedAudio = _wavData_to_cleanup
|
self.bufferedAudio = _wavData_to_cleanup
|
||||||
|
self.bufferedChapter = nextChapter
|
||||||
|
self.bufferedParagraph = nextParagraph
|
||||||
_wavData_to_cleanup = None # Transfer ownership
|
_wavData_to_cleanup = None # Transfer ownership
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error buffering paragraph: {e}")
|
print(f"Error buffering paragraph: {e}")
|
||||||
with self.bufferLock:
|
with self.bufferLock:
|
||||||
self.bufferedAudio = None
|
self.bufferedAudio = None
|
||||||
|
self.bufferedChapter = -1
|
||||||
|
self.bufferedParagraph = -1
|
||||||
if _wavData_to_cleanup is not None:
|
if _wavData_to_cleanup is not None:
|
||||||
del _wavData_to_cleanup
|
del _wavData_to_cleanup
|
||||||
_wavData_to_cleanup = None
|
_wavData_to_cleanup = None
|
||||||
@@ -2508,6 +2558,8 @@ class BookReader:
|
|||||||
del self.bufferedAudio
|
del self.bufferedAudio
|
||||||
self.bufferedAudio = None
|
self.bufferedAudio = None
|
||||||
self.cancelBuffer = False
|
self.cancelBuffer = False
|
||||||
|
# Force garbage collection if we discarded a buffer
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
# Reset thread reference
|
# Reset thread reference
|
||||||
self.bufferThread = None
|
self.bufferThread = None
|
||||||
|
|||||||
Reference in New Issue
Block a user