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,
ObstacleObservation,
SourceAccounting,
SourceEnvelope,
TemporalObstacle,
TemporalState,
validate_exclusive_point_ownership,
@@ -197,7 +198,9 @@ class ReferencePerceptionGraphV1:
self._threads: list[Thread] = []
self._outcomes: dict[int, TerminalOutcome] = {}
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._queue_high_watermarks: dict[str, int] = {
stage_id: 0 for stage_id in ("detector", "geometry", "temporal", "rolling", "threat")
@@ -334,7 +337,7 @@ class ReferencePerceptionGraphV1:
with self._result_lock:
if envelope.sequence in self._admitted:
return False
self._admitted[envelope.sequence] = packet
self._admitted[envelope.sequence] = envelope
self._admitted_at_ns[envelope.sequence] = self._now()
if envelope.source_id != BASELINE_SOURCE_ID:
self._terminal(packet, TerminalOutcomeType.REJECTED, "source", "source-not-admitted")
@@ -620,7 +623,15 @@ class ReferencePerceptionGraphV1:
stage_id: str,
reason: str,
) -> 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(
source_id=envelope.source_id,
session_id=envelope.session_id,
@@ -632,6 +643,7 @@ class ReferencePerceptionGraphV1:
)
with self._result_lock:
self._outcomes.setdefault(envelope.sequence, terminal)
self._admitted_at_ns.pop(envelope.sequence, None)
def _put_latest(
self,
@@ -737,10 +749,10 @@ class ReferencePerceptionGraphV1:
def _close_accounting(self) -> None:
with self._result_lock:
missing = sorted(set(self._admitted) - set(self._outcomes))
packets = [self._admitted[sequence] for sequence in missing]
for packet in packets:
self._terminal(
packet,
envelopes = [self._admitted[sequence] for sequence in missing]
for envelope in envelopes:
self._terminal_envelope(
envelope,
TerminalOutcomeType.FAILED,
"graph",
"terminal-accounting-gap",
+36
View File
@@ -1,9 +1,11 @@
from __future__ import annotations
import gc
import json
import subprocess
import sys
import threading
import weakref
from collections.abc import Callable, Iterator
from dataclasses import replace
from pathlib import Path
@@ -553,6 +555,40 @@ def test_reference_graph_v2_observes_final_delivery_completion_age() -> None:
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:
observed: list[tuple[int, int, tuple[str, ...], frozenset[str], int]] = []