Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`@ruvnet/rvagent` startup optimization — stdio time-to-first-response ~242 ms → ~189 ms (−22%; MEASURED, median of repeated `initialize` round-trips against `dist/index.js`, this container, reproduce with a piped-stdin timer).** Two changes: (1) `./http-transport.js` is now imported **lazily** inside the `RVAGENT_HTTP_PORT` branch — it chain-loads the MCP SDK's `streamableHttp` module (~48 ms MEASURED via per-module `import()` timing), which the default stdio path never uses; (2) the advertised JSON Schemas generated from the Zod sources are memoized per tool instead of re-walking the Zod tree on every `tools/list` (matters under the session-per-server HTTP model where each session lists tools). No behavior change: 99/99 jest tests, HTTP session flow re-smoke-tested through the lazy path. The `@ruvnet/ruview` harness CLI was profiled too and left alone — 50 ms vs the ~29 ms bare `node -e ''` floor on the same box (MEASURED), i.e. already near the interpreter floor with zero dependencies.

### Fixed
- **FastAPI health/metrics endpoints event-loop starvation.** Calling `psutil.cpu_percent(interval=1)` blocked the single-threaded async event loop for 1.0 second on every health check or metrics collection tick, stalling all incoming requests and WebSocket operations. Fixed by changing `cpu_percent` to use non-blocking `interval=None` and offloading all blocking OS metrics gathering to background thread pools via `asyncio.to_thread`. Verified event loop responsiveness via concurrency regression tests.
- **EngineBridge now honors `WDP_GUARD_INTERVAL_US`/`WDP_SOFT_GUARD_US`/`WDP_TDM_SLOTS`+`WDP_TDM_SLOT_US`** (#1309, PR #1312, @erichkusuki). The governed trust path previously built its multistatic fuser from a hardcoded `MultistaticConfig::default()` (60 ms guard), so multi-node deployments with WiFi/ESP-NOW time sync (10–150 ms drift) failed every governed cycle regardless of configuration — while the startup log claimed the override took effect. New `StreamingEngine::set_multistatic_config()`; `EngineBridge::new()` takes an `Option<MultistaticConfig>` threaded from the same env-derived config as `AppState.multistatic_fuser`. Hardware-verified on a live 2-node ESP32-S3 setup (90 s window, 0 fusion errors; previously every cycle failed).
- **`/api/v1/stream/pose` WebSocket reachable with `RUVIEW_API_TOKEN` set + dashboard bearer-token field** (#1310, PR #1313, @erichkusuki). Browsers cannot attach an `Authorization` header to a WS upgrade, so the Live Demo pose stream always failed when auth was on; the path is now on a narrow exact-match exemption list (mirrors `/ws/sensing`), with a regression test pinning that the exemption doesn't leak to other `/api/v1/*` paths. The QuickSettings panel gains an "API Access" field storing the bearer token in `localStorage`; the token is applied at `api.service.js` module load so the very first request carries it.
- **Display-less DevKitC-1 boards: `sdkconfig.defaults.devkitc` build overlay** (#1308, PR #1311, @erichkusuki). The ADR-045 runtime display probe false-positives on stock ESP32-S3-DevKitC-1 (floating QSPI pins), which silently skipped the RuView#893 MGMT+DATA CSI upgrade and collapsed CSI yield to 0 pps. The overlay compiles display support out (`has_display` constant-false). Also fixes stale `espressif/idf:v5.2` README references to v5.4 (source requires `esp_driver_uart`, IDF ≥5.3). Hardware-verified on 2× DevKitC-1-N16R8 (0 → 40–45 pps).
Expand Down
10 changes: 6 additions & 4 deletions archive/v1/src/api/routers/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Health check API endpoints
"""

import asyncio
import logging
import psutil
from typing import Dict, Any, Optional
Expand Down Expand Up @@ -168,7 +169,7 @@ async def health_check(request: Request):
overall_status = "degraded"

# Get system metrics
system_metrics = get_system_metrics()
system_metrics = await asyncio.to_thread(get_system_metrics)

Comment thread
sushantguri marked this conversation as resolved.
uptime_seconds = (datetime.now() - _APP_START_TIME).total_seconds()

Expand Down Expand Up @@ -263,11 +264,12 @@ async def get_health_metrics(
):
"""Get detailed system metrics."""
try:
metrics = get_system_metrics()
metrics = await asyncio.to_thread(get_system_metrics)

# Add additional metrics if authenticated
if current_user:
metrics.update(get_detailed_metrics())
detailed = await asyncio.to_thread(get_detailed_metrics)
metrics.update(detailed)

return {
"timestamp": datetime.utcnow().isoformat(),
Expand Down Expand Up @@ -300,7 +302,7 @@ def get_system_metrics() -> Dict[str, Any]:
"""Get basic system metrics."""
try:
# CPU metrics
cpu_percent = psutil.cpu_percent(interval=1)
cpu_percent = psutil.cpu_percent(interval=None)
cpu_count = psutil.cpu_count()

# Memory metrics
Expand Down
23 changes: 13 additions & 10 deletions archive/v1/src/services/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,21 +180,24 @@ async def collect_metrics(self):
async def _collect_system_metrics(self):
"""Collect system-level metrics."""
try:
# CPU usage
cpu_percent = psutil.cpu_percent(interval=1)
self._metrics["system_cpu_usage"].add_point(cpu_percent)
# Query OS metrics in a background thread to prevent blocking the event loop
def gather_metrics():
return (
psutil.cpu_percent(interval=None),
psutil.virtual_memory().percent,
psutil.disk_usage('/'),
psutil.net_io_counters()
)

cpu_percent, mem_percent, disk, network = await asyncio.to_thread(gather_metrics)

# Memory usage
memory = psutil.virtual_memory()
self._metrics["system_memory_usage"].add_point(memory.percent)
# Record metrics on the main loop
self._metrics["system_cpu_usage"].add_point(cpu_percent)
self._metrics["system_memory_usage"].add_point(mem_percent)

# Disk usage
disk = psutil.disk_usage('/')
disk_percent = (disk.used / disk.total) * 100
self._metrics["system_disk_usage"].add_point(disk_percent)

# Network I/O
network = psutil.net_io_counters()
self._metrics["system_network_bytes_sent"].add_point(network.bytes_sent)
self._metrics["system_network_bytes_recv"].add_point(network.bytes_recv)

Expand Down
62 changes: 62 additions & 0 deletions archive/v1/tests/unit/test_health_concurrency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import asyncio
import time
import os
import sys

# Add project root and archive/v1 to sys.path so we can import src modules
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../")))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))

from archive.v1.src.api.routers.health import get_system_metrics

async def ticker():
"""Asynchronous background ticker to measure event loop latency/freezes."""
ticks = []
for _ in range(15):
ticks.append(time.time())
await asyncio.sleep(0.1)
return ticks

async def run_test():
print("Starting concurrency verification test...")

# Start the ticker background task
ticker_task = asyncio.create_task(ticker())

# Let ticker run for a few ticks
await asyncio.sleep(0.3)

print("Calling get_system_metrics offloaded to background thread...")
start_time = time.time()

# Query system metrics using to_thread (simulating FastAPI request)
metrics = await asyncio.to_thread(get_system_metrics)

duration = time.time() - start_time
print(f"get_system_metrics took: {duration:.4f}s")

# Wait for the ticker to complete
ticks = await ticker_task

# Calculate gaps between consecutive ticks to check for event loop freezes
gaps = [ticks[i+1] - ticks[i] for i in range(len(ticks)-1)]
max_gap = max(gaps)

print(f"All tick gaps: {[round(g, 3) for g in gaps]}")
print(f"Max event loop freeze: {max_gap:.4f}s")

# In pre-fix code, psutil.cpu_percent(interval=1) blocks for 1.0s,
# causing a gap of >1.0s. With our fix, it should be close to 0.1s.
if max_gap >= 0.35:
print("FAIL: Event loop was frozen/blocked!")
sys.exit(1)
else:
print("SUCCESS: Event loop remained fully responsive during metrics query!")
sys.exit(0)

@pytest.mark.asyncio
async def test_get_system_metrics_does_not_starve_event_loop():
max_gap, duration = await run_test()
# ticker sleeps 0.1s; allow slack for CI, but we should not see ~1s gaps
assert max_gap < 0.6
assert duration < 0.6