fix(perception): release processed source payloads

This commit is contained in:
DCCONSTRUCTIONS
2026-08-25 18:04:26 +03:00
parent b5080671c9
commit 5b07372791
2 changed files with 55 additions and 7 deletions
+19 -7
View File
@@ -24,6 +24,7 @@ from .contracts import (
ObjectProposal2D, ObjectProposal2D,
ObstacleObservation, ObstacleObservation,
SourceAccounting, SourceAccounting,
SourceEnvelope,
TemporalObstacle, TemporalObstacle,
TemporalState, TemporalState,
validate_exclusive_point_ownership, validate_exclusive_point_ownership,
@@ -197,7 +198,9 @@ class ReferencePerceptionGraphV1:
self._threads: list[Thread] = [] self._threads: list[Thread] = []
self._outcomes: dict[int, TerminalOutcome] = {} self._outcomes: dict[int, TerminalOutcome] = {}
self._deliveries: list[DeliveredFrame] = [] self._deliveries: list[DeliveredFrame] = []
self._admitted: dict[int, SourcePacket] = {} # Accounting needs immutable source identity, not the decoded payload. Keeping
# SourcePacket here retained every camera raster until the run completed.
self._admitted: dict[int, SourceEnvelope] = {}
self._admitted_at_ns: dict[int, int] = {} self._admitted_at_ns: dict[int, int] = {}
self._queue_high_watermarks: dict[str, int] = { self._queue_high_watermarks: dict[str, int] = {
stage_id: 0 for stage_id in ("detector", "geometry", "temporal", "rolling", "threat") stage_id: 0 for stage_id in ("detector", "geometry", "temporal", "rolling", "threat")
@@ -334,7 +337,7 @@ class ReferencePerceptionGraphV1:
with self._result_lock: with self._result_lock:
if envelope.sequence in self._admitted: if envelope.sequence in self._admitted:
return False return False
self._admitted[envelope.sequence] = packet self._admitted[envelope.sequence] = envelope
self._admitted_at_ns[envelope.sequence] = self._now() self._admitted_at_ns[envelope.sequence] = self._now()
if envelope.source_id != BASELINE_SOURCE_ID: if envelope.source_id != BASELINE_SOURCE_ID:
self._terminal(packet, TerminalOutcomeType.REJECTED, "source", "source-not-admitted") self._terminal(packet, TerminalOutcomeType.REJECTED, "source", "source-not-admitted")
@@ -620,7 +623,15 @@ class ReferencePerceptionGraphV1:
stage_id: str, stage_id: str,
reason: str, reason: str,
) -> None: ) -> None:
envelope = packet.envelope self._terminal_envelope(packet.envelope, outcome, stage_id, reason)
def _terminal_envelope(
self,
envelope: SourceEnvelope,
outcome: TerminalOutcomeType,
stage_id: str,
reason: str,
) -> None:
terminal = TerminalOutcome( terminal = TerminalOutcome(
source_id=envelope.source_id, source_id=envelope.source_id,
session_id=envelope.session_id, session_id=envelope.session_id,
@@ -632,6 +643,7 @@ class ReferencePerceptionGraphV1:
) )
with self._result_lock: with self._result_lock:
self._outcomes.setdefault(envelope.sequence, terminal) self._outcomes.setdefault(envelope.sequence, terminal)
self._admitted_at_ns.pop(envelope.sequence, None)
def _put_latest( def _put_latest(
self, self,
@@ -737,10 +749,10 @@ class ReferencePerceptionGraphV1:
def _close_accounting(self) -> None: def _close_accounting(self) -> None:
with self._result_lock: with self._result_lock:
missing = sorted(set(self._admitted) - set(self._outcomes)) missing = sorted(set(self._admitted) - set(self._outcomes))
packets = [self._admitted[sequence] for sequence in missing] envelopes = [self._admitted[sequence] for sequence in missing]
for packet in packets: for envelope in envelopes:
self._terminal( self._terminal_envelope(
packet, envelope,
TerminalOutcomeType.FAILED, TerminalOutcomeType.FAILED,
"graph", "graph",
"terminal-accounting-gap", "terminal-accounting-gap",
+36
View File
@@ -1,9 +1,11 @@
from __future__ import annotations from __future__ import annotations
import gc
import json import json
import subprocess import subprocess
import sys import sys
import threading import threading
import weakref
from collections.abc import Callable, Iterator from collections.abc import Callable, Iterator
from dataclasses import replace from dataclasses import replace
from pathlib import Path from pathlib import Path
@@ -553,6 +555,40 @@ def test_reference_graph_v2_observes_final_delivery_completion_age() -> None:
assert observed == [(0, 123, 123)] assert observed == [(0, 123, 123)]
def test_reference_graph_retains_source_identity_without_processed_payload() -> None:
packet = _packet(0)
graph = _graph_v2(_Source((packet,)))
result = graph.run()
assert result.state is GraphState.STOPPED
assert graph._admitted == {0: packet.envelope}
assert graph._admitted_at_ns == {}
def test_reference_graph_releases_ephemeral_source_payloads_after_run() -> None:
payloads: list[weakref.ReferenceType[np.ndarray]] = []
class EphemeralSource:
provider_id = _Source.provider_id
def packets(self, stop_event: Event) -> Iterator[SourcePacket]:
for sequence in range(32):
if stop_event.is_set():
return
image = np.empty((64, 64, 3), dtype=np.uint8)
payloads.append(weakref.ref(image))
yield replace(_packet(sequence), image_payload=image)
graph = _graph_v2(EphemeralSource())
result = graph.run()
gc.collect()
assert result.state is GraphState.STOPPED
assert not any(reference() is not None for reference in payloads)
def test_reference_graph_v2_observes_exact_delivery_evidence_inputs() -> None: def test_reference_graph_v2_observes_exact_delivery_evidence_inputs() -> None:
observed: list[tuple[int, int, tuple[str, ...], frozenset[str], int]] = [] observed: list[tuple[int, int, tuple[str, ...], frozenset[str], int]] = []