mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-04 21:33:58 +08:00
200 lines
7.0 KiB
Python
200 lines
7.0 KiB
Python
"""Small, shared cache for read-mostly UI parameters.
|
|
|
|
Parameter reads are file-backed. The raylib UIs ask for the same values from
|
|
multiple widgets during a frame, so a short cache avoids repeated open/read/
|
|
close cycles. Writes invalidate immediately; opt-in background refresh keeps
|
|
expired reads off the render thread while retaining the latest cached value.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import queue
|
|
import threading
|
|
import time
|
|
from collections.abc import Callable
|
|
from typing import Any, NamedTuple
|
|
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.swaglog import cloudlog
|
|
from openpilot.system.hardware import PC
|
|
|
|
|
|
class _RefreshRequest(NamedTuple):
|
|
cache_key: tuple[Any, ...]
|
|
cached: tuple[float, Any]
|
|
args: tuple[Any, ...]
|
|
kwargs: dict[str, Any]
|
|
|
|
|
|
class UIParamCache:
|
|
def __init__(self, params: Params | Any | None = None, ttl: float = 0.1,
|
|
clock: Callable[[], float] = time.monotonic):
|
|
self._params = params if params is not None else Params()
|
|
self._ttl = max(0.0, ttl)
|
|
self._clock = clock
|
|
self._cache: dict[tuple[Any, ...], tuple[float, Any]] = {}
|
|
self._lock = threading.Lock()
|
|
self._worker: threading.Thread | None = None
|
|
self._refresh_queue: queue.Queue[_RefreshRequest | None] = queue.Queue()
|
|
self._pending: set[tuple[Any, ...]] = set()
|
|
self._stop_event = threading.Event()
|
|
|
|
def start(self) -> None:
|
|
with self._lock:
|
|
if self._worker is not None:
|
|
return
|
|
self._refresh_queue = queue.Queue()
|
|
self._pending = set()
|
|
self._stop_event = threading.Event()
|
|
self._worker = threading.Thread(target=self._refresh_worker, name="ui-param-cache", daemon=True,
|
|
args=(self._refresh_queue, self._pending, self._stop_event))
|
|
self._worker.start()
|
|
|
|
def stop(self, timeout: float = 1.0) -> None:
|
|
with self._lock:
|
|
worker = self._worker
|
|
if worker is None:
|
|
return
|
|
self._worker = None
|
|
self._stop_event.set()
|
|
self._refresh_queue.put(None)
|
|
worker.join(timeout)
|
|
|
|
def _refresh_worker(self, requests: queue.Queue[_RefreshRequest | None], pending: set[tuple[Any, ...]],
|
|
stop_event: threading.Event) -> None:
|
|
if not PC:
|
|
try:
|
|
os.sched_setscheduler(0, os.SCHED_OTHER, os.sched_param(0))
|
|
except OSError:
|
|
cloudlog.exception("Unable to lower UI parameter worker priority")
|
|
with self._lock:
|
|
if self._stop_event is stop_event:
|
|
self._worker = None
|
|
return
|
|
|
|
while True:
|
|
request = requests.get()
|
|
try:
|
|
if request is None:
|
|
return
|
|
with self._lock:
|
|
current = not stop_event.is_set() and self._cache.get(request.cache_key) is request.cached
|
|
if not current:
|
|
continue
|
|
|
|
method, key = request.cache_key[:2]
|
|
try:
|
|
value = getattr(self._params, method)(key, *request.args, **request.kwargs)
|
|
except Exception:
|
|
value = request.cached[1]
|
|
refreshed_at = self._clock()
|
|
with self._lock:
|
|
if not stop_event.is_set() and self._cache.get(request.cache_key) is request.cached:
|
|
self._cache[request.cache_key] = (refreshed_at, value)
|
|
finally:
|
|
if request is not None:
|
|
with self._lock:
|
|
pending.discard(request.cache_key)
|
|
requests.task_done()
|
|
|
|
def _queue_refresh(self, cache_key: tuple[Any, ...], cached: tuple[float, Any],
|
|
args: tuple[Any, ...], kwargs: dict[str, Any]) -> None:
|
|
if cache_key in self._pending:
|
|
return
|
|
with self._lock:
|
|
if self._worker is not None and cache_key not in self._pending and self._cache.get(cache_key) is cached:
|
|
self._pending.add(cache_key)
|
|
self._refresh_queue.put(_RefreshRequest(cache_key, cached, args, kwargs))
|
|
|
|
@staticmethod
|
|
def _cache_key(method: str, key: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> tuple[Any, ...]:
|
|
if not args and not kwargs:
|
|
return method, key
|
|
# Params arguments are primitive values in UI call sites. repr keeps this
|
|
# robust for an occasional list/dict default without requiring hashability.
|
|
return (method, key, repr(args), repr(sorted(kwargs.items())))
|
|
|
|
def _read(self, method: str, key: str, *args: Any, **kwargs: Any) -> Any:
|
|
cache_key = self._cache_key(method, key, args, kwargs)
|
|
now = self._clock()
|
|
cached = self._cache.get(cache_key)
|
|
if cached is not None and now - cached[0] < self._ttl:
|
|
return cached[1]
|
|
|
|
blocking = kwargs.get("block", args[0] if args else False)
|
|
if cached is not None and self._worker is not None and self._ttl > 0 and not blocking:
|
|
self._queue_refresh(cache_key, cached, args, kwargs)
|
|
return cached[1]
|
|
|
|
value = getattr(self._params, method)(key, *args, **kwargs)
|
|
with self._lock:
|
|
self._cache[cache_key] = (now, value)
|
|
return value
|
|
|
|
def get(self, key: str, *args: Any, **kwargs: Any) -> Any:
|
|
return self._read("get", key, *args, **kwargs)
|
|
|
|
def get_bool(self, key: str, *args: Any, **kwargs: Any) -> bool:
|
|
return self._read("get_bool", key, *args, **kwargs)
|
|
|
|
def get_int(self, key: str, *args: Any, **kwargs: Any) -> int:
|
|
return self._read("get_int", key, *args, **kwargs)
|
|
|
|
def get_float(self, key: str, *args: Any, **kwargs: Any) -> float:
|
|
return self._read("get_float", key, *args, **kwargs)
|
|
|
|
def invalidate(self, key: str | None = None) -> None:
|
|
with self._lock:
|
|
if key is None:
|
|
self._cache.clear()
|
|
return
|
|
self._cache = {cache_key: value for cache_key, value in self._cache.items()
|
|
if cache_key[1] != key}
|
|
|
|
def put(self, key: str, value: Any, *args: Any, **kwargs: Any) -> None:
|
|
self._params.put(key, value, *args, **kwargs)
|
|
self.invalidate(key)
|
|
|
|
def put_bool(self, key: str, value: bool, *args: Any, **kwargs: Any) -> None:
|
|
self._params.put_bool(key, value, *args, **kwargs)
|
|
self.invalidate(key)
|
|
|
|
def put_int(self, key: str, value: int, *args: Any, **kwargs: Any) -> None:
|
|
self._params.put_int(key, value, *args, **kwargs)
|
|
self.invalidate(key)
|
|
|
|
def put_float(self, key: str, value: float, *args: Any, **kwargs: Any) -> None:
|
|
self._params.put_float(key, value, *args, **kwargs)
|
|
self.invalidate(key)
|
|
|
|
def put_nonblocking(self, key: str, value: Any) -> None:
|
|
self._params.put_nonblocking(key, value)
|
|
self.invalidate(key)
|
|
|
|
def put_bool_nonblocking(self, key: str, value: bool) -> None:
|
|
self._params.put_bool_nonblocking(key, value)
|
|
self.invalidate(key)
|
|
|
|
def remove(self, key: str) -> None:
|
|
self._params.remove(key)
|
|
self.invalidate(key)
|
|
|
|
def clear_all(self, *args: Any, **kwargs: Any) -> None:
|
|
self._params.clear_all(*args, **kwargs)
|
|
self.invalidate()
|
|
|
|
def __getattr__(self, name: str) -> Any:
|
|
return getattr(self._params, name)
|
|
|
|
|
|
_SHARED_UI_PARAMS: UIParamCache | None = None
|
|
|
|
|
|
def shared_ui_params() -> UIParamCache:
|
|
"""Return the cache shared by raylib UI views and settings panels."""
|
|
global _SHARED_UI_PARAMS
|
|
if _SHARED_UI_PARAMS is None:
|
|
_SHARED_UI_PARAMS = UIParamCache()
|
|
return _SHARED_UI_PARAMS
|