Auto-pause physics engine when no one is viewing
- Physics pauses after IDLE_PAUSE_SECONDS (default 30s) of inactivity - Resumes instantly when someone views the dashboard - No container restart needed - just pauses the simulation loop - CPU usage drops to ~0% when paused
This commit is contained in:
@@ -8,7 +8,6 @@ thermal-electrical coupling in real-time using instrument interfaces.
|
|||||||
import asyncio
|
import asyncio
|
||||||
import atexit
|
import atexit
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from collections import deque
|
from collections import deque
|
||||||
@@ -29,40 +28,36 @@ from py_dvt_ate.tests.thermal.tempco import TempCoTest
|
|||||||
# Thread pool for background test execution
|
# Thread pool for background test execution
|
||||||
_test_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="test_runner")
|
_test_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="test_runner")
|
||||||
|
|
||||||
# Idle shutdown configuration
|
# Idle pause configuration
|
||||||
# Set IDLE_TIMEOUT_MINUTES=0 to disable auto-shutdown
|
# Physics engine pauses after IDLE_PAUSE_SECONDS of no activity (default: 30s)
|
||||||
IDLE_TIMEOUT_MINUTES = int(os.environ.get("IDLE_TIMEOUT_MINUTES", "0"))
|
IDLE_PAUSE_SECONDS = int(os.environ.get("IDLE_PAUSE_SECONDS", "30"))
|
||||||
_last_activity_time: float = time.time()
|
_last_activity_time: float = time.time()
|
||||||
_idle_monitor_started = False
|
_idle_checker_started = False
|
||||||
|
_server_ref: SimulationServer | None = None # Reference for idle checker thread
|
||||||
|
|
||||||
|
|
||||||
def _update_activity() -> None:
|
def _idle_checker() -> None:
|
||||||
"""Update the last activity timestamp."""
|
"""Background thread that pauses physics when idle."""
|
||||||
global _last_activity_time
|
global _last_activity_time, _server_ref
|
||||||
_last_activity_time = time.time()
|
|
||||||
|
|
||||||
|
|
||||||
def _idle_monitor() -> None:
|
|
||||||
"""Background thread that exits the app after idle timeout."""
|
|
||||||
global _last_activity_time
|
|
||||||
while True:
|
while True:
|
||||||
time.sleep(60) # Check every minute
|
time.sleep(5) # Check every 5 seconds
|
||||||
if IDLE_TIMEOUT_MINUTES <= 0:
|
if _server_ref is None:
|
||||||
continue
|
continue
|
||||||
idle_minutes = (time.time() - _last_activity_time) / 60
|
|
||||||
if idle_minutes >= IDLE_TIMEOUT_MINUTES:
|
idle_seconds = time.time() - _last_activity_time
|
||||||
print(f"Idle timeout reached ({IDLE_TIMEOUT_MINUTES} minutes). Shutting down.")
|
if idle_seconds > IDLE_PAUSE_SECONDS and not _server_ref.paused:
|
||||||
os._exit(0)
|
_server_ref.paused = True
|
||||||
|
print(f"Physics engine paused (idle for {idle_seconds:.0f}s)")
|
||||||
|
|
||||||
|
|
||||||
def _start_idle_monitor() -> None:
|
def _start_idle_checker(server: SimulationServer) -> None:
|
||||||
"""Start the idle monitor thread if timeout is configured."""
|
"""Start the idle checker thread."""
|
||||||
global _idle_monitor_started
|
global _idle_checker_started, _server_ref
|
||||||
if IDLE_TIMEOUT_MINUTES > 0 and not _idle_monitor_started:
|
_server_ref = server
|
||||||
_idle_monitor_started = True
|
if not _idle_checker_started:
|
||||||
thread = threading.Thread(target=_idle_monitor, daemon=True)
|
_idle_checker_started = True
|
||||||
|
thread = threading.Thread(target=_idle_checker, daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
print(f"Idle auto-shutdown enabled: {IDLE_TIMEOUT_MINUTES} minutes")
|
|
||||||
|
|
||||||
# History buffer size for charts
|
# History buffer size for charts
|
||||||
HISTORY_SIZE = 500
|
HISTORY_SIZE = 500
|
||||||
@@ -159,12 +154,21 @@ def start_embedded_server() -> tuple[SimulationServer, threading.Thread]:
|
|||||||
return server, thread
|
return server, thread
|
||||||
|
|
||||||
|
|
||||||
|
def _update_activity() -> None:
|
||||||
|
"""Update activity timestamp and resume physics if paused."""
|
||||||
|
global _last_activity_time
|
||||||
|
_last_activity_time = time.time()
|
||||||
|
|
||||||
|
# Resume physics if it was paused
|
||||||
|
if "server" in st.session_state:
|
||||||
|
server = st.session_state.server
|
||||||
|
if server.paused:
|
||||||
|
server.paused = False
|
||||||
|
print("Physics engine resumed (user activity detected)")
|
||||||
|
|
||||||
|
|
||||||
def init_session_state() -> None:
|
def init_session_state() -> None:
|
||||||
"""Initialise Streamlit session state."""
|
"""Initialise Streamlit session state."""
|
||||||
# Start idle monitor for auto-shutdown
|
|
||||||
_start_idle_monitor()
|
|
||||||
_update_activity()
|
|
||||||
|
|
||||||
if "server" not in st.session_state:
|
if "server" not in st.session_state:
|
||||||
with st.spinner("Starting simulation server..."):
|
with st.spinner("Starting simulation server..."):
|
||||||
st.session_state.server, st.session_state.server_thread = start_embedded_server()
|
st.session_state.server, st.session_state.server_thread = start_embedded_server()
|
||||||
@@ -174,6 +178,9 @@ def init_session_state() -> None:
|
|||||||
st.error("Failed to start simulation server. Please refresh the page.")
|
st.error("Failed to start simulation server. Please refresh the page.")
|
||||||
st.stop()
|
st.stop()
|
||||||
|
|
||||||
|
# Start idle checker to pause physics when no one's viewing
|
||||||
|
_start_idle_checker(st.session_state.server)
|
||||||
|
|
||||||
# Register cleanup
|
# Register cleanup
|
||||||
def cleanup() -> None:
|
def cleanup() -> None:
|
||||||
if hasattr(st.session_state, "server") and st.session_state.server is not None:
|
if hasattr(st.session_state, "server") and st.session_state.server is not None:
|
||||||
@@ -427,7 +434,7 @@ def display_controls() -> None:
|
|||||||
@st.fragment(run_every=0.1)
|
@st.fragment(run_every=0.1)
|
||||||
def simulation_display() -> None:
|
def simulation_display() -> None:
|
||||||
"""Fragment that displays and updates simulation state."""
|
"""Fragment that displays and updates simulation state."""
|
||||||
_update_activity() # Track that someone is viewing the dashboard
|
_update_activity() # Track activity and resume physics if paused
|
||||||
|
|
||||||
if "server" not in st.session_state:
|
if "server" not in st.session_state:
|
||||||
st.warning("Initializing simulation server...")
|
st.warning("Initializing simulation server...")
|
||||||
|
|||||||
Reference in New Issue
Block a user