mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat(runtime): add system resource metrics to /server_info endpoint (#5207)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
36e3dc5c19
commit
bb8b4a0b18
@ -52,6 +52,7 @@ from openhands.runtime.utils.bash import BashSession
|
||||
from openhands.runtime.utils.files import insert_lines, read_lines
|
||||
from openhands.runtime.utils.runtime_init import init_user_and_working_directory
|
||||
from openhands.runtime.utils.system import check_port_available
|
||||
from openhands.runtime.utils.system_stats import get_system_stats
|
||||
from openhands.utils.async_utils import call_sync_from_async, wait_all
|
||||
|
||||
|
||||
@ -420,7 +421,12 @@ if __name__ == '__main__':
|
||||
current_time = time.time()
|
||||
uptime = current_time - client.start_time
|
||||
idle_time = current_time - client.last_execution_time
|
||||
return {'uptime': uptime, 'idle_time': idle_time}
|
||||
|
||||
return {
|
||||
'uptime': uptime,
|
||||
'idle_time': idle_time,
|
||||
'resources': get_system_stats(),
|
||||
}
|
||||
|
||||
@app.post('/execute_action')
|
||||
async def execute_action(action_request: ActionRequest):
|
||||
|
||||
62
openhands/runtime/utils/system_stats.py
Normal file
62
openhands/runtime/utils/system_stats.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""Utilities for getting system resource statistics."""
|
||||
|
||||
import time
|
||||
|
||||
import psutil
|
||||
|
||||
|
||||
def get_system_stats() -> dict:
|
||||
"""Get current system resource statistics.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing:
|
||||
- cpu_percent: CPU usage percentage for the current process
|
||||
- memory: Memory usage stats (rss, vms, percent)
|
||||
- disk: Disk usage stats (total, used, free, percent)
|
||||
- io: I/O statistics (read/write bytes)
|
||||
"""
|
||||
process = psutil.Process()
|
||||
# Get initial CPU percentage (this will return 0.0)
|
||||
process.cpu_percent()
|
||||
# Wait a bit and get the actual CPU percentage
|
||||
time.sleep(0.1)
|
||||
|
||||
with process.oneshot():
|
||||
cpu_percent = process.cpu_percent()
|
||||
memory_info = process.memory_info()
|
||||
memory_percent = process.memory_percent()
|
||||
|
||||
disk_usage = psutil.disk_usage('/')
|
||||
|
||||
# Get I/O stats directly from /proc/[pid]/io to avoid psutil's field name assumptions
|
||||
try:
|
||||
with open(f'/proc/{process.pid}/io', 'rb') as f:
|
||||
io_stats = {}
|
||||
for line in f:
|
||||
if line:
|
||||
try:
|
||||
name, value = line.strip().split(b': ')
|
||||
io_stats[name.decode('ascii')] = int(value)
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
continue
|
||||
except (FileNotFoundError, PermissionError):
|
||||
io_stats = {'read_bytes': 0, 'write_bytes': 0}
|
||||
|
||||
return {
|
||||
'cpu_percent': cpu_percent,
|
||||
'memory': {
|
||||
'rss': memory_info.rss,
|
||||
'vms': memory_info.vms,
|
||||
'percent': memory_percent,
|
||||
},
|
||||
'disk': {
|
||||
'total': disk_usage.total,
|
||||
'used': disk_usage.used,
|
||||
'free': disk_usage.free,
|
||||
'percent': disk_usage.percent,
|
||||
},
|
||||
'io': {
|
||||
'read_bytes': io_stats.get('read_bytes', 0),
|
||||
'write_bytes': io_stats.get('write_bytes', 0),
|
||||
},
|
||||
}
|
||||
60
tests/runtime/utils/test_system_stats.py
Normal file
60
tests/runtime/utils/test_system_stats.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Tests for system stats utilities."""
|
||||
|
||||
import psutil
|
||||
|
||||
from openhands.runtime.utils.system_stats import get_system_stats
|
||||
|
||||
|
||||
def test_get_system_stats():
|
||||
"""Test that get_system_stats returns valid system statistics."""
|
||||
stats = get_system_stats()
|
||||
|
||||
# Test structure
|
||||
assert isinstance(stats, dict)
|
||||
assert set(stats.keys()) == {'cpu_percent', 'memory', 'disk', 'io'}
|
||||
|
||||
# Test CPU stats
|
||||
assert isinstance(stats['cpu_percent'], float)
|
||||
assert 0 <= stats['cpu_percent'] <= 100 * psutil.cpu_count()
|
||||
|
||||
# Test memory stats
|
||||
assert isinstance(stats['memory'], dict)
|
||||
assert set(stats['memory'].keys()) == {'rss', 'vms', 'percent'}
|
||||
assert isinstance(stats['memory']['rss'], int)
|
||||
assert isinstance(stats['memory']['vms'], int)
|
||||
assert isinstance(stats['memory']['percent'], float)
|
||||
assert stats['memory']['rss'] > 0
|
||||
assert stats['memory']['vms'] > 0
|
||||
assert 0 <= stats['memory']['percent'] <= 100
|
||||
|
||||
# Test disk stats
|
||||
assert isinstance(stats['disk'], dict)
|
||||
assert set(stats['disk'].keys()) == {'total', 'used', 'free', 'percent'}
|
||||
assert isinstance(stats['disk']['total'], int)
|
||||
assert isinstance(stats['disk']['used'], int)
|
||||
assert isinstance(stats['disk']['free'], int)
|
||||
assert isinstance(stats['disk']['percent'], float)
|
||||
assert stats['disk']['total'] > 0
|
||||
assert stats['disk']['used'] >= 0
|
||||
assert stats['disk']['free'] >= 0
|
||||
assert 0 <= stats['disk']['percent'] <= 100
|
||||
# Verify that used + free is less than or equal to total
|
||||
# (might not be exactly equal due to filesystem overhead)
|
||||
assert stats['disk']['used'] + stats['disk']['free'] <= stats['disk']['total']
|
||||
|
||||
# Test I/O stats
|
||||
assert isinstance(stats['io'], dict)
|
||||
assert set(stats['io'].keys()) == {'read_bytes', 'write_bytes'}
|
||||
assert isinstance(stats['io']['read_bytes'], int)
|
||||
assert isinstance(stats['io']['write_bytes'], int)
|
||||
assert stats['io']['read_bytes'] >= 0
|
||||
assert stats['io']['write_bytes'] >= 0
|
||||
|
||||
|
||||
def test_get_system_stats_stability():
|
||||
"""Test that get_system_stats can be called multiple times without errors."""
|
||||
# Call multiple times to ensure stability
|
||||
for _ in range(3):
|
||||
stats = get_system_stats()
|
||||
assert isinstance(stats, dict)
|
||||
assert stats['cpu_percent'] >= 0
|
||||
Loading…
x
Reference in New Issue
Block a user