fix(perception): release processed source payloads
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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]] = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user