feat: implement logging setup and CSV telemetry logging for performance metrics
This commit is contained in:
104
app/telemetry/csv_logger.py
Normal file
104
app/telemetry/csv_logger.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""CsvTelemetryLogger — writes telemetry snapshots to a CSV file with throttling.
|
||||
|
||||
Design decisions:
|
||||
- Does NOT use the logging module — writes directly via csv.writer so the file
|
||||
is readable independently of the text log level.
|
||||
- Flushes after every row so the file is intact even on crash or force-quit.
|
||||
- Throttle: only one row per TELEMETRY_CSV_INTERVAL_S seconds, even if
|
||||
metrics_updated fires every 500 ms. This keeps the file manageable for
|
||||
long sessions (8 h @ 5 s interval = 5 760 rows).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from app.config import TELEMETRY_CSV_INTERVAL_S
|
||||
from app.telemetry.telemetry_collector import TelemetrySnapshot
|
||||
|
||||
_CSV_HEADER = [
|
||||
"timestamp",
|
||||
"fps_got",
|
||||
"fps_req",
|
||||
"frame_time_ms",
|
||||
"dropped_frames",
|
||||
"cpu_sys_pct",
|
||||
"cpu_core_pct",
|
||||
"mem_mb",
|
||||
]
|
||||
|
||||
|
||||
class CsvTelemetryLogger:
|
||||
"""
|
||||
Receives TelemetrySnapshot objects and writes throttled rows to a CSV file.
|
||||
|
||||
Usage:
|
||||
logger = CsvTelemetryLogger(path)
|
||||
telemetry_collector.metrics_updated.connect(logger.on_metrics_updated)
|
||||
# call logger.close() on application exit
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: Path,
|
||||
interval_s: float = TELEMETRY_CSV_INTERVAL_S,
|
||||
) -> None:
|
||||
self._interval_s = interval_s
|
||||
self._last_write_time: float = 0.0
|
||||
self._rows_written: int = 0
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._file = path.open("w", newline="", encoding="utf-8")
|
||||
self._writer = csv.writer(self._file)
|
||||
self._writer.writerow(_CSV_HEADER)
|
||||
self._file.flush()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Slot — connect to TelemetryCollector.metrics_updated
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def on_metrics_updated(self, snapshot: TelemetrySnapshot) -> None:
|
||||
"""Write a row if the throttle interval has elapsed."""
|
||||
now = time.monotonic()
|
||||
if now - self._last_write_time < self._interval_s:
|
||||
return
|
||||
self._last_write_time = now
|
||||
self._write_row(snapshot)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def close(self) -> None:
|
||||
"""Flush and close the CSV file. Call on application shutdown."""
|
||||
try:
|
||||
self._file.flush()
|
||||
self._file.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def rows_written(self) -> int:
|
||||
return self._rows_written
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Private
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _write_row(self, snap: TelemetrySnapshot) -> None:
|
||||
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3] # HH:MM:SS.mmm
|
||||
self._writer.writerow([
|
||||
ts,
|
||||
f"{snap.fps:.1f}",
|
||||
f"{snap.target_fps:.1f}" if snap.target_fps is not None else "",
|
||||
f"{snap.frame_time_ms:.2f}",
|
||||
snap.dropped_frames,
|
||||
f"{snap.cpu_percent_sys:.1f}",
|
||||
f"{snap.cpu_percent_core:.1f}",
|
||||
f"{snap.memory_mb:.1f}" if snap.memory_mb is not None else "",
|
||||
])
|
||||
self._file.flush()
|
||||
self._rows_written += 1
|
||||
Reference in New Issue
Block a user