mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-29 17:43:47 +08:00
Reduce repeated startup configuration serialization
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
from capnp.lib.capnp import _DynamicStructBuilder
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
import threading
|
||||
import time
|
||||
|
||||
from opendbc.car import Bus, make_tester_present_msg
|
||||
from opendbc.car.can_definitions import CanRecvCallable, CanSendCallable
|
||||
from opendbc.can.parser import MAX_BAD_COUNTER
|
||||
from opendbc.car.hyundai.ioniq5pe_long_profile import qualified
|
||||
from opendbc.car.hyundai.ccnc_ev_stock import qualified as stock_qualified
|
||||
@@ -39,12 +41,40 @@ class Lease:
|
||||
return self.phase is Phase.OWNED
|
||||
|
||||
|
||||
def _plain_configuration(value):
|
||||
pending = [(value, 0)]
|
||||
while pending:
|
||||
container, depth = pending.pop()
|
||||
kind = type(container)
|
||||
if kind is dict:
|
||||
for key in container:
|
||||
if type(key) is not str:
|
||||
return False
|
||||
children = container.values()
|
||||
elif kind is list:
|
||||
children = container
|
||||
else:
|
||||
return False
|
||||
for child in children:
|
||||
kind = type(child)
|
||||
if kind is int or kind is float or kind is bool or kind is str or kind is bytes or child is None:
|
||||
continue
|
||||
if (kind is not dict and kind is not list) or depth >= 15:
|
||||
return False
|
||||
pending.append((child, depth + 1))
|
||||
return True
|
||||
|
||||
|
||||
def _configuration_bytes(cp):
|
||||
return cp.as_reader().as_builder().to_bytes()
|
||||
|
||||
|
||||
class PEPendingStartup:
|
||||
@staticmethod
|
||||
def profile_qualified(cp):
|
||||
return qualified(cp)
|
||||
|
||||
def __init__(self, cp, callbacks, *, stock_cp, clock=time.monotonic,
|
||||
def __init__(self, cp, callbacks: tuple[CanRecvCallable, CanSendCallable], *, stock_cp, clock=time.monotonic,
|
||||
boot_clock=clock_boot_ns,
|
||||
exchange=query_ioniq6_adas, event_factory=threading.Event, thread_factory=threading.Thread):
|
||||
if not self.profile_qualified(cp) or not stock_qualified(stock_cp) or stock_cp.carFingerprint != cp.carFingerprint:
|
||||
@@ -107,9 +137,27 @@ class PEPendingStartup:
|
||||
return self.owner.cp
|
||||
|
||||
def prepared_for(self, cp):
|
||||
return (not self.closed and self.fault is None and self.snapshot is not None and
|
||||
self.owner.phase in (Phase.PENDING, Phase.OWNED, Phase.STOCK) and
|
||||
self.owner.cp.to_dict() == self.snapshot == cp.to_dict())
|
||||
if (self.closed or self.fault is not None or self.snapshot is None or
|
||||
self.owner.phase not in (Phase.PENDING, Phase.OWNED, Phase.STOCK)):
|
||||
return False
|
||||
previous = getattr(self, '_prepared_cp_comparison', None)
|
||||
try:
|
||||
if (previous is not None and previous[0] is self.owner.cp and previous[1] is cp and
|
||||
_plain_configuration(self.snapshot) and self.snapshot == previous[2] and
|
||||
_configuration_bytes(self.owner.cp) == previous[3] and _configuration_bytes(cp) == previous[4]):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
matches = self.owner.cp.to_dict() == self.snapshot == cp.to_dict()
|
||||
self._prepared_cp_comparison = None
|
||||
if (matches and type(self.owner.cp) is _DynamicStructBuilder and type(cp) is _DynamicStructBuilder and
|
||||
_plain_configuration(self.snapshot)):
|
||||
try:
|
||||
self._prepared_cp_comparison = (self.owner.cp, cp, deepcopy(self.snapshot),
|
||||
_configuration_bytes(self.owner.cp), _configuration_bytes(cp))
|
||||
except Exception:
|
||||
pass
|
||||
return matches
|
||||
|
||||
def configure(self, ci):
|
||||
if self.owner.published or not self.prepared_for(ci.CP):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from contextlib import contextmanager
|
||||
from copy import deepcopy
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
@@ -9,7 +10,7 @@ from opendbc.car import Bus, CanData, structs
|
||||
from opendbc.car.hyundai.interface import CarInterface
|
||||
from opendbc.car.hyundai.ev9_longitudinal import candidate, qualified
|
||||
from opendbc.car.hyundai.ev9_pending_startup import EV9PendingStartup as PEPendingStartup
|
||||
from opendbc.car.hyundai.ioniq5pe_startup import Phase
|
||||
from opendbc.car.hyundai.ioniq5pe_startup import Lease, Phase
|
||||
from opendbc.car.hyundai.ioniq5pe_ready import PEReadySource
|
||||
from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum
|
||||
from opendbc.car.hyundai.tests.test_ioniq5pe_stock import params as stock_params
|
||||
@@ -590,7 +591,7 @@ class TestEV9PendingIdentity(unittest.TestCase):
|
||||
stock = stock_params()
|
||||
long = pe_candidate(stock, enabled=True, is_release=False)
|
||||
with self.assertRaises(ValueError):
|
||||
OriginalPEPendingStartup(long, (lambda **kw: [], lambda frames: None), stock_cp=params())
|
||||
OriginalPEPendingStartup(long, (lambda wait_for_one=False, **kw: [], lambda frames: None), stock_cp=params())
|
||||
|
||||
|
||||
class TestEV9UnpreparedController(unittest.TestCase):
|
||||
@@ -607,3 +608,120 @@ class TestEV9UnpreparedController(unittest.TestCase):
|
||||
control.actuators.accel = 1.
|
||||
_, frames = ci.apply(control.as_reader(), 130_000_000)
|
||||
self.assertEqual(frames, [])
|
||||
|
||||
|
||||
class TestPreparedConfigurationComparison(unittest.TestCase):
|
||||
@staticmethod
|
||||
def context(car=CAR.KIA_EV9):
|
||||
cp = stock_params(candidate=car)
|
||||
owner = PEPendingStartup.__new__(PEPendingStartup)
|
||||
owner.owner = Lease(cp.copy())
|
||||
owner.owner.phase = Phase.OWNED
|
||||
owner.closed = False
|
||||
owner.fault = None
|
||||
owner.snapshot = deepcopy(cp.to_dict())
|
||||
assert owner.prepared_for(cp)
|
||||
return owner, cp
|
||||
|
||||
@staticmethod
|
||||
def leaf_paths(value, path=()):
|
||||
if type(value) is dict:
|
||||
for key, child in value.items():
|
||||
yield from TestPreparedConfigurationComparison.leaf_paths(child, path + (key,))
|
||||
elif type(value) is list:
|
||||
for key, child in enumerate(value):
|
||||
yield from TestPreparedConfigurationComparison.leaf_paths(child, path + (key,))
|
||||
else:
|
||||
yield path, value
|
||||
|
||||
@staticmethod
|
||||
def assign(cp, path, value):
|
||||
for key in path[:-1]:
|
||||
cp = getattr(cp, key) if type(key) is str else cp[key]
|
||||
if type(path[-1]) is str:
|
||||
setattr(cp, path[-1], value)
|
||||
else:
|
||||
cp[path[-1]] = value
|
||||
|
||||
def test_every_legal_configuration_mutation_remains_freshly_rejected(self):
|
||||
checked = 0
|
||||
for car in (CAR.KIA_EV9, CAR.HYUNDAI_IONIQ_5_PE):
|
||||
for path, value in self.leaf_paths(stock_params(candidate=car).to_dict()):
|
||||
if type(value) is bool:
|
||||
changed = not value
|
||||
elif type(value) is int:
|
||||
changed = value ^ 1
|
||||
elif type(value) is float:
|
||||
changed = value + .125
|
||||
elif type(value) is str:
|
||||
changed = value + 'changed'
|
||||
elif type(value) is bytes:
|
||||
changed = value + b'changed'
|
||||
else:
|
||||
continue
|
||||
for target in ('owner', 'caller', 'snapshot'):
|
||||
owner, cp = self.context(car)
|
||||
if target == 'snapshot':
|
||||
container = owner.snapshot
|
||||
for key in path[:-1]:
|
||||
container = container[key]
|
||||
container[path[-1]] = changed
|
||||
else:
|
||||
try:
|
||||
self.assign(owner.owner.cp if target == 'owner' else cp, path, changed)
|
||||
except (ValueError, TypeError, AttributeError, RuntimeError):
|
||||
# Invalid enum/union assignments never become a configuration.
|
||||
continue
|
||||
self.assertFalse(owner.prepared_for(cp), (car, path, target))
|
||||
checked += 1
|
||||
self.assertGreater(checked, 300)
|
||||
|
||||
def test_owner_fault_and_phase_are_never_cached(self):
|
||||
for phase in Phase:
|
||||
owner, cp = self.context()
|
||||
owner.owner.phase = phase
|
||||
self.assertEqual(owner.prepared_for(cp), phase in (Phase.PENDING, Phase.OWNED, Phase.STOCK))
|
||||
for name, value in (('closed', True), ('fault', 'current fault'), ('snapshot', None)):
|
||||
owner, cp = self.context()
|
||||
setattr(owner, name, value)
|
||||
self.assertFalse(owner.prepared_for(cp))
|
||||
|
||||
def test_equivalent_replacements_arena_changes_and_reader_fallback(self):
|
||||
owner, cp = self.context()
|
||||
original = cp.carFingerprint
|
||||
cp.carFingerprint += 'temporary'
|
||||
cp.carFingerprint = original
|
||||
self.assertTrue(owner.prepared_for(cp))
|
||||
cp = cp.copy()
|
||||
self.assertTrue(owner.prepared_for(cp))
|
||||
owner.owner.cp = owner.owner.cp.copy()
|
||||
self.assertTrue(owner.prepared_for(cp))
|
||||
self.assertTrue(owner.prepared_for(cp.as_reader()))
|
||||
cp.mass = owner.owner.cp.mass = 0.
|
||||
owner.snapshot['mass'] = -0.
|
||||
self.assertTrue(owner.prepared_for(cp))
|
||||
cp.mass = owner.owner.cp.mass = float('nan')
|
||||
owner.snapshot['mass'] = float('nan')
|
||||
self.assertFalse(owner.prepared_for(cp))
|
||||
|
||||
def test_custom_snapshot_callbacks_keep_original_semantics(self):
|
||||
trace = []
|
||||
|
||||
class Meta(type):
|
||||
def __eq__(self, other):
|
||||
raise AssertionError('Metadata validation must not invoke metaclass equality')
|
||||
|
||||
def __hash__(self):
|
||||
raise AssertionError('Metadata validation must not invoke metaclass hashing')
|
||||
|
||||
class EqualValue(metaclass=Meta):
|
||||
def __eq__(self, other):
|
||||
trace.append(other)
|
||||
return True
|
||||
|
||||
owner, cp = self.context()
|
||||
mass = owner.snapshot['mass']
|
||||
owner.snapshot['mass'] = EqualValue()
|
||||
self.assertTrue(owner.prepared_for(cp))
|
||||
self.assertEqual(trace, [mass, mass])
|
||||
self.assertIsNone(owner._prepared_cp_comparison)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "555501de1ea904245a97f16da247c150a8b01bf7135ef95ba591d80cb9c8a20a", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "6b983839bdaba3db71695f983ce0be058495ea9bd615b753282f9e536ffda058", "schema_version": 1}
|
||||
|
||||
Reference in New Issue
Block a user