From 02e772c799aa474eb7108a39d724111ea238e721 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Wed, 10 Dec 2025 10:40:17 -0500 Subject: [PATCH] Hopefully finally tracked down and fix the high memory alert on text based books. --- bookstorm.py | 80 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/bookstorm.py b/bookstorm.py index a72c2cb..016da10 100755 --- a/bookstorm.py +++ b/bookstorm.py @@ -194,6 +194,8 @@ class BookReader: # Audio buffering for seamless playback 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.cancelBuffer = False self.bufferLock = threading.Lock() @@ -822,16 +824,42 @@ class BookReader: gc.collect(generation=0) # Fast collection at 10 seconds # 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: - import resource - # pylint: disable=no-member - memUsage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024 # MB + # 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 + # pylint: disable=no-member + currentMemMb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024 - # More aggressive memory warnings and cleanup - if memUsage > 1536: # 1.5GB threshold + # Get total system memory from /proc/meminfo + 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: - 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 if hasattr(self, 'parser') and self.parser: self.parser.cleanup() @@ -847,7 +875,7 @@ class BookReader: gc.collect() gc.collect() # Second pass - if memUsage > 2048 and not memoryWarningShown: + if currentMemMb > warningThreshold and not memoryWarningShown: memoryWarningShown = True self.speechEngine.speak("Warning: High memory usage detected. Consider restarting BookStorm soon.") except Exception as e: @@ -2303,13 +2331,26 @@ class BookReader: if not self.brailleOutput.muteVoice: wavData = None try: - # Check if we have buffered audio ready + # Check if we have buffered audio ready for THIS paragraph with self.bufferLock: - if self.bufferedAudio is not None: - # Use pre-generated audio + if (self.bufferedAudio is not None and + self.bufferedChapter == self.currentChapter and + self.bufferedParagraph == self.currentParagraph): + # Use pre-generated audio (matches current position) wavData = self.bufferedAudio self.bufferedAudio = None + self.bufferedChapter = -1 + self.bufferedParagraph = -1 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) wavData = self.ttsEngine.text_to_wav_data(paragraph) @@ -2422,12 +2463,17 @@ class BookReader: return # 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: 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 self.bufferedAudio = None + self.bufferedChapter = -1 + self.bufferedParagraph = -1 + # Force immediate garbage collection + gc.collect() # Calculate next paragraph position nextChapter = self.currentChapter @@ -2464,15 +2510,19 @@ class BookReader: _wavData_to_cleanup = None return - # Store buffered audio + # Store buffered audio with position tracking with self.bufferLock: if not self.cancelBuffer: self.bufferedAudio = _wavData_to_cleanup + self.bufferedChapter = nextChapter + self.bufferedParagraph = nextParagraph _wavData_to_cleanup = None # Transfer ownership except Exception as e: print(f"Error buffering paragraph: {e}") with self.bufferLock: self.bufferedAudio = None + self.bufferedChapter = -1 + self.bufferedParagraph = -1 if _wavData_to_cleanup is not None: del _wavData_to_cleanup _wavData_to_cleanup = None @@ -2508,6 +2558,8 @@ class BookReader: del self.bufferedAudio self.bufferedAudio = None self.cancelBuffer = False + # Force garbage collection if we discarded a buffer + gc.collect() # Reset thread reference self.bufferThread = None