Lab034: evaluate stale video frame dropping

This commit is contained in:
LittleSam129
2026-08-03 13:17:55 +03:00
parent 21751f41dd
commit b63e36abdb
14 changed files with 1415 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
"""Frame-aware stale-video policy for the Lab034 shared link queue.
The scheduler remains strict-priority, state-replacing, and non-preemptive.
Unlike Lab033, packet availability is separate from the timestamp stored in
the common header. Every aligned video packet therefore carries the source
composite-frame generation time while FEC construction metadata remains local
to the scheduler.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
from protocol.link_packet import HEADER_SIZE, LinkPacket, TrafficClass, encode_link_packet
from protocol.priority_scheduler import transmission_duration_seconds
TIME_EPSILON_SECONDS = 1e-12
@dataclass(frozen=True)
class AgePolicyPacket:
"""A common packet plus non-serialized availability/frame metadata."""
packet: LinkPacket
arrival_order: int
available_time_us: int
composite_frame_id: int | None = None
@property
def available_time_seconds(self) -> float:
return self.available_time_us / 1_000_000.0
@property
def frame_generation_seconds(self) -> float:
return self.packet.generation_time_us / 1_000_000.0
@property
def wire_size_bytes(self) -> int:
return HEADER_SIZE + len(self.packet.payload)
@dataclass(frozen=True)
class StateReplacement:
removed: AgePolicyPacket
replacement: AgePolicyPacket
time_seconds: float
@dataclass(frozen=True)
class DroppedVideoPacket:
item: AgePolicyPacket
drop_time_seconds: float
@dataclass(frozen=True)
class AgeScheduledPacket:
item: AgePolicyPacket
start_seconds: float
end_seconds: float
blocked_by: AgePolicyPacket | None
blocking_delay_seconds: float
wire_packet: bytes
@dataclass(frozen=True)
class AgeScheduleResult:
channel_bitrate_bps: float
max_video_age_seconds: float | None
transmitted: tuple[AgeScheduledPacket, ...]
dropped_video: tuple[DroppedVideoPacket, ...]
replacements: tuple[StateReplacement, ...]
def _enqueue(
ready: list[AgePolicyPacket],
item: AgePolicyPacket,
replacements: list[StateReplacement],
) -> None:
if item.packet.traffic_class in (TrafficClass.CONTROL, TrafficClass.TELEMETRY):
retained = []
for old in ready:
if (
old.packet.traffic_class == item.packet.traffic_class
and old.packet.stream_id == item.packet.stream_id
):
replacements.append(
StateReplacement(old, item, item.available_time_seconds)
)
else:
retained.append(old)
ready[:] = retained
ready.append(item)
def schedule_with_video_age(
packets: Iterable[AgePolicyPacket],
channel_bitrate_bps: float,
max_video_age_seconds: float | None,
) -> AgeScheduleResult:
"""Run a strict-priority non-preemptive scheduler with frame drops.
At every packet-selection instant all queued packets belonging to a video
frame older than ``max_video_age_seconds`` are removed together. An active
packet has already left the queue and is never interrupted.
"""
if channel_bitrate_bps <= 0.0:
raise ValueError("channel bitrate must be positive")
if max_video_age_seconds is not None and max_video_age_seconds <= 0.0:
raise ValueError("maximum video age must be positive")
arrivals = sorted(
tuple(packets),
key=lambda item: (item.available_time_seconds, item.arrival_order),
)
if len({item.arrival_order for item in arrivals}) != len(arrivals):
raise ValueError("arrival_order values must be unique")
for item in arrivals:
if item.packet.traffic_class is TrafficClass.VIDEO:
if item.composite_frame_id is None:
raise ValueError("video packet lacks composite_frame_id")
elif item.composite_frame_id is not None:
raise ValueError("non-video packet has composite_frame_id")
ready: list[AgePolicyPacket] = []
transmitted: list[AgeScheduledPacket] = []
dropped: list[DroppedVideoPacket] = []
replacements: list[StateReplacement] = []
blocker_by_order: dict[int, tuple[AgePolicyPacket, float]] = {}
dropped_frames: set[int] = set()
arrival_index = 0
cursor = 0.0
def admit_until(limit: float, active: AgePolicyPacket | None = None, active_end: float = 0.0) -> None:
nonlocal arrival_index
while (
arrival_index < len(arrivals)
and arrivals[arrival_index].available_time_seconds
<= limit + TIME_EPSILON_SECONDS
):
item = arrivals[arrival_index]
arrival_index += 1
if (
active is not None
and item.available_time_seconds > cursor + TIME_EPSILON_SECONDS
):
blocker_by_order[item.arrival_order] = (
active,
max(0.0, active_end - item.available_time_seconds),
)
if item.composite_frame_id in dropped_frames:
dropped.append(DroppedVideoPacket(item, limit))
else:
_enqueue(ready, item, replacements)
def expire_video(now: float) -> None:
if max_video_age_seconds is None:
return
expired_frames = {
item.composite_frame_id
for item in ready
if (
item.packet.traffic_class is TrafficClass.VIDEO
and now - item.frame_generation_seconds
> max_video_age_seconds + TIME_EPSILON_SECONDS
)
}
if not expired_frames:
return
retained = []
for item in ready:
if item.composite_frame_id in expired_frames:
dropped.append(DroppedVideoPacket(item, now))
assert item.composite_frame_id is not None
dropped_frames.add(item.composite_frame_id)
else:
retained.append(item)
ready[:] = retained
while arrival_index < len(arrivals) or ready:
if not ready:
cursor = max(cursor, arrivals[arrival_index].available_time_seconds)
admit_until(cursor)
expire_video(cursor)
if not ready:
continue
selected_index = min(
range(len(ready)),
key=lambda index: (
int(ready[index].packet.traffic_class),
ready[index].arrival_order,
),
)
selected = ready.pop(selected_index)
start = max(cursor, selected.available_time_seconds)
wire_packet = encode_link_packet(selected.packet)
end = start + transmission_duration_seconds(
len(wire_packet), channel_bitrate_bps
)
cursor = start
admit_until(end, selected, end)
blocker, blocking_delay = blocker_by_order.get(
selected.arrival_order, (None, 0.0)
)
transmitted.append(
AgeScheduledPacket(
item=selected,
start_seconds=start,
end_seconds=end,
blocked_by=blocker,
blocking_delay_seconds=blocking_delay,
wire_packet=wire_packet,
)
)
cursor = end
return AgeScheduleResult(
channel_bitrate_bps=channel_bitrate_bps,
max_video_age_seconds=max_video_age_seconds,
transmitted=tuple(transmitted),
dropped_video=tuple(dropped),
replacements=tuple(replacements),
)