mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-02 20:13:53 +08:00
Reuse Card settings derivations within each frame
This commit is contained in:
@@ -655,7 +655,8 @@ class Car:
|
||||
manual_snapshot = self.manual_settings_owner.refresh(now_ns)
|
||||
manual_drive_id = int(self.sm['deviceState'].startedMonoTime)
|
||||
manual_verdict = self.manual_settings_owner.verdict(manual_snapshot, now_mono_ns=now_ns, drive_id=manual_drive_id)
|
||||
manual_fingerprint = (settings_fingerprint(manual_snapshot) if manual_verdict.status == 'ready' and manual_verdict.safe_mode is False and
|
||||
manual_fingerprint = (settings_fingerprint(manual_snapshot, owner=self.manual_settings_owner)
|
||||
if manual_verdict.status == 'ready' and manual_verdict.safe_mode is False and
|
||||
manual_verdict.selection is not None and manual_verdict.selection.choice in (ModeChoice.CEM, ModeChoice.CCM)
|
||||
else None)
|
||||
self.manual_button_tracker.keep_claim(manual_drive_id, manual_fingerprint)
|
||||
|
||||
@@ -51,7 +51,7 @@ def conditional_manual_candidate(CS: car.CarState, tracker: ButtonTracker, param
|
||||
if choice not in (ModeChoice.CEM, ModeChoice.CCM) or snapshot.preferences is None:
|
||||
tracker.invalidate_media()
|
||||
return None
|
||||
fingerprint = settings_fingerprint(snapshot)
|
||||
fingerprint = settings_fingerprint(snapshot, owner=settings_owner)
|
||||
if fingerprint is None:
|
||||
tracker.invalidate_media()
|
||||
return None
|
||||
@@ -113,7 +113,7 @@ def conditional_traffic_candidate(CS: car.CarState, tracker: ButtonTracker, para
|
||||
return None
|
||||
snapshot = settings_owner.refresh(now_ns)
|
||||
verdict = settings_owner.verdict(snapshot, now_mono_ns=now_ns, drive_id=drive_id)
|
||||
fingerprint = settings_fingerprint(snapshot) if verdict.status == 'ready' and verdict.safe_mode is False else None
|
||||
fingerprint = settings_fingerprint(snapshot, owner=settings_owner) if verdict.status == 'ready' and verdict.safe_mode is False else None
|
||||
if fingerprint is None:
|
||||
tracker.invalidate_media()
|
||||
return None
|
||||
|
||||
@@ -9,8 +9,9 @@ Replay continues to use the synchronous source-clock owner.
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from openpilot.starpilot.conditional_mode.preferences import CCMOptions, CEMOptions, ModeSelection, SavedPreferences
|
||||
from openpilot.starpilot.conditional_mode.runtime_settings import (
|
||||
REVERIFY_NS, ConditionalSettingsOwner, SettingsSnapshot,
|
||||
REVERIFY_NS, ConditionalSettingsOwner, DocumentState, SafeModeState, SettingsSnapshot,
|
||||
)
|
||||
|
||||
|
||||
@@ -28,10 +29,14 @@ class CardSettingsView(ConditionalSettingsOwner):
|
||||
super().__init__(None)
|
||||
self.owner_token = owner_token
|
||||
self.unavailable = self._unavailable(0)
|
||||
self._selection_cache = None
|
||||
self._fingerprint_cache = None
|
||||
|
||||
def adopt(self, observation: CardSettingsObservation) -> None:
|
||||
# Latch the whole immutable source once per frame. The worker never writes
|
||||
# this view; all consumers within that frame see the same revision.
|
||||
self._selection_cache = None
|
||||
self._fingerprint_cache = None
|
||||
snapshot = observation.snapshot
|
||||
self.current = (snapshot if observation.status == 'verified' and
|
||||
type(snapshot) is SettingsSnapshot and snapshot.owner_token == self.owner_token
|
||||
@@ -47,6 +52,58 @@ class CardSettingsView(ConditionalSettingsOwner):
|
||||
0 <= now_mono_ns - current.verified_mono_ns < REVERIFY_NS and
|
||||
super().affirm(snapshot, now_mono_ns=now_mono_ns))
|
||||
|
||||
@staticmethod
|
||||
def _preference_fields(snapshot: SettingsSnapshot):
|
||||
try:
|
||||
preferences = snapshot.preferences
|
||||
if (type(preferences) is not SavedPreferences or type(preferences.cem) is not CEMOptions or
|
||||
type(preferences.ccm) is not CCMOptions):
|
||||
return None
|
||||
cem, ccm = preferences.cem, preferences.ccm
|
||||
# Each successful derivation validated exact immutable scalars. Identity
|
||||
# checks also notice deliberate object.__setattr__ fixture mutations.
|
||||
return (preferences, cem, ccm, preferences.version, preferences.mode,
|
||||
cem.speed_mps, cem.speed_with_lead_mps, cem.signal_speed_mps,
|
||||
cem.open_road, cem.curves, cem.curves_with_lead, cem.lead, cem.slower_lead,
|
||||
cem.stopped_lead, cem.stop_lights, cem.model_stop_s, cem.signal_lane_detection,
|
||||
cem.signal_lane_width_m, cem.persist_manual, ccm.speed_mps, ccm.speed_with_lead_mps,
|
||||
ccm.set_speed_margin_mps, ccm.lead, ccm.launch_assist, ccm.persist_manual)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def _selection_for_drive(self, snapshot: SettingsSnapshot, drive_id: int) -> ModeSelection | None:
|
||||
# verdict still checks the current owner, revision, SafeMode and clock on
|
||||
# every call. Only its pure post-affirmation derivation may be reused.
|
||||
if (type(self) is not CardSettingsView or type(snapshot) is not SettingsSnapshot or
|
||||
snapshot is not self.current or type(drive_id) is not int or drive_id <= 0):
|
||||
return super()._selection_for_drive(snapshot, drive_id)
|
||||
key = self._preference_fields(snapshot)
|
||||
if key is None:
|
||||
return super()._selection_for_drive(snapshot, drive_id)
|
||||
cached = self._selection_cache
|
||||
if cached is not None and cached[0] == drive_id and all(a is b for a, b in zip(key, cached[1], strict=True)):
|
||||
return cached[2]
|
||||
selection = super()._selection_for_drive(snapshot, drive_id)
|
||||
self._selection_cache = (drive_id, key, selection)
|
||||
return selection
|
||||
|
||||
def fingerprint(self, snapshot: SettingsSnapshot | None) -> str | None:
|
||||
from openpilot.starpilot.conditional_mode.status import _settings_fingerprint
|
||||
|
||||
if (type(self) is not CardSettingsView or type(snapshot) is not SettingsSnapshot or
|
||||
snapshot is not self.current or type(snapshot.document_state) is not DocumentState or
|
||||
type(snapshot.safe_mode_state) is not SafeModeState or
|
||||
snapshot.document_raw is not None and type(snapshot.document_raw) is not bytes or
|
||||
snapshot.safe_mode_raw is not None and type(snapshot.safe_mode_raw) is not bytes):
|
||||
return _settings_fingerprint(snapshot)
|
||||
key = (snapshot.document_state, snapshot.safe_mode_state, snapshot.document_raw, snapshot.safe_mode_raw)
|
||||
cached = self._fingerprint_cache
|
||||
if cached is not None and all(a is b for a, b in zip(key, cached[0], strict=True)):
|
||||
return cached[1]
|
||||
fingerprint = _settings_fingerprint(snapshot)
|
||||
self._fingerprint_cache = (key, fingerprint)
|
||||
return fingerprint
|
||||
|
||||
|
||||
class CardSettingsWorker:
|
||||
"""One worker-owned reader and an atomically replaced immutable publication."""
|
||||
|
||||
@@ -218,7 +218,11 @@ class ConditionalSettingsOwner:
|
||||
current.observed_mono_ns if current is not None else None,
|
||||
None, None, None, safe)
|
||||
assert snapshot is not None and snapshot.preferences is not None
|
||||
selection = saved_selection_for_drive(snapshot.preferences, drive_id)
|
||||
selection = self._selection_for_drive(snapshot, drive_id)
|
||||
return SettingsVerdict('ready' if selection is not None else 'invalid_drive', snapshot.revision,
|
||||
snapshot.observed_mono_ns, selection, snapshot.preferences.cem,
|
||||
snapshot.preferences.ccm, False)
|
||||
|
||||
def _selection_for_drive(self, snapshot: SettingsSnapshot, drive_id: int) -> ModeSelection | None:
|
||||
assert snapshot.preferences is not None
|
||||
return saved_selection_for_drive(snapshot.preferences, drive_id)
|
||||
|
||||
@@ -22,7 +22,16 @@ CHOICES = {ModeChoice.STOCK: 'stock', ModeChoice.CEM: 'conditionalExperimental',
|
||||
_HEX = re.compile(r'[0-9a-f]{64}\Z')
|
||||
|
||||
|
||||
def settings_fingerprint(snapshot: SettingsSnapshot | None) -> str | None:
|
||||
def settings_fingerprint(snapshot: SettingsSnapshot | None, *, owner=None) -> str | None:
|
||||
if owner is not None:
|
||||
from openpilot.starpilot.conditional_mode.card_settings import CardSettingsView
|
||||
|
||||
if type(owner) is CardSettingsView:
|
||||
return owner.fingerprint(snapshot)
|
||||
return _settings_fingerprint(snapshot)
|
||||
|
||||
|
||||
def _settings_fingerprint(snapshot: SettingsSnapshot | None) -> str | None:
|
||||
"""Bind two independent readers to the exact saved document and SafeMode."""
|
||||
if (
|
||||
snapshot is None
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
"""Frame-local pure derivations; live settings/authority checks still run."""
|
||||
from dataclasses import fields, replace
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.starpilot.conditional_mode import runtime_settings, status
|
||||
from openpilot.starpilot.conditional_mode.card_settings import CardSettingsObservation, CardSettingsView, CardSettingsWorker
|
||||
from openpilot.starpilot.conditional_mode.policy import ModeChoice
|
||||
from openpilot.starpilot.conditional_mode.preferences import CCMOptions, CEMOptions, SavedPreferences
|
||||
from openpilot.starpilot.conditional_mode.runtime_settings import ConditionalSettingsOwner, DocumentState, REVERIFY_NS, SafeModeState
|
||||
|
||||
NOW = 10_000_000_000
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def worker(tmp_path):
|
||||
result = CardSettingsWorker(Params(str(tmp_path)))
|
||||
result.refresh(NOW)
|
||||
result.adopt()
|
||||
return result
|
||||
|
||||
|
||||
class UncachedCardSettingsView(CardSettingsView):
|
||||
# The live Card's 500ms source-age bound must remain in the reference too;
|
||||
# a generic owner has a different one-second bound. Only derivation is uncached.
|
||||
_selection_for_drive = ConditionalSettingsOwner._selection_for_drive
|
||||
|
||||
|
||||
def reference(view):
|
||||
owner = UncachedCardSettingsView(view.owner_token)
|
||||
owner.current = view.current
|
||||
return owner
|
||||
|
||||
|
||||
def outcome(owner, snapshot, now=NOW, drive=1):
|
||||
try:
|
||||
return owner.verdict(snapshot, now_mono_ns=now, drive_id=drive)
|
||||
except Exception as error:
|
||||
return type(error), str(error)
|
||||
|
||||
|
||||
def test_reuses_only_pure_derivations_and_affirms_every_call(worker):
|
||||
view, snapshot = worker.view, worker.view.current
|
||||
expected = reference(view).verdict(snapshot, now_mono_ns=NOW, drive_id=1)
|
||||
with patch.object(runtime_settings, 'saved_selection_for_drive', wraps=runtime_settings.saved_selection_for_drive) as selection, \
|
||||
patch.object(status, '_settings_fingerprint', wraps=status._settings_fingerprint) as fingerprint, \
|
||||
patch.object(view, 'affirm', wraps=view.affirm) as affirm, \
|
||||
patch.object(runtime_settings, 'read_mode_sources', side_effect=AssertionError('main-thread I/O')):
|
||||
for _ in range(4):
|
||||
assert view.refresh(NOW) is snapshot
|
||||
assert view.verdict(snapshot, now_mono_ns=NOW, drive_id=1) == expected
|
||||
assert status.settings_fingerprint(snapshot, owner=view) == status.settings_fingerprint(snapshot)
|
||||
assert selection.call_count == 1 and affirm.call_count == 4
|
||||
assert fingerprint.call_count == 5 # One cached miss plus four uncached references.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('now', [0, NOW - 1, NOW + REVERIFY_NS - 1, NOW + REVERIFY_NS, NOW + 1_000_000_001, True, 1.0])
|
||||
def test_warm_cache_does_not_bypass_clock_and_stale_gates(worker, now):
|
||||
view, snapshot = worker.view, worker.view.current
|
||||
view.verdict(snapshot, now_mono_ns=NOW, drive_id=1)
|
||||
assert outcome(view, snapshot, now) == outcome(reference(view), snapshot, now)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('drive', [2, 0, -1, True, 1.0, '1', None])
|
||||
def test_warm_cache_does_not_reuse_another_drive_or_accept_invalid_drive(worker, drive):
|
||||
view, snapshot = worker.view, worker.view.current
|
||||
view.verdict(snapshot, now_mono_ns=NOW, drive_id=1)
|
||||
assert outcome(view, snapshot, drive=drive) == outcome(reference(view), snapshot, drive=drive)
|
||||
|
||||
|
||||
def test_adoption_clears_both_caches_even_for_the_same_snapshot(worker):
|
||||
view, snapshot = worker.view, worker.view.current
|
||||
with patch.object(runtime_settings, 'saved_selection_for_drive', wraps=runtime_settings.saved_selection_for_drive) as selection, \
|
||||
patch.object(status, '_settings_fingerprint', wraps=status._settings_fingerprint) as fingerprint:
|
||||
for _ in range(2):
|
||||
worker.adopt()
|
||||
view.verdict(snapshot, now_mono_ns=NOW, drive_id=1)
|
||||
status.settings_fingerprint(snapshot, owner=view)
|
||||
assert selection.call_count == fingerprint.call_count == 2
|
||||
|
||||
|
||||
PREFERENCE_FIELDS = [('preferences', field.name) for field in fields(SavedPreferences)] + \
|
||||
[('cem', field.name) for field in fields(CEMOptions)] + [('ccm', field.name) for field in fields(CCMOptions)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('section,name', PREFERENCE_FIELDS)
|
||||
def test_frozen_input_fixture_mutation_cannot_hide_behind_identity(worker, section, name):
|
||||
view, snapshot = worker.view, worker.view.current
|
||||
view.verdict(snapshot, now_mono_ns=NOW, drive_id=1)
|
||||
preferences = snapshot.preferences
|
||||
target = preferences if section == 'preferences' else getattr(preferences, section)
|
||||
previous = getattr(target, name)
|
||||
if type(previous) is bool:
|
||||
changed = not previous
|
||||
elif type(previous) in (int, float):
|
||||
changed = previous + 0.125
|
||||
elif type(previous) is ModeChoice:
|
||||
changed = ModeChoice.CCM if previous is not ModeChoice.CCM else ModeChoice.CEM
|
||||
elif type(previous) is CEMOptions:
|
||||
changed = replace(previous, curves=not previous.curves)
|
||||
elif type(previous) is CCMOptions:
|
||||
changed = replace(previous, lead=not previous.lead)
|
||||
else:
|
||||
raise AssertionError(type(previous))
|
||||
try:
|
||||
object.__setattr__(target, name, changed)
|
||||
assert outcome(view, snapshot) == outcome(reference(view), snapshot)
|
||||
finally:
|
||||
object.__setattr__(target, name, previous)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('preferences', [SimpleNamespace(), replace(SavedPreferences(), cem=SimpleNamespace()),
|
||||
replace(SavedPreferences(), ccm=SimpleNamespace()),
|
||||
replace(SavedPreferences(), version='invalid')])
|
||||
def test_custom_or_malformed_preferences_follow_original_failure(worker, preferences):
|
||||
view, snapshot = worker.view, worker.view.current
|
||||
view.verdict(snapshot, now_mono_ns=NOW, drive_id=1)
|
||||
changed = replace(snapshot, preferences=preferences)
|
||||
view.adopt(CardSettingsObservation(changed, 'verified', NOW))
|
||||
assert outcome(view, changed) == outcome(reference(view), changed)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value', [('document_raw', b'{}'), ('safe_mode_raw', b'0'),
|
||||
('document_state', DocumentState.INVALID), ('safe_mode_state', SafeModeState.TRUE),
|
||||
('safe_mode_raw', bytearray(b'0'))])
|
||||
def test_fingerprint_reads_current_values_and_preserves_fallback(worker, field, value):
|
||||
view, snapshot = worker.view, worker.view.current
|
||||
status.settings_fingerprint(snapshot, owner=view)
|
||||
previous = getattr(snapshot, field)
|
||||
try:
|
||||
object.__setattr__(snapshot, field, value)
|
||||
assert status.settings_fingerprint(snapshot, owner=view) == status.settings_fingerprint(snapshot)
|
||||
finally:
|
||||
object.__setattr__(snapshot, field, previous)
|
||||
|
||||
|
||||
def test_foreign_snapshots_and_custom_view_subclasses_do_not_use_cache(worker):
|
||||
view, snapshot = worker.view, worker.view.current
|
||||
view.verdict(snapshot, now_mono_ns=NOW, drive_id=1)
|
||||
foreign = replace(snapshot, owner_token='foreign')
|
||||
assert outcome(view, foreign) == outcome(reference(view), foreign)
|
||||
|
||||
class CustomView(CardSettingsView):
|
||||
pass
|
||||
|
||||
custom = CustomView(view.owner_token)
|
||||
custom.adopt(worker.published)
|
||||
generic = reference(custom)
|
||||
with patch.object(runtime_settings, 'saved_selection_for_drive', wraps=runtime_settings.saved_selection_for_drive) as selection:
|
||||
for _ in range(2):
|
||||
assert custom.verdict(snapshot, now_mono_ns=NOW, drive_id=1) == generic.verdict(snapshot, now_mono_ns=NOW, drive_id=1)
|
||||
assert selection.call_count == 4
|
||||
assert custom._selection_cache is None and custom._fingerprint_cache is None
|
||||
assert status.settings_fingerprint(snapshot, owner=object()) == status.settings_fingerprint(snapshot)
|
||||
|
||||
|
||||
def test_deleted_nested_fixture_field_preserves_original_error_order(worker):
|
||||
view, snapshot = worker.view, worker.view.current
|
||||
view.verdict(snapshot, now_mono_ns=NOW, drive_id=1)
|
||||
preferences = snapshot.preferences
|
||||
previous = preferences.cem
|
||||
try:
|
||||
object.__setattr__(preferences, 'version', 'invalid')
|
||||
object.__delattr__(preferences, 'cem')
|
||||
assert outcome(view, snapshot) == outcome(reference(view), snapshot)
|
||||
finally:
|
||||
object.__setattr__(preferences, 'version', 1)
|
||||
object.__setattr__(preferences, 'cem', previous)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value', [('safe_mode_state', SafeModeState.TRUE),
|
||||
('safe_mode_state', SafeModeState.INVALID),
|
||||
('document_state', DocumentState.READ_ERROR),
|
||||
('owner_token', 'foreign')])
|
||||
def test_current_source_gate_changes_after_warm_cache_still_deny(worker, field, value):
|
||||
view, snapshot = worker.view, worker.view.current
|
||||
view.verdict(snapshot, now_mono_ns=NOW, drive_id=1)
|
||||
changed = replace(snapshot, **{field: value})
|
||||
# Deliberately replace the live source without adopt to verify the cache is
|
||||
# subordinate to current owner/source gates, not just adoption bookkeeping.
|
||||
view.current = changed
|
||||
assert outcome(view, changed) == outcome(reference(view), changed)
|
||||
assert outcome(view, snapshot) == outcome(reference(view), snapshot)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('full_op', [False, True])
|
||||
def test_actual_card_adoption_and_multiple_consumers_keep_live_authority(full_op):
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.car import card as module
|
||||
from openpilot.starpilot.aol.tests.test_ioniq_availability import IoniqAvailabilityTests
|
||||
from openpilot.starpilot.conditional_mode.manual import ButtonTracker, IoniqMediaMapCache
|
||||
|
||||
fixture = IoniqAvailabilityTests()
|
||||
try:
|
||||
with OpenpilotPrefix():
|
||||
sd, _, drive, card, receive = fixture._ioniq_availability_callers(full_op=full_op)
|
||||
card.conditional_replay = True
|
||||
card.manual_button_tracker = ButtonTracker()
|
||||
card.traffic_button_tracker = ButtonTracker()
|
||||
card.ioniq_media_map = IoniqMediaMapCache()
|
||||
card.manual_settings_owner = ConditionalSettingsOwner(card.params)
|
||||
with patch.object(module, 'REPLAY', False):
|
||||
card.configure_conditional_settings_worker()
|
||||
worker = card.conditional_settings_worker
|
||||
for tick in (2, 3):
|
||||
now = NOW + tick * 10_000_000
|
||||
worker.refresh(now)
|
||||
axis, _, command = drive(tick)
|
||||
with patch.object(runtime_settings, 'saved_selection_for_drive', wraps=runtime_settings.saved_selection_for_drive) as selection, \
|
||||
patch.object(worker.view, 'affirm', wraps=worker.view.affirm) as affirm:
|
||||
assert receive(now, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
|
||||
assert selection.call_count == 1
|
||||
assert affirm.call_count >= (2 if full_op else 1)
|
||||
assert axis.lateralActive and command.latActive
|
||||
assert bool(axis.longitudinalActive) is full_op and bool(command.longActive) is full_op
|
||||
# A fresh restrictive worker publication must revoke optional proposals.
|
||||
Path(card.params.get_param_path('ConditionalModeConfig')).write_bytes(b'{')
|
||||
now = NOW + 40_000_000
|
||||
worker.refresh(now)
|
||||
axis, _, command = drive(4)
|
||||
assert receive(now, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
|
||||
assert worker.view.verdict(worker.view.current, now_mono_ns=now, drive_id=NOW - 1_000_000_000).status == 'unavailable_document'
|
||||
assert card.manual_receipt is None and card.traffic_receipt is None and card.switchback_receipt is None
|
||||
assert axis.lateralActive and command.latActive
|
||||
assert bool(axis.longitudinalActive) is full_op and bool(command.longActive) is full_op
|
||||
finally:
|
||||
fixture.doCleanups()
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "61df7f15b13c2f911a49ade67d1cd5181114a36e04f1487d1a250d58d44aa545", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "1993fd85c920482bc76ed2ba706a08eb04d8975510f27747752783898d2dfb79", "schema_version": 1}
|
||||
|
||||
Reference in New Issue
Block a user