test(perception): verify incremental decoder parity and binary ingress on Worker
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
"""CPU-only bounded decoder parity probe; paths belong only to this source adapter."""
|
||||||
|
|
||||||
|
# The reference decoder runs in the pinned Python 3.9 environment.
|
||||||
|
# ruff: noqa: UP017, B905
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import resource
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def read_fragment(root, row):
|
||||||
|
path = (root / "segments" / (str(row["sequence"]) + ".m4s")).resolve(strict=True)
|
||||||
|
if path.parent != (root / "segments").resolve(strict=True):
|
||||||
|
raise ValueError("camera path confinement failed")
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
payload = stream.read(1024 * 1024 + 1)
|
||||||
|
if len(payload) != row["length"] or len(payload) > 1024 * 1024:
|
||||||
|
raise ValueError("camera fragment size changed")
|
||||||
|
if hashlib.sha256(payload).hexdigest() != row["sha256"]:
|
||||||
|
raise ValueError("camera fragment hash changed")
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def run(args):
|
||||||
|
resource.setrlimit(resource.RLIMIT_AS, (1024**3, 1024**3))
|
||||||
|
report = {
|
||||||
|
"mode": args.mode,
|
||||||
|
"created_utc": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"started_monotonic_ns": time.monotonic_ns(),
|
||||||
|
"frames": [],
|
||||||
|
"source_pace": 1 if args.mode == "fragments" else None,
|
||||||
|
}
|
||||||
|
decoder = reference = None
|
||||||
|
if args.mode == "fragments":
|
||||||
|
from k1link.perception.streaming_decoder import FragmentDecoder
|
||||||
|
|
||||||
|
decoder = FragmentDecoder()
|
||||||
|
with (args.camera_root / "init.mp4").open("rb") as stream:
|
||||||
|
init = stream.read(65537)
|
||||||
|
begin = time.monotonic_ns()
|
||||||
|
decoder.configure(init)
|
||||||
|
report["init_ms"] = (time.monotonic_ns() - begin) / 1e6
|
||||||
|
report["init_sha256"] = hashlib.sha256(init).hexdigest()
|
||||||
|
else:
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
cv2.setNumThreads(1)
|
||||||
|
reference = cv2.VideoCapture(str(args.reference_video))
|
||||||
|
if not reference.isOpened():
|
||||||
|
raise ValueError("reference video unavailable")
|
||||||
|
report["opencv_version"] = cv2.__version__
|
||||||
|
source_zero = wall_zero = None
|
||||||
|
with (args.camera_root / "index.jsonl").open("rb") as index:
|
||||||
|
for sequence in range(args.frames):
|
||||||
|
line = index.readline(65537)
|
||||||
|
if not line or len(line) > 65536:
|
||||||
|
raise ValueError("index absent or unbounded")
|
||||||
|
row = json.loads(line)
|
||||||
|
if row["sequence"] != sequence + 1 or row["kind"] != "media":
|
||||||
|
raise ValueError("camera source identity changed")
|
||||||
|
source_time = row["host_monotonic_ns"]
|
||||||
|
if source_zero is None:
|
||||||
|
source_zero, wall_zero = source_time, time.monotonic_ns()
|
||||||
|
due = wall_zero + source_time - source_zero
|
||||||
|
if decoder:
|
||||||
|
time.sleep(max(0, (due - time.monotonic_ns()) / 1e9))
|
||||||
|
read_started = time.monotonic_ns()
|
||||||
|
payload = read_fragment(args.camera_root, row) if decoder else None
|
||||||
|
decode_started = time.monotonic_ns()
|
||||||
|
if decoder:
|
||||||
|
image = decoder.decode(payload)
|
||||||
|
else:
|
||||||
|
ok, image = reference.read()
|
||||||
|
if not ok:
|
||||||
|
raise ValueError("reference ended early")
|
||||||
|
decoded = time.monotonic_ns()
|
||||||
|
report["frames"].append(
|
||||||
|
{
|
||||||
|
"sequence": sequence,
|
||||||
|
"source_time_ns": source_time,
|
||||||
|
"fragment_sha256": row["sha256"],
|
||||||
|
"read_started_ns": read_started,
|
||||||
|
"decoded_ns": decoded,
|
||||||
|
"decode_ms": (decoded - decode_started) / 1e6,
|
||||||
|
"release_lag_ms": (read_started - due) / 1e6 if decoder else None,
|
||||||
|
"shape": list(image.shape),
|
||||||
|
"bgr_sha256": hashlib.sha256(image).hexdigest(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
del image, payload
|
||||||
|
if decoder:
|
||||||
|
report["frame_before_next_fragment_read"] = all(
|
||||||
|
a["decoded_ns"] < b["read_started_ns"]
|
||||||
|
for a, b in zip(report["frames"], report["frames"][1:])
|
||||||
|
)
|
||||||
|
decoder.close()
|
||||||
|
if reference:
|
||||||
|
reference.release()
|
||||||
|
report["finished_monotonic_ns"] = time.monotonic_ns()
|
||||||
|
report["maxrss_kib"] = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
||||||
|
args.output.write_text(json.dumps(report, indent=2) + "\n")
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{"mode": args.mode, "frames": len(report["frames"]), "maxrss_kib": report["maxrss_kib"]}
|
||||||
|
),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("mode", choices=("fragments", "reference"))
|
||||||
|
parser.add_argument("--camera-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--reference-video", type=Path)
|
||||||
|
parser.add_argument("--frames", type=int, default=128)
|
||||||
|
parser.add_argument("--output", type=Path, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not 1 <= args.frames <= 256 or (args.mode == "reference" and not args.reference_video):
|
||||||
|
parser.error("bounded frame count and explicit reference required")
|
||||||
|
run(args)
|
||||||
+44
-4
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
Recorded mode transfers original fMP4 init/media plus existing normalized
|
Recorded mode transfers original fMP4 init/media plus existing normalized
|
||||||
point/pose increments at original 1x release times. Receiver is never told the
|
point/pose increments at original 1x release times. Receiver is never told the
|
||||||
source length. The consumer only hashes raw input, it does not decode/infer.
|
source length. Optional camera decoding does not infer or bind sensors into a
|
||||||
|
full graph scene; the default consumer only hashes raw input.
|
||||||
Truncated mode is explicitly synthetic and exercises multi-fragment failure.
|
Truncated mode is explicitly synthetic and exercises multi-fragment failure.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -172,13 +173,14 @@ def producer(args):
|
|||||||
threading.Event().wait(30)
|
threading.Event().wait(30)
|
||||||
|
|
||||||
|
|
||||||
def run_case(base, case, generation):
|
def run_case(base, case, generation, *, decode_camera=False):
|
||||||
root = base / case
|
root = base / case
|
||||||
root.mkdir()
|
root.mkdir()
|
||||||
specification = {
|
specification = {
|
||||||
"case": case,
|
"case": case,
|
||||||
"transport": wire.SCHEMA,
|
"transport": wire.SCHEMA,
|
||||||
"consumer": "raw-digest-only",
|
"consumer": "fragment-decode-and-raw-digest" if decode_camera else "raw-digest-only",
|
||||||
|
"full_graph": False,
|
||||||
"camera": "original-fmp4-init-and-media",
|
"camera": "original-fmp4-init-and-media",
|
||||||
"points": "u32le-count+xyz-f64le+intensity-u8",
|
"points": "u32le-count+xyz-f64le+intensity-u8",
|
||||||
"pose": "position-f64le3+map-from-lidar-quaternion-f64le4",
|
"pose": "position-f64le3+map-from-lidar-quaternion-f64le4",
|
||||||
@@ -217,9 +219,16 @@ def run_case(base, case, generation):
|
|||||||
heartbeat.start()
|
heartbeat.start()
|
||||||
left, right = socket.socketpair()
|
left, right = socket.socketpair()
|
||||||
receiver = None
|
receiver = None
|
||||||
|
decoder = codec_reservation = None
|
||||||
started = time.monotonic_ns()
|
started = time.monotonic_ns()
|
||||||
with (root / "producer.log").open("wb") as log, (root / "received.jsonl").open("w") as journal:
|
with (root / "producer.log").open("wb") as log, (root / "received.jsonl").open("w") as journal:
|
||||||
try:
|
try:
|
||||||
|
if decode_camera:
|
||||||
|
from k1link.perception.streaming_decoder import FragmentDecoder
|
||||||
|
|
||||||
|
# Stored init/extradata have independent ownership until close.
|
||||||
|
codec_reservation = run.mailbox.reserve_ingress(2 * 65536)
|
||||||
|
decoder = FragmentDecoder()
|
||||||
run.spawn(
|
run.spawn(
|
||||||
lambda: subprocess.Popen(
|
lambda: subprocess.Popen(
|
||||||
[
|
[
|
||||||
@@ -244,12 +253,38 @@ def run_case(base, case, generation):
|
|||||||
left.close()
|
left.close()
|
||||||
|
|
||||||
def consume(event):
|
def consume(event):
|
||||||
|
decoded = {}
|
||||||
|
if decoder is not None and event.modality.startswith("camera"):
|
||||||
|
# Conservative raw/sample/BGR scratch, reserved BEFORE decode.
|
||||||
|
# FFmpeg codec/DPB/native allocations are additionally bounded
|
||||||
|
# by the CPU-only container, not claimed as Python payloads.
|
||||||
|
scratch = run.mailbox.reserve_ingress(
|
||||||
|
2 * wire.MAX_FRAGMENT + 2 * 1440000 + 65536
|
||||||
|
)
|
||||||
|
image = None
|
||||||
|
begin = time.monotonic_ns()
|
||||||
|
try:
|
||||||
|
if event.modality == "camera-init":
|
||||||
|
decoder.configure(event.payload)
|
||||||
|
decoded["initialized"] = True
|
||||||
|
else:
|
||||||
|
image = decoder.decode(event.payload)
|
||||||
|
decoded.update(
|
||||||
|
bgr_sha256=hashlib.sha256(image).hexdigest(),
|
||||||
|
decoded_frame_index=decoder.frames - 1,
|
||||||
|
shape=list(image.shape),
|
||||||
|
)
|
||||||
|
decoded["decode_ms"] = (time.monotonic_ns() - begin) / 1e6
|
||||||
|
finally:
|
||||||
|
image = None
|
||||||
|
scratch.release()
|
||||||
emit_json(
|
emit_json(
|
||||||
journal,
|
journal,
|
||||||
{
|
{
|
||||||
**record(event),
|
**record(event),
|
||||||
"received_monotonic_ns": str(time.monotonic_ns()),
|
"received_monotonic_ns": str(time.monotonic_ns()),
|
||||||
"original_received_monotonic_ns": str(event.received_monotonic_ns),
|
"original_received_monotonic_ns": str(event.received_monotonic_ns),
|
||||||
|
**decoded,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -273,6 +308,10 @@ def run_case(base, case, generation):
|
|||||||
right.close()
|
right.close()
|
||||||
if receiver:
|
if receiver:
|
||||||
receiver.join()
|
receiver.join()
|
||||||
|
if decoder is not None:
|
||||||
|
decoder.close()
|
||||||
|
if codec_reservation is not None:
|
||||||
|
codec_reservation.release()
|
||||||
heartbeat_stop.set()
|
heartbeat_stop.set()
|
||||||
heartbeat.join(timeout=1)
|
heartbeat.join(timeout=1)
|
||||||
released = run.close()
|
released = run.close()
|
||||||
@@ -303,6 +342,7 @@ def run_case(base, case, generation):
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--producer", action="store_true")
|
parser.add_argument("--producer", action="store_true")
|
||||||
|
parser.add_argument("--decode-camera", action="store_true")
|
||||||
parser.add_argument("--fd", type=int)
|
parser.add_argument("--fd", type=int)
|
||||||
parser.add_argument("--case", choices=("recorded", "truncated"))
|
parser.add_argument("--case", choices=("recorded", "truncated"))
|
||||||
parser.add_argument("--output", default="/out")
|
parser.add_argument("--output", default="/out")
|
||||||
@@ -311,4 +351,4 @@ if __name__ == "__main__":
|
|||||||
producer(args)
|
producer(args)
|
||||||
else:
|
else:
|
||||||
for generation, case in enumerate(("recorded", "truncated"), 1):
|
for generation, case in enumerate(("recorded", "truncated"), 1):
|
||||||
run_case(Path(args.output), case, generation)
|
run_case(Path(args.output), case, generation, decode_camera=args.decode_camera)
|
||||||
|
|||||||
Reference in New Issue
Block a user