Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions config/config.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"daily_forecast": 30,
"stock_news": 20,
"odds_ticker": 60,
"leaderboard": 60,
"leaderboard": 300,
"nhl_live": 30,
"nhl_recent": 30,
"nhl_upcoming": 30,
Expand Down Expand Up @@ -197,13 +197,11 @@
"update_interval": 3600,
"scroll_speed": 1,
"scroll_delay": 0.01,
"display_duration": 60,
"loop": false,
"request_timeout": 30,
"dynamic_duration": true,
"min_duration": 45,
"max_duration": 600,
"duration_buffer": 0.1,
"min_duration": 30,
"max_display_time": 600,
"background_service": {
"enabled": true,
"max_workers": 3,
Expand Down
36 changes: 25 additions & 11 deletions src/display_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,11 +508,15 @@ def get_current_duration(self) -> int:
if not hasattr(self, '_last_logged_duration') or self._last_logged_duration != dynamic_duration:
logger.info(f"Using dynamic duration for stocks: {dynamic_duration} seconds")
self._last_logged_duration = dynamic_duration
# Debug: Always log the current dynamic duration value
logger.debug(f"Stocks dynamic duration check: {dynamic_duration}s")
return dynamic_duration
except Exception as e:
logger.error(f"Error getting dynamic duration for stocks: {e}")
# Fall back to configured duration
return self.display_durations.get(mode_key, 60)
fallback_duration = self.display_durations.get(mode_key, 60)
logger.debug(f"Using fallback duration for stocks: {fallback_duration}s")
return fallback_duration

# Handle dynamic duration for stock_news
if mode_key == 'stock_news' and self.news:
Expand Down Expand Up @@ -542,19 +546,20 @@ def get_current_duration(self) -> int:
# Fall back to configured duration
return self.display_durations.get(mode_key, 60)

# Handle dynamic duration for leaderboard
# Handle leaderboard duration (user choice between fixed or dynamic)
if mode_key == 'leaderboard' and self.leaderboard:
try:
dynamic_duration = self.leaderboard.get_dynamic_duration()
duration = self.leaderboard.get_duration()
mode_type = "dynamic" if self.leaderboard.dynamic_duration else "fixed"
# Only log if duration has changed or we haven't logged this duration yet
if not hasattr(self, '_last_logged_leaderboard_duration') or self._last_logged_leaderboard_duration != dynamic_duration:
logger.info(f"Using dynamic duration for leaderboard: {dynamic_duration} seconds")
self._last_logged_leaderboard_duration = dynamic_duration
return dynamic_duration
if not hasattr(self, '_last_logged_leaderboard_duration') or self._last_logged_leaderboard_duration != duration:
logger.info(f"Using leaderboard {mode_type} duration: {duration} seconds")
self._last_logged_leaderboard_duration = duration
return duration
except Exception as e:
logger.error(f"Error getting dynamic duration for leaderboard: {e}")
logger.error(f"Error getting duration for leaderboard: {e}")
# Fall back to configured duration
return self.display_durations.get(mode_key, 60)
return self.display_durations.get(mode_key, 600)

# Simplify weather key handling
if mode_key.startswith('weather_'):
Expand All @@ -576,6 +581,8 @@ def _update_modules(self):
# Defer updates for modules that might cause lag during scrolling
if self.odds_ticker:
self.display_manager.defer_update(self.odds_ticker.update, priority=1)
if self.leaderboard:
self.display_manager.defer_update(self.leaderboard.update, priority=1)
if self.stocks:
self.display_manager.defer_update(self.stocks.update_stock_data, priority=2)
if self.news:
Expand Down Expand Up @@ -1127,8 +1134,9 @@ def run(self):
# Update data for all modules first
self._update_modules()

# Process any deferred updates that may have accumulated
self.display_manager.process_deferred_updates()
# Process deferred updates less frequently when scrolling to improve performance
if not self.display_manager.is_currently_scrolling() or (current_time % 2.0 < 0.1):
self.display_manager.process_deferred_updates()

# Update live modes in rotation if needed
self._update_live_modes_in_rotation()
Expand Down Expand Up @@ -1250,6 +1258,10 @@ def run(self):
if hasattr(self, '_last_logged_duration'):
delattr(self, '_last_logged_duration')
elif current_time - self.last_switch >= self.get_current_duration() or self.force_change:
# Debug timing information
elapsed_time = current_time - self.last_switch
expected_duration = self.get_current_duration()
logger.debug(f"Mode switch triggered: {self.current_display_mode} - Elapsed: {elapsed_time:.1f}s, Expected: {expected_duration}s, Force: {self.force_change}")
self.force_change = False
if self.current_display_mode == 'calendar' and self.calendar:
self.calendar.advance_event()
Expand All @@ -1271,6 +1283,8 @@ def run(self):
if needs_switch:
self.force_clear = True
self.last_switch = current_time
# Debug: Log when we set the switch time for a new mode
logger.debug(f"Mode switch completed: {self.current_display_mode} - Switch time set to {current_time}, Duration: {self.get_current_duration()}s")
else:
self.force_clear = False
# Only set manager_to_display if it hasn't been set by live priority logic
Expand Down
75 changes: 64 additions & 11 deletions src/display_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ def __init__(self, config: Dict[str, Any] = None, force_fallback: bool = False,
'is_scrolling': False,
'last_scroll_activity': 0,
'scroll_inactivity_threshold': 2.0, # seconds of inactivity before considering "not scrolling"
'deferred_updates': []
'deferred_updates': [],
'max_deferred_updates': 50, # Limit queue size to prevent memory issues
'deferred_update_ttl': 300.0 # 5 minutes TTL for deferred updates
}

self._setup_matrix()
Expand Down Expand Up @@ -677,13 +679,27 @@ def defer_update(self, update_func, priority: int = 0):
update_func: Function to call when not scrolling
priority: Priority level (lower numbers = higher priority)
"""
current_time = time.time()

# Clean up expired updates before adding new ones
self._cleanup_expired_deferred_updates(current_time)

# Limit queue size to prevent memory issues
if len(self._scrolling_state['deferred_updates']) >= self._scrolling_state['max_deferred_updates']:
# Remove oldest update to make room
self._scrolling_state['deferred_updates'].pop(0)
logger.debug("Removed oldest deferred update due to queue size limit")

self._scrolling_state['deferred_updates'].append({
'func': update_func,
'priority': priority,
'timestamp': time.time()
'timestamp': current_time
})
# Sort by priority (lower numbers first)
self._scrolling_state['deferred_updates'].sort(key=lambda x: x['priority'])

# Only sort if we have a reasonable number of updates to avoid excessive sorting
if len(self._scrolling_state['deferred_updates']) <= 20:
self._scrolling_state['deferred_updates'].sort(key=lambda x: x['priority'])

logger.debug(f"Deferred update added. Total deferred: {len(self._scrolling_state['deferred_updates'])}")

def process_deferred_updates(self):
Expand All @@ -693,29 +709,66 @@ def process_deferred_updates(self):

if not self._scrolling_state['deferred_updates']:
return

current_time = time.time()

# Clean up expired updates first
self._cleanup_expired_deferred_updates(current_time)

# Process all deferred updates
updates_to_process = self._scrolling_state['deferred_updates'].copy()
self._scrolling_state['deferred_updates'].clear()
if not self._scrolling_state['deferred_updates']:
return

# Process only a limited number of updates per call to avoid blocking
max_updates_per_call = min(5, len(self._scrolling_state['deferred_updates']))
updates_to_process = self._scrolling_state['deferred_updates'][:max_updates_per_call]
self._scrolling_state['deferred_updates'] = self._scrolling_state['deferred_updates'][max_updates_per_call:]

logger.debug(f"Processing {len(updates_to_process)} deferred updates")
logger.debug(f"Processing {len(updates_to_process)} deferred updates (queue size: {len(self._scrolling_state['deferred_updates'])})")

failed_updates = []
for update_info in updates_to_process:
try:
# Check if update is still valid (not too old)
if current_time - update_info['timestamp'] > self._scrolling_state['deferred_update_ttl']:
logger.debug("Skipping expired deferred update")
continue

update_info['func']()
logger.debug("Deferred update executed successfully")
except Exception as e:
logger.error(f"Error executing deferred update: {e}")
# Re-add failed updates for retry
self._scrolling_state['deferred_updates'].append(update_info)
# Only retry recent failures, and limit retries
if current_time - update_info['timestamp'] < 60.0: # Only retry for 1 minute
failed_updates.append(update_info)

# Re-add failed updates to the end of the queue (not the beginning)
if failed_updates:
self._scrolling_state['deferred_updates'].extend(failed_updates)

def _cleanup_expired_deferred_updates(self, current_time: float):
"""Remove expired deferred updates to prevent memory leaks."""
ttl = self._scrolling_state['deferred_update_ttl']
initial_count = len(self._scrolling_state['deferred_updates'])

# Filter out expired updates
self._scrolling_state['deferred_updates'] = [
update for update in self._scrolling_state['deferred_updates']
if current_time - update['timestamp'] <= ttl
]

removed_count = initial_count - len(self._scrolling_state['deferred_updates'])
if removed_count > 0:
logger.debug(f"Cleaned up {removed_count} expired deferred updates")

def get_scrolling_stats(self) -> dict:
"""Get current scrolling statistics for debugging."""
return {
'is_scrolling': self._scrolling_state['is_scrolling'],
'last_activity': self._scrolling_state['last_scroll_activity'],
'deferred_count': len(self._scrolling_state['deferred_updates']),
'inactivity_threshold': self._scrolling_state['scroll_inactivity_threshold']
'inactivity_threshold': self._scrolling_state['scroll_inactivity_threshold'],
'max_deferred_updates': self._scrolling_state['max_deferred_updates'],
'deferred_update_ttl': self._scrolling_state['deferred_update_ttl']
}

def _write_snapshot_if_due(self) -> None:
Expand Down
Loading