feat(perception): trace M4.8S pipeline latency

This commit is contained in:
DCCONSTRUCTIONS
2026-08-25 18:21:57 +03:00
parent 5b07372791
commit a0152f371e
7 changed files with 453 additions and 9 deletions
+83
View File
@@ -0,0 +1,83 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
from k1link.perception.detector import DetectorFrameTiming
from k1link.perception.recorded_source import DecodedFrameTiming
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
RUNNER_PATH = (
REPOSITORY_ROOT / "experiments/perception/run_m48s_reference_graph_shadow_worker.py"
)
SPEC = importlib.util.spec_from_file_location("m48s_timed_shadow_runner", RUNNER_PATH)
assert SPEC is not None and SPEC.loader is not None
RUNNER = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(RUNNER)
def test_frame_timing_store_closes_provider_and_unattributed_time() -> None:
store = RUNNER.FrameTimingStore()
store.observe_decode(DecodedFrameTiming(sequence=7, duration_ns=5))
store.observe_detector(
DetectorFrameTiming(
sequence=7,
preprocess_duration_ns=10,
inference_transport_duration_ns=20,
postprocess_duration_ns=30,
total_duration_ns=60,
)
)
for stage_id, duration_ns in (
("geometry", 1),
("temporal", 2),
("motion", 3),
("rolling", 4),
("threat", 5),
):
store.observe_provider(stage_id, 7, duration_ns)
document = store.take(sequence=7, source_age_ns=100, completion_age_ns=300)
assert document["schema_version"] == RUNNER.PIPELINE_TIMING_SCHEMA
assert document["graph_admission_to_delivery_ns"] == 200
assert document["graph_attributed_provider_ns"] == 75
assert document["graph_unattributed_ns"] == 125
assert document["decode_to_delivery_processing_ns"] == 205
assert store.delivered() == (document,)
def test_pipeline_timing_metrics_preserve_single_pass_stage_breakdown() -> None:
store = RUNNER.FrameTimingStore()
store.observe_decode(DecodedFrameTiming(sequence=3, duration_ns=1_000_000))
store.observe_detector(
DetectorFrameTiming(
sequence=3,
preprocess_duration_ns=2_000_000,
inference_transport_duration_ns=3_000_000,
postprocess_duration_ns=4_000_000,
total_duration_ns=9_000_000,
)
)
for stage_id, duration_ns in (
("geometry", 1_000_000),
("temporal", 2_000_000),
("motion", 3_000_000),
("rolling", 4_000_000),
("threat", 5_000_000),
):
store.observe_provider(stage_id, 3, duration_ns)
document = store.take(
sequence=3,
source_age_ns=0,
completion_age_ns=30_000_000,
)
metrics = RUNNER._pipeline_timing_metrics([document])
assert metrics["sample_count"] == 1
assert metrics["detector_ms"]["inference_transport"]["maximum"] == 3.0
assert metrics["provider_ms"]["threat"]["maximum"] == 5.0
assert metrics["graph_unattributed_ms"]["maximum"] == 6.0
assert metrics["maximum_graph_sequence"] == 3
assert metrics["additional_inference_passes"] == 0
+29
View File
@@ -60,6 +60,7 @@ from k1link.perception.providers import (
SourcePacket,
)
from k1link.perception.recorded_source import (
DecodedFrameTiming,
DecodedRecordedSource,
RecordedRavnoves00Source,
RecordedSourceError,
@@ -869,6 +870,34 @@ def test_decoded_recorded_source_attaches_images_without_detector_logic(tmp_path
assert int(packets[1].image_payload[0, 0, 0]) == 7
def test_decoded_recorded_source_reports_per_frame_decode_timing(tmp_path: Path) -> None:
camera_path, timeline_path = _write_recorded_fixture(tmp_path)
source = RecordedRavnoves00Source(
camera_index_path=camera_path,
source_pack_path=timeline_path,
expected_frame_count=2,
expected_source_pack_sha256=None,
)
observed: list[DecodedFrameTiming] = []
class Decoder:
def frames(self, _stop_event: Event) -> Iterator[np.ndarray]:
yield np.zeros((600, 800, 3), dtype=np.uint8)
yield np.zeros((600, 800, 3), dtype=np.uint8)
packets = list(
DecodedRecordedSource(
source=source,
decoder=Decoder(),
timing_observer=observed.append,
clock_ns=iter((10, 30, 40, 90, 100)).__next__,
).packets(Event())
)
assert len(packets) == 2
assert observed == [DecodedFrameTiming(0, 20), DecodedFrameTiming(1, 50)]
def test_decoded_recorded_source_primes_decoder_before_source_clock() -> None:
events: list[str] = []
+23
View File
@@ -20,6 +20,7 @@ from k1link.perception.detector import (
RF_DETR_SHADOW_MODEL_ID,
RF_DETR_SHADOW_PREPROCESS_ID,
RF_DETR_SHADOW_PROVIDER_ID,
DetectorFrameTiming,
RfDetrShadowDetectorProvider,
)
from k1link.perception.providers import SourcePacket
@@ -170,6 +171,28 @@ def test_shadow_provider_uses_one_pass_and_preserves_semantic_hints() -> None:
assert provider.snapshot().core_duration_ns == 20
def test_shadow_provider_reports_preprocess_transport_and_postprocess_timing() -> None:
observed: list[DetectorFrameTiming] = []
provider = RfDetrShadowDetectorProvider(
mask=np.ones((600, 800), dtype=np.bool_),
backend=_Backend(_output()),
resizer=_Resizer(),
clock_ns=iter((10, 20, 50, 70)).__next__,
timing_observer=observed.append,
)
provider.detect(_packet(7, np.zeros((600, 800, 3), dtype=np.uint8)))
assert len(observed) == 1
assert observed[0].to_dict() == {
"sequence": 7,
"preprocess_duration_ns": 10,
"inference_transport_duration_ns": 30,
"postprocess_duration_ns": 20,
"total_duration_ns": 60,
}
def test_shadow_profile_is_fixed_and_transport_pins_model_version() -> None:
assert RF_DETR_CONFIG.minimum_score == 0.25
with pytest.raises(RfDetrDetectorError, match="cannot be tuned"):