diff --git a/src/server/communication/request_handler.py b/src/server/communication/request_handler.py index aaff010..84a2cd3 100644 --- a/src/server/communication/request_handler.py +++ b/src/server/communication/request_handler.py @@ -102,8 +102,29 @@ def _cpu_frequency_values() -> list[float]: return values +_CPU_TIMES_FIELDS = ( + "idle", + "user", + "system", + "nice", + "iowait", + "irq", + "softirq", + "steal", + "guest", + "guest_nice", +) +_previous_cpu_times: dict[str, float] | None = None +_previous_cpu_times_lock = threading.Lock() + + def get_edge_system_metrics() -> dict: - """Return edge host system metrics used in offloading decision telemetry.""" + """Return edge host system metrics used in offloading decision telemetry. + + ``cpu_idle``/``cpu_user``/``cpu_system`` are the fraction of CPU time spent + in each state since the previous call, not the cumulative time since boot + that ``psutil.cpu_times()`` reports. + """ metrics = { "cpu_idle": 0.0, "cpu_user": 0.0, @@ -121,9 +142,25 @@ def get_edge_system_metrics() -> dict: try: cpu_times = psutil.cpu_times() - metrics["cpu_idle"] = _finite_metric(getattr(cpu_times, "idle", 0.0)) - metrics["cpu_user"] = _finite_metric(getattr(cpu_times, "user", 0.0)) - metrics["cpu_system"] = _finite_metric(getattr(cpu_times, "system", 0.0)) + current = { + field: getattr(cpu_times, field) + for field in _CPU_TIMES_FIELDS + if hasattr(cpu_times, field) + } + with _previous_cpu_times_lock: + global _previous_cpu_times + previous = _previous_cpu_times + _previous_cpu_times = current + if previous is not None: + delta = { + field: current[field] - previous.get(field, current[field]) + for field in current + } + total = sum(value for value in delta.values() if value > 0) + if total > 0: + metrics["cpu_idle"] = _finite_metric(delta.get("idle", 0.0) / total) + metrics["cpu_user"] = _finite_metric(delta.get("user", 0.0) / total) + metrics["cpu_system"] = _finite_metric(delta.get("system", 0.0) / total) except Exception: pass diff --git a/tests/unit/test_request_handler_helpers.py b/tests/unit/test_request_handler_helpers.py index d97f8a4..ae78ac6 100644 --- a/tests/unit/test_request_handler_helpers.py +++ b/tests/unit/test_request_handler_helpers.py @@ -101,15 +101,16 @@ def cpu_percent(interval=None): assert get_edge_cpu_percent() == 0.0 -def test_get_edge_system_metrics_collects_psutil_fields(monkeypatch): - class Obj: - def __init__(self, **kwargs): - self.__dict__.update(kwargs) +class _Obj: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + +def _make_fake_psutil(monkeypatch, cpu_times_obj): class FakePsutil: @staticmethod def cpu_times(): - return Obj(idle=100.1234, user=20.5678, system=7.8912) + return cpu_times_obj @staticmethod def cpu_count(): @@ -118,7 +119,7 @@ def cpu_count(): @staticmethod def cpu_freq(percpu=True): assert percpu is True - return [Obj(current=2400.1234), Obj(current=2399.9876)] + return [_Obj(current=2400.1234), _Obj(current=2399.9876)] @staticmethod def getloadavg(): @@ -126,14 +127,23 @@ def getloadavg(): @staticmethod def virtual_memory(): - return Obj(total=16_000, available=4_000) + return _Obj(total=16_000, available=4_000) monkeypatch.setattr(rh, "psutil", FakePsutil) - assert get_edge_system_metrics() == { - "cpu_idle": 100.123, - "cpu_user": 20.568, - "cpu_system": 7.891, + +def test_get_edge_system_metrics_reports_zero_cpu_ratios_on_first_call(monkeypatch): + monkeypatch.setattr(rh, "_previous_cpu_times", None) + _make_fake_psutil(monkeypatch, _Obj(idle=100.0, user=20.0, system=7.0)) + + result = get_edge_system_metrics() + + # No prior sample yet: cpu ratios can't be computed, only the other + # (non-cumulative) fields are meaningful on the first call. + assert result == { + "cpu_idle": 0.0, + "cpu_user": 0.0, + "cpu_system": 0.0, "cpu_count": 8, "cpu_freq": [2400.123, 2399.988], "load_avg_1_min": 1.234, @@ -144,6 +154,21 @@ def virtual_memory(): } +def test_get_edge_system_metrics_reports_delta_ratio_on_subsequent_call(monkeypatch): + monkeypatch.setattr(rh, "_previous_cpu_times", None) + _make_fake_psutil(monkeypatch, _Obj(idle=100.0, user=20.0, system=7.0)) + get_edge_system_metrics() + + # Second call, 10s later: idle +8, user +1, system +1 -> total delta 10. + _make_fake_psutil(monkeypatch, _Obj(idle=108.0, user=21.0, system=8.0)) + + result = get_edge_system_metrics() + + assert result["cpu_idle"] == pytest.approx(0.8) + assert result["cpu_user"] == pytest.approx(0.1) + assert result["cpu_system"] == pytest.approx(0.1) + + def test_get_edge_system_metrics_returns_defaults_without_psutil(monkeypatch): monkeypatch.setattr(rh, "psutil", None)