diff --git a/RELEASE.md b/RELEASE.md index 9f13364..f9d2e2a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -4,6 +4,12 @@ OpenFilter Library release notes ## [Unreleased] +## v1.1.2 - 2026-06-28 + +### Fixed + +- **GPU usage percent emitted as float for Cloud Monitoring ([FILTER-585](https://plainsight-ai.atlassian.net/browse/FILTER-585))**: `Metrics.gpu_thread_func` now casts `gpu_usage_percent` to `float` before export. NVML reports `gpu_util` as an integer, but the Cloud Monitoring descriptor `openfilter_gpu_usage_percent` is type-locked to DOUBLE, so each export batch had one point rejected with `value type for metric must be DOUBLE, but is INT64`. The cast is targeted to this single summary field; the per-device `gpuN` series and the legitimately INT64 descriptors (`gpu_accessible`, `camera_connected`) are unchanged. + ## v1.1.1 - 2026-06-01 ### Fixed diff --git a/VERSION b/VERSION index 56130fb..0f1acbd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.1.1 +v1.1.2 diff --git a/openfilter/filter_runtime/metrics.py b/openfilter/filter_runtime/metrics.py index edbc201..2990157 100644 --- a/openfilter/filter_runtime/metrics.py +++ b/openfilter/filter_runtime/metrics.py @@ -134,7 +134,7 @@ def gpu_thread_func(self, stop_evt: Event): self.gpu = gpu self.gpu_accessible = 1 - self.gpu_usage_percent = gpu.get('gpu0', 0.0) + self.gpu_usage_percent = float(gpu.get('gpu0', 0.0)) # NVML gpu_util is int; Cloud Monitoring's DOUBLE descriptor rejects it otherwise except Exception as exc: logger.error(exc) diff --git a/tests/test_gpu_usage_percent_float.py b/tests/test_gpu_usage_percent_float.py new file mode 100644 index 0000000..4f9d524 --- /dev/null +++ b/tests/test_gpu_usage_percent_float.py @@ -0,0 +1,35 @@ +"""Tests for ensuring that openfilter_gpu_usage_percent is always emitted as a float.""" + +from unittest.mock import MagicMock, patch + +from openfilter.filter_runtime.gpu import GPUMetrics +from openfilter.filter_runtime.metrics import Metrics + + +def _run_one_iteration(mock_metrics): + """Run gpu_thread_func for exactly one loop iteration with mocked NVML metrics.""" + metrics = object.__new__(Metrics) # bare object, skip __init__'s background threads + + stop_evt = MagicMock() + stop_evt.wait.side_effect = [False, True] # run the loop body exactly once + + with patch('openfilter.filter_runtime.metrics.get_gpu_metrics', return_value=mock_metrics): + metrics.gpu_thread_func(stop_evt) + + return metrics + + +def test_gpu_usage_percent_is_float(): + """gpu_usage_percent is cast to float even when NVML returns an int gpu_util.""" + metrics = _run_one_iteration([GPUMetrics(index=0, gpu_util=55, mem_used_mb=1024)]) + + assert type(metrics.gpu_usage_percent) is float + assert metrics.gpu_usage_percent == 55.0 + + +def test_gpu_usage_percent_is_float_when_gpu_idle(): + """An idle GPU reports integer gpu_util==0; without the float() cast this path would emit an int 0 and be rejected by the DOUBLE-locked Cloud Monitoring descriptor, so this test fails on the un-cast code.""" + metrics = _run_one_iteration([GPUMetrics(index=0, gpu_util=0, mem_used_mb=1024)]) + + assert type(metrics.gpu_usage_percent) is float + assert metrics.gpu_usage_percent == 0.0