diff --git a/openhands/runtime/action_execution_server.py b/openhands/runtime/action_execution_server.py index 1c19ca62ed..caab652919 100644 --- a/openhands/runtime/action_execution_server.py +++ b/openhands/runtime/action_execution_server.py @@ -72,7 +72,10 @@ from openhands.runtime.utils.bash import BashSession from openhands.runtime.utils.files import insert_lines, read_lines from openhands.runtime.utils.memory_monitor import MemoryMonitor from openhands.runtime.utils.runtime_init import init_user_and_working_directory -from openhands.runtime.utils.system_stats import get_system_stats +from openhands.runtime.utils.system_stats import ( + get_system_stats, + update_last_execution_time, +) from openhands.utils.async_utils import call_sync_from_async, wait_all if sys.platform == 'win32': @@ -844,6 +847,8 @@ if __name__ == '__main__': status_code=500, detail=traceback.format_exc(), ) + finally: + update_last_execution_time() @app.post('/update_mcp_server') async def update_mcp_server(request: Request): diff --git a/openhands/runtime/impl/action_execution/action_execution_client.py b/openhands/runtime/impl/action_execution/action_execution_client.py index 018e338636..773a806aa5 100644 --- a/openhands/runtime/impl/action_execution/action_execution_client.py +++ b/openhands/runtime/impl/action_execution/action_execution_client.py @@ -46,6 +46,7 @@ from openhands.integrations.provider import PROVIDER_TOKEN_TYPE from openhands.runtime.base import Runtime from openhands.runtime.plugins import PluginRequirement from openhands.runtime.utils.request import send_request +from openhands.runtime.utils.system_stats import update_last_execution_time from openhands.utils.http_session import HttpSession from openhands.utils.tenacity_stop import stop_if_should_exit @@ -328,6 +329,8 @@ class ActionExecutionClient(Runtime): raise AgentRuntimeTimeoutError( f'Runtime failed to return execute_action before the requested timeout of {action.timeout}s' ) + finally: + update_last_execution_time() return obs def run(self, action: CmdRunAction) -> Observation: diff --git a/openhands/runtime/utils/system_stats.py b/openhands/runtime/utils/system_stats.py index b34738d48e..dda66f907c 100644 --- a/openhands/runtime/utils/system_stats.py +++ b/openhands/runtime/utils/system_stats.py @@ -4,6 +4,25 @@ import time import psutil +_start_time = time.time() +_last_execution_time = time.time() + + +def get_system_info() -> dict[str, object]: + current_time = time.time() + uptime = current_time - _start_time + idle_time = current_time - _last_execution_time + return { + 'uptime': uptime, + 'idle_time': idle_time, + 'resources': get_system_stats(), + } + + +def update_last_execution_time(): + global _last_execution_time + _last_execution_time = time.time() + def get_system_stats() -> dict[str, object]: """Get current system resource statistics. diff --git a/openhands/server/routes/health.py b/openhands/server/routes/health.py index 2a9d4bb102..2a1c6e5c0b 100644 --- a/openhands/server/routes/health.py +++ b/openhands/server/routes/health.py @@ -1,11 +1,6 @@ -import time +from fastapi import FastAPI -from fastapi import FastAPI, Request - -from openhands.runtime.utils.system_stats import get_system_stats - -start_time = time.time() -last_execution_time = start_time +from openhands.runtime.utils.system_stats import get_system_info def add_health_endpoints(app: FastAPI): @@ -19,20 +14,4 @@ def add_health_endpoints(app: FastAPI): @app.get('/server_info') async def get_server_info(): - current_time = time.time() - uptime = current_time - start_time - idle_time = current_time - last_execution_time - - response = { - 'uptime': uptime, - 'idle_time': idle_time, - 'resources': get_system_stats(), - } - return response - - @app.middleware('http') - async def update_last_execution_time(request: Request, call_next): - global last_execution_time - response = await call_next(request) - last_execution_time = time.time() - return response + return get_system_info() diff --git a/tests/runtime/utils/test_system_stats.py b/tests/runtime/utils/test_system_stats.py index afb6c00c29..7fd52cb221 100644 --- a/tests/runtime/utils/test_system_stats.py +++ b/tests/runtime/utils/test_system_stats.py @@ -1,8 +1,15 @@ """Tests for system stats utilities.""" +import time +from unittest.mock import patch + import psutil -from openhands.runtime.utils.system_stats import get_system_stats +from openhands.runtime.utils.system_stats import ( + get_system_info, + get_system_stats, + update_last_execution_time, +) def test_get_system_stats(): @@ -58,3 +65,96 @@ def test_get_system_stats_stability(): stats = get_system_stats() assert isinstance(stats, dict) assert stats['cpu_percent'] >= 0 + + +def test_get_system_info(): + """Test that get_system_info returns valid system information.""" + with patch( + 'openhands.runtime.utils.system_stats.get_system_stats' + ) as mock_get_stats: + mock_get_stats.return_value = {'cpu_percent': 10.0} + + info = get_system_info() + + # Test structure + assert isinstance(info, dict) + assert set(info.keys()) == {'uptime', 'idle_time', 'resources'} + + # Test values + assert isinstance(info['uptime'], float) + assert isinstance(info['idle_time'], float) + assert info['uptime'] > 0 + assert info['idle_time'] >= 0 + assert info['resources'] == {'cpu_percent': 10.0} + + # Verify get_system_stats was called + mock_get_stats.assert_called_once() + + +def test_update_last_execution_time(): + """Test that update_last_execution_time updates the last execution time.""" + # Get initial system info + initial_info = get_system_info() + initial_idle_time = initial_info['idle_time'] + + # Wait a bit to ensure time difference + time.sleep(0.1) + + # Update last execution time + update_last_execution_time() + + # Get updated system info + updated_info = get_system_info() + updated_idle_time = updated_info['idle_time'] + + # The idle time should be reset (close to zero) + assert updated_idle_time < initial_idle_time + assert updated_idle_time < 0.1 # Should be very small + + +def test_idle_time_increases_without_updates(): + """Test that idle_time increases when no updates are made.""" + # Update last execution time to reset idle time + update_last_execution_time() + + # Get initial system info + initial_info = get_system_info() + initial_idle_time = initial_info['idle_time'] + + # Wait a bit + time.sleep(0.2) + + # Get updated system info without calling update_last_execution_time + updated_info = get_system_info() + updated_idle_time = updated_info['idle_time'] + + # The idle time should have increased + assert updated_idle_time > initial_idle_time + assert updated_idle_time >= 0.2 # Should be at least the sleep time + + +@patch('time.time') +def test_idle_time_calculation(mock_time): + """Test that idle_time is calculated correctly.""" + # Mock time.time() to return controlled values + mock_time.side_effect = [ + 100.0, # Initial _start_time + 100.0, # Initial _last_execution_time + 110.0, # Current time in get_system_info + ] + + # Import the module again to reset the global variables with our mocked time + import importlib + + import openhands.runtime.utils.system_stats + + importlib.reload(openhands.runtime.utils.system_stats) + + # Get system info + from openhands.runtime.utils.system_stats import get_system_info + + info = get_system_info() + + # Verify idle_time calculation + assert info['uptime'] == 10.0 # 110 - 100 + assert info['idle_time'] == 10.0 # 110 - 100