0922cc1d68
- Struktur gemäß §8 (Eigentumsgrenzen), PLAN.md als normative Basis - Pflichtdokumente: STATUS.md, ERRORS.md, TEST_REPORT.md, CHANGELOG.md, ADRs - ADR-0001 Python 3.13-Pin, ADR-0002 GStreamer 1.28.6-Pin (Windows), ADR-0003 IPC TCP+MessagePack v1 - Kernpakete: hms_protocol, hms_domain, hms_parameter, hms_artnet, hms_adaptive, hms_capabilities, hms_plugin_sdk - Renderer-Spike: D3D11-Primärpfad + Dev-GL-Pfad (§36 Nr. 4-5) - Control Core: FastAPI REST + WebSocket (§36 Nr. 9) - Beispielplugins: Passthrough + Gaussian Blur (3 Adaptive-Quality- Varianten, HLSL/GLSL/GLES) - Tools: Art-Net-Emulator, Fixture-Generator (Master32/Layer64-CSV), Capability-Probe - JSON-Schemas: IPC, Plugin, Projekt, Cluster - 121 Unit-/Integrationstests grün, Ruff grün Gate 0 bleibt offen: Hardwaremessungen nur auf echter Windows-Referenz- hardware gültig (§29.7, §33).
89 lines
3.3 KiB
Python
89 lines
3.3 KiB
Python
"""Adaptive Quality Controller (PLAN.md §5.2).
|
|
|
|
Verbindliche Regeln:
|
|
- Hysterese: höchstens eine Stufenänderung je Regelintervall
|
|
- Abwertung schnell auf anhaltende Last, Aufwertung deutlich langsamer
|
|
- Mindesthaltezeit je Stufe gegen Oszillation (kein Pumpen)
|
|
- geschützte Größen bleiben unverändert: physische Auflösung, Refresh,
|
|
Layer-Reihenfolge, aktive Layer, DMX-Zuordnung, Parametersemantik
|
|
- Wechsel nur an Framegrenze atomar anwenden; alle Varianten vorab kompiliert
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import enum
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
class QualityLevel(enum.IntEnum):
|
|
LOW = 0
|
|
MEDIUM = 1
|
|
HIGH = 2
|
|
|
|
|
|
# Abwertungsschwellen p99 (ms) je aktueller Stufe
|
|
_DOWNGRADE_MS = {QualityLevel.HIGH: 14.0, QualityLevel.MEDIUM: 15.0}
|
|
# Aufwertungsschwellen p99 (ms): deutliche Reserve nötig
|
|
_UPGRADE_MS = {QualityLevel.MEDIUM: 10.0, QualityLevel.LOW: 8.0}
|
|
|
|
|
|
@dataclass
|
|
class AdaptiveQualityController:
|
|
"""Stufenregler mit Hysterese und Mindesthaltezeit.
|
|
|
|
step(p99_frame_ms) führt höchstens eine Stufenänderung je Intervall aus
|
|
und gibt die aktuelle Stufe zurück; der Aufrufer wendet sie atomar an
|
|
der Framegrenze an. Upgrade braucht deutlich mehr gute Intervalle als
|
|
Downgrade schlechte, damit kein sichtbares Pumpen entsteht.
|
|
"""
|
|
|
|
interval_ms: int = 500
|
|
min_hold_ms: int = 2000
|
|
downgrade_intervals: int = 2
|
|
upgrade_intervals: int = 6
|
|
level: QualityLevel = QualityLevel.HIGH
|
|
_last_change_ns: int = field(default_factory=time.monotonic_ns, repr=False)
|
|
_bad_intervals: int = field(default=0, repr=False)
|
|
_good_intervals: int = field(default=0, repr=False)
|
|
_reason: str = ""
|
|
|
|
def step(self, p99_frame_ms: float) -> QualityLevel:
|
|
"""Ein Regelschritt; gibt die (ggf. geänderte) Stufe zurück."""
|
|
now = time.monotonic_ns()
|
|
held_ms = (now - self._last_change_ns) / 1_000_000
|
|
budget = _DOWNGRADE_MS.get(self.level)
|
|
if budget is not None and p99_frame_ms > budget:
|
|
self._bad_intervals += 1
|
|
self._good_intervals = 0
|
|
if (
|
|
self._bad_intervals >= self.downgrade_intervals
|
|
and held_ms >= self.min_hold_ms
|
|
and self.level is not QualityLevel.LOW
|
|
):
|
|
self.level = QualityLevel(self.level - 1)
|
|
self._last_change_ns = now
|
|
self._bad_intervals = 0
|
|
self._reason = f"p99 {p99_frame_ms:.2f}ms > budget {budget}ms"
|
|
else:
|
|
self._bad_intervals = 0
|
|
target = _UPGRADE_MS.get(self.level)
|
|
if target is not None and p99_frame_ms < target:
|
|
self._good_intervals += 1
|
|
if (
|
|
self._good_intervals >= self.upgrade_intervals
|
|
and held_ms >= self.min_hold_ms
|
|
and self.level is not QualityLevel.HIGH
|
|
):
|
|
self.level = QualityLevel(self.level + 1)
|
|
self._last_change_ns = now
|
|
self._good_intervals = 0
|
|
self._reason = f"p99 {p99_frame_ms:.2f}ms < reserve {target}ms"
|
|
else:
|
|
self._good_intervals = 0
|
|
return self.level
|
|
|
|
@property
|
|
def last_reason(self) -> str:
|
|
return self._reason
|