fix(uvc): preserve control event fairness
This commit is contained in:
parent
2621f7dead
commit
2a5f059a0b
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1658,7 +1658,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.27.9"
|
version = "0.27.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1692,7 +1692,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.27.9"
|
version = "0.27.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1704,7 +1704,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.27.9"
|
version = "0.27.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.27.9"
|
version = "0.27.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.27.9"
|
version = "0.27.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -314,7 +314,19 @@ into the Lesavka tree, installs it with the server, requires `libgpiod`, and
|
|||||||
runs it with `/usr/bin/python` before capture discovery. This removes the
|
runs it with `/usr/bin/python` before capture discovery. This removes the
|
||||||
unversioned virtualenv from the physical eye-feed power path.
|
unversioned virtualenv from the physical eye-feed power path.
|
||||||
|
|
||||||
## 7. The Current Downstream Video Failure
|
### The `0.27.10` UVC Event-Fairness Fix
|
||||||
|
|
||||||
|
The first post-repair synthetic upstream probe reached Tethys but produced no
|
||||||
|
decodable MJPEG timeline. Tethys recorded repeated `EOI before SOF` packets and
|
||||||
|
two five-second UVC probe-control timeouts. The helper drained completed V4L2
|
||||||
|
output buffers without a per-turn bound before servicing control events, so a
|
||||||
|
continuously consuming host could starve probe/commit traffic. Release 0.27.10
|
||||||
|
bounds dequeue work, reports fairness yields, and makes the native receiver
|
||||||
|
probe terminate with evidence even when malformed packets produce no decoded
|
||||||
|
timestamps. This is a hardware-testable correction; marked Tethys capture is
|
||||||
|
still required before declaring the tear/smear class fixed.
|
||||||
|
|
||||||
|
## 7. The Resolved Downstream Video Failure
|
||||||
|
|
||||||
The current blank downstream feeds fail before transport or decoding.
|
The current blank downstream feeds fail before transport or decoding.
|
||||||
|
|
||||||
@ -369,6 +381,10 @@ successfully imported the installed `libgpiod` binding. This proves the common
|
|||||||
downstream failure was capture-card power automation, not two simultaneous
|
downstream failure was capture-card power automation, not two simultaneous
|
||||||
card failures, H.264 transport, or client decoding. Release 0.27.9 makes the
|
card failures, H.264 transport, or client decoding. Release 0.27.9 makes the
|
||||||
working system binding and a versioned helper the installed service contract.
|
working system binding and a versioned helper the installed service contract.
|
||||||
|
After installing 0.27.9, GPIO27 stayed asserted, both GC311 devices enumerated,
|
||||||
|
and both downstream RPCs decoded 1280x720 H.264. The right eye carried changing
|
||||||
|
desktop video; the left eye carried the capture card's explicit `No Signal`
|
||||||
|
frame, isolating that remaining input condition to its HDMI source/cable.
|
||||||
|
|
||||||
## 8. Recovery And Deployment Sequence
|
## 8. Recovery And Deployment Sequence
|
||||||
|
|
||||||
@ -386,7 +402,7 @@ The safe completion sequence for this incident is:
|
|||||||
6. Run `scripts/install/server.sh` as the trusted deployment path. Preserve the
|
6. Run `scripts/install/server.sh` as the trusted deployment path. Preserve the
|
||||||
already-attached USB gadget unless a controlled rebuild is explicitly
|
already-attached USB gadget unless a controlled rebuild is explicitly
|
||||||
required.
|
required.
|
||||||
7. Confirm Theia reports server version `0.27.9`, the pushed release revision,
|
7. Confirm Theia reports server version `0.27.10`, the pushed release revision,
|
||||||
direct MJPEG normalizer timeout `0`, and a coherent UVC contract.
|
direct MJPEG normalizer timeout `0`, and a coherent UVC contract.
|
||||||
8. Open both downstream RPCs and prove that each emits changing, decodable H.264
|
8. Open both downstream RPCs and prove that each emits changing, decodable H.264
|
||||||
frames.
|
frames.
|
||||||
@ -404,7 +420,7 @@ hardware contract is repeatable. The remaining work falls into five groups.
|
|||||||
|
|
||||||
### A. Install And Version Parity
|
### A. Install And Version Parity
|
||||||
|
|
||||||
- Push and deploy `0.27.9` through the client/server install scripts.
|
- Push and deploy `0.27.10` through the client/server install scripts.
|
||||||
- Confirm client/server version and revision in every hardware probe artifact.
|
- Confirm client/server version and revision in every hardware probe artifact.
|
||||||
- Eliminate the current state where a fixed client talks to an unfixed server.
|
- Eliminate the current state where a fixed client talks to an unfixed server.
|
||||||
|
|
||||||
@ -470,6 +486,6 @@ host repair:
|
|||||||
9. disconnect/reconnect and device changes recover without stale media; and
|
9. disconnect/reconnect and device changes recover without stale media; and
|
||||||
10. diagnostics identify the failed physical stage when any item breaks.
|
10. diagnostics identify the failed physical stage when any item breaks.
|
||||||
|
|
||||||
Until that sequence passes on the installed `0.27.9` client/server pair, the
|
Until that sequence passes on the installed `0.27.10` client/server pair, the
|
||||||
current release should be described as a validated code correction awaiting
|
current release should be described as a validated code correction awaiting
|
||||||
hardware deployment and end-to-end acceptance, not as a completed product fix.
|
hardware deployment and end-to-end acceptance, not as a completed product fix.
|
||||||
|
|||||||
@ -418,6 +418,7 @@ LESAVKA_UVC_WIDTH=$(uvc_env_value LESAVKA_UVC_WIDTH 1280)
|
|||||||
LESAVKA_UVC_HEIGHT=$(uvc_env_value LESAVKA_UVC_HEIGHT 720)
|
LESAVKA_UVC_HEIGHT=$(uvc_env_value LESAVKA_UVC_HEIGHT 720)
|
||||||
LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC}
|
LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC}
|
||||||
LESAVKA_UVC_BUFFER_COUNT=$(uvc_env_value LESAVKA_UVC_BUFFER_COUNT 4)
|
LESAVKA_UVC_BUFFER_COUNT=$(uvc_env_value LESAVKA_UVC_BUFFER_COUNT 4)
|
||||||
|
LESAVKA_UVC_PUMP_DEQUEUE_BUDGET=$(uvc_env_value LESAVKA_UVC_PUMP_DEQUEUE_BUDGET 4)
|
||||||
LESAVKA_UVC_BLOCKING=$(uvc_env_value LESAVKA_UVC_BLOCKING 1)
|
LESAVKA_UVC_BLOCKING=$(uvc_env_value LESAVKA_UVC_BLOCKING 1)
|
||||||
LESAVKA_UVC_CONTROL_READ_ONLY=$(uvc_env_value LESAVKA_UVC_CONTROL_READ_ONLY 0)
|
LESAVKA_UVC_CONTROL_READ_ONLY=$(uvc_env_value LESAVKA_UVC_CONTROL_READ_ONLY 0)
|
||||||
LESAVKA_UVC_QUEUE_PACING=$(uvc_env_value LESAVKA_UVC_QUEUE_PACING 0)
|
LESAVKA_UVC_QUEUE_PACING=$(uvc_env_value LESAVKA_UVC_QUEUE_PACING 0)
|
||||||
|
|||||||
@ -1672,8 +1672,8 @@ def run_capture(args: argparse.Namespace) -> int:
|
|||||||
command, capture_width, capture_height, device = ffmpeg_cmd(args, width, height)
|
command, capture_width, capture_height, device = ffmpeg_cmd(args, width, height)
|
||||||
artifact_dir = pathlib.Path(args.artifact_dir) if args.artifact_dir else pathlib.Path("/tmp") / f"lesavka-synthetic-rct-capture-{timestamp()}"
|
artifact_dir = pathlib.Path(args.artifact_dir) if args.artifact_dir else pathlib.Path("/tmp") / f"lesavka-synthetic-rct-capture-{timestamp()}"
|
||||||
artifact_dir.mkdir(parents=True, exist_ok=True)
|
artifact_dir.mkdir(parents=True, exist_ok=True)
|
||||||
if args.deep_capture and args.source != "uvc":
|
if args.deep_capture and args.source != "device":
|
||||||
raise SystemExit("--deep-capture requires --source uvc")
|
raise SystemExit("--deep-capture requires --source device")
|
||||||
if args.deep_capture:
|
if args.deep_capture:
|
||||||
write_deep_capture_snapshot(artifact_dir, "before", device)
|
write_deep_capture_snapshot(artifact_dir, "before", device)
|
||||||
dynamic_debug_enabled = (
|
dynamic_debug_enabled = (
|
||||||
@ -1688,6 +1688,7 @@ def run_capture(args: argparse.Namespace) -> int:
|
|||||||
analysis_elapsed = 0.0
|
analysis_elapsed = 0.0
|
||||||
raw_capture_bytes = 0
|
raw_capture_bytes = 0
|
||||||
ffmpeg_rc: int | None = None
|
ffmpeg_rc: int | None = None
|
||||||
|
capture_timed_out = False
|
||||||
frame_index = 0
|
frame_index = 0
|
||||||
suspicious_count = 0
|
suspicious_count = 0
|
||||||
visual_suspicious_count = 0
|
visual_suspicious_count = 0
|
||||||
@ -1807,11 +1808,26 @@ def run_capture(args: argparse.Namespace) -> int:
|
|||||||
capture_command[-1:-1] = ["-t", str(args.duration)]
|
capture_command[-1:-1] = ["-t", str(args.duration)]
|
||||||
capture_command[-1] = str(raw_path)
|
capture_command[-1] = str(raw_path)
|
||||||
(artifact_dir / "command.txt").write_text(" ".join(shlex.quote(part) for part in capture_command) + "\n")
|
(artifact_dir / "command.txt").write_text(" ".join(shlex.quote(part) for part in capture_command) + "\n")
|
||||||
print(f"capturing raw RCT frames before analysis: {raw_path}", file=sys.stderr)
|
capture_target = native_mjpeg if native_mjpeg is not None else raw_path
|
||||||
|
print(f"capturing RCT frames before analysis: {capture_target}", file=sys.stderr)
|
||||||
capture_started = time.monotonic()
|
capture_started = time.monotonic()
|
||||||
proc = subprocess.run(capture_command, stdout=subprocess.DEVNULL, stderr=err, check=False)
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
capture_command,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=err,
|
||||||
|
check=False,
|
||||||
|
timeout=max(5.0, args.duration + 10.0),
|
||||||
|
)
|
||||||
|
ffmpeg_rc = proc.returncode
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
capture_timed_out = True
|
||||||
|
ffmpeg_rc = 124
|
||||||
|
err.write(
|
||||||
|
f"capture exceeded wall timeout after {args.duration + 10.0:.1f}s\n".encode()
|
||||||
|
)
|
||||||
|
err.flush()
|
||||||
capture_elapsed = time.monotonic() - capture_started
|
capture_elapsed = time.monotonic() - capture_started
|
||||||
ffmpeg_rc = proc.returncode
|
|
||||||
if args.deep_capture and native_mjpeg is not None and native_mjpeg.exists():
|
if args.deep_capture and native_mjpeg is not None and native_mjpeg.exists():
|
||||||
decode_command = [
|
decode_command = [
|
||||||
"ffmpeg", "-hide_banner", "-nostdin", "-loglevel", "warning",
|
"ffmpeg", "-hide_banner", "-nostdin", "-loglevel", "warning",
|
||||||
@ -1819,6 +1835,7 @@ def run_capture(args: argparse.Namespace) -> int:
|
|||||||
]
|
]
|
||||||
(artifact_dir / "decode-command.txt").write_text(" ".join(shlex.quote(part) for part in decode_command) + "\n")
|
(artifact_dir / "decode-command.txt").write_text(" ".join(shlex.quote(part) for part in decode_command) + "\n")
|
||||||
subprocess.run(decode_command, stdout=subprocess.DEVNULL, stderr=err, check=False)
|
subprocess.run(decode_command, stdout=subprocess.DEVNULL, stderr=err, check=False)
|
||||||
|
raw_path.touch(exist_ok=True)
|
||||||
raw_capture_bytes = raw_path.stat().st_size if raw_path.exists() else 0
|
raw_capture_bytes = raw_path.stat().st_size if raw_path.exists() else 0
|
||||||
print(
|
print(
|
||||||
f"analyzing captured raw RCT frames bytes={raw_capture_bytes} capture_s={capture_elapsed:.3f}",
|
f"analyzing captured raw RCT frames bytes={raw_capture_bytes} capture_s={capture_elapsed:.3f}",
|
||||||
@ -1855,6 +1872,7 @@ def run_capture(args: argparse.Namespace) -> int:
|
|||||||
"duration_observed_s": round(elapsed, 3),
|
"duration_observed_s": round(elapsed, 3),
|
||||||
"analysis_duration_s": round(analysis_elapsed, 3),
|
"analysis_duration_s": round(analysis_elapsed, 3),
|
||||||
"ffmpeg_rc": ffmpeg_rc,
|
"ffmpeg_rc": ffmpeg_rc,
|
||||||
|
"capture_timed_out": capture_timed_out,
|
||||||
"raw_capture_bytes": raw_capture_bytes,
|
"raw_capture_bytes": raw_capture_bytes,
|
||||||
"frames": frame_index,
|
"frames": frame_index,
|
||||||
"fps_observed": round(frame_index / elapsed, 3),
|
"fps_observed": round(frame_index / elapsed, 3),
|
||||||
|
|||||||
@ -16,7 +16,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.27.9"
|
version = "0.27.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,7 @@ const IDLE_MJPEG_FRAME: &[u8] = include_bytes!("lesavka_uvc/idle_1280x720_black.
|
|||||||
// Keep enough output buffers queued that the USB gadget is not starved while a
|
// Keep enough output buffers queued that the USB gadget is not starved while a
|
||||||
// browser is consuming high-speed isochronous MJPEG.
|
// browser is consuming high-speed isochronous MJPEG.
|
||||||
const DEFAULT_UVC_BUFFER_COUNT: u32 = 4;
|
const DEFAULT_UVC_BUFFER_COUNT: u32 = 4;
|
||||||
|
const DEFAULT_UVC_PUMP_DEQUEUE_BUDGET: usize = 4;
|
||||||
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
|
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
|
||||||
const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
|
const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
|
||||||
const DEFAULT_UVC_QUEUE_PACING: bool = false;
|
const DEFAULT_UVC_QUEUE_PACING: bool = false;
|
||||||
@ -292,6 +293,7 @@ struct UvcVideoStats {
|
|||||||
last_rejected_oversize_cap: usize,
|
last_rejected_oversize_cap: usize,
|
||||||
paced_sleeps: u64,
|
paced_sleeps: u64,
|
||||||
paced_sleep_ms: u64,
|
paced_sleep_ms: u64,
|
||||||
|
pump_budget_yields: u64,
|
||||||
last_report: Option<Instant>,
|
last_report: Option<Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,7 +382,7 @@ impl UvcVideoStream {
|
|||||||
if !self.streaming {
|
if !self.streaming {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
loop {
|
for _ in 0..uvc_pump_dequeue_budget() {
|
||||||
let mut buf = empty_v4l2_buffer();
|
let mut buf = empty_v4l2_buffer();
|
||||||
let req = ioctl_readwrite::<V4l2Buffer>(b'V', 17);
|
let req = ioctl_readwrite::<V4l2Buffer>(b'V', 17);
|
||||||
let rc = unsafe { libc::ioctl(self.fd, req, &mut buf) };
|
let rc = unsafe { libc::ioctl(self.fd, req, &mut buf) };
|
||||||
@ -399,6 +401,8 @@ impl UvcVideoStream {
|
|||||||
}
|
}
|
||||||
self.queue_buffer(buf.index, buf.flags)?;
|
self.queue_buffer(buf.index, buf.flags)?;
|
||||||
}
|
}
|
||||||
|
self.stats.pump_budget_yields += 1;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_format(&self, cfg: UvcConfig) -> Result<()> {
|
fn set_format(&self, cfg: UvcConfig) -> Result<()> {
|
||||||
@ -684,7 +688,7 @@ impl UvcVideoStream {
|
|||||||
}
|
}
|
||||||
self.stats.last_report = Some(now);
|
self.stats.last_report = Some(now);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[lesavka-uvc] video stats queued={} reloaded={} stale_replay={} rejected_oversize={} rejected_invalid={} fallback_idle={} held_last_good={} read_errors={} strict_validation_failures={} dqbuf_ioctl_errors={} dqbuf_flag_errors={} qbuf_ioctl_errors={} latest_bytes={} frame_cap={} last_rejected_oversize_bytes={} last_rejected_oversize_cap={} paced_sleeps={} paced_sleep_ms={}",
|
"[lesavka-uvc] video stats queued={} reloaded={} stale_replay={} rejected_oversize={} rejected_invalid={} fallback_idle={} held_last_good={} read_errors={} strict_validation_failures={} dqbuf_ioctl_errors={} dqbuf_flag_errors={} qbuf_ioctl_errors={} latest_bytes={} frame_cap={} last_rejected_oversize_bytes={} last_rejected_oversize_cap={} paced_sleeps={} paced_sleep_ms={} pump_budget_yields={}",
|
||||||
self.stats.queued,
|
self.stats.queued,
|
||||||
self.stats.reloaded,
|
self.stats.reloaded,
|
||||||
self.stats.replayed_stale,
|
self.stats.replayed_stale,
|
||||||
@ -702,7 +706,8 @@ impl UvcVideoStream {
|
|||||||
self.stats.last_rejected_oversize_bytes,
|
self.stats.last_rejected_oversize_bytes,
|
||||||
self.stats.last_rejected_oversize_cap,
|
self.stats.last_rejected_oversize_cap,
|
||||||
self.stats.paced_sleeps,
|
self.stats.paced_sleeps,
|
||||||
self.stats.paced_sleep_ms
|
self.stats.paced_sleep_ms,
|
||||||
|
self.stats.pump_budget_yields
|
||||||
);
|
);
|
||||||
if let Some(path) = uvc_stats_path() {
|
if let Some(path) = uvc_stats_path() {
|
||||||
let _ = write_atomic_text(
|
let _ = write_atomic_text(
|
||||||
@ -778,6 +783,14 @@ fn uvc_buffer_count() -> u32 {
|
|||||||
env_u32("LESAVKA_UVC_BUFFER_COUNT", DEFAULT_UVC_BUFFER_COUNT).clamp(1, 8)
|
env_u32("LESAVKA_UVC_BUFFER_COUNT", DEFAULT_UVC_BUFFER_COUNT).clamp(1, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn uvc_pump_dequeue_budget() -> usize {
|
||||||
|
env_u64(
|
||||||
|
"LESAVKA_UVC_PUMP_DEQUEUE_BUDGET",
|
||||||
|
DEFAULT_UVC_PUMP_DEQUEUE_BUDGET as u64,
|
||||||
|
)
|
||||||
|
.clamp(1, 64) as usize
|
||||||
|
}
|
||||||
|
|
||||||
fn uvc_idle_pump_sleep() -> Duration {
|
fn uvc_idle_pump_sleep() -> Duration {
|
||||||
Duration::from_millis(env_u64(
|
Duration::from_millis(env_u64(
|
||||||
"LESAVKA_UVC_IDLE_PUMP_MS",
|
"LESAVKA_UVC_IDLE_PUMP_MS",
|
||||||
@ -903,7 +916,7 @@ fn write_atomic_text(path: &std::path::Path, text: &str) -> Result<()> {
|
|||||||
fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
|
fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
|
||||||
let kernel = uvc_kernel_stats_json();
|
let kernel = uvc_kernel_stats_json();
|
||||||
format!(
|
format!(
|
||||||
"{{\"generated_unix_ms\":{},\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"held_last_good\":{},\"read_errors\":{},\"strict_validation_failures\":{},\"dqbuf_ioctl_errors\":{},\"dqbuf_flag_errors\":{},\"qbuf_ioctl_errors\":{},\"latest_bytes\":{},\"frame_cap\":{},\"last_rejected_oversize_bytes\":{},\"last_rejected_oversize_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{},\"kernel\":{}}}\n",
|
"{{\"generated_unix_ms\":{},\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"held_last_good\":{},\"read_errors\":{},\"strict_validation_failures\":{},\"dqbuf_ioctl_errors\":{},\"dqbuf_flag_errors\":{},\"qbuf_ioctl_errors\":{},\"latest_bytes\":{},\"frame_cap\":{},\"last_rejected_oversize_bytes\":{},\"last_rejected_oversize_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{},\"pump_budget_yields\":{},\"kernel\":{}}}\n",
|
||||||
unix_now_ns() / 1_000_000,
|
unix_now_ns() / 1_000_000,
|
||||||
stats.queued,
|
stats.queued,
|
||||||
stats.reloaded,
|
stats.reloaded,
|
||||||
@ -923,6 +936,7 @@ fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
|
|||||||
stats.last_rejected_oversize_cap,
|
stats.last_rejected_oversize_cap,
|
||||||
stats.paced_sleeps,
|
stats.paced_sleeps,
|
||||||
stats.paced_sleep_ms,
|
stats.paced_sleep_ms,
|
||||||
|
stats.pump_budget_yields,
|
||||||
kernel
|
kernel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -276,6 +276,7 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
|||||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_HEIGHT 720"));
|
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_HEIGHT 720"));
|
||||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_CONTROL_READ_ONLY 0"));
|
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_CONTROL_READ_ONLY 0"));
|
||||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_BUFFER_COUNT 4"));
|
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_BUFFER_COUNT 4"));
|
||||||
|
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_PUMP_DEQUEUE_BUDGET 4"));
|
||||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_QUEUE_PACING 0"));
|
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_QUEUE_PACING 0"));
|
||||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_BULK 1"));
|
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_BULK 1"));
|
||||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_FRAME_SIZE_GUARD 1"));
|
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_FRAME_SIZE_GUARD 1"));
|
||||||
|
|||||||
@ -376,6 +376,24 @@ mod uvc_binary {
|
|||||||
assert!(source.contains("streaming_bulk is not 1; using isochronous payload sizing"));
|
assert!(source.contains("streaming_bulk is not 1; using isochronous payload sizing"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn uvc_video_pump_is_bounded_so_control_requests_cannot_starve() {
|
||||||
|
with_var("LESAVKA_UVC_PUMP_DEQUEUE_BUDGET", None::<&str>, || {
|
||||||
|
assert_eq!(uvc_pump_dequeue_budget(), 4);
|
||||||
|
});
|
||||||
|
with_var("LESAVKA_UVC_PUMP_DEQUEUE_BUDGET", Some("1"), || {
|
||||||
|
assert_eq!(uvc_pump_dequeue_budget(), 1);
|
||||||
|
});
|
||||||
|
with_var("LESAVKA_UVC_PUMP_DEQUEUE_BUDGET", Some("999"), || {
|
||||||
|
assert_eq!(uvc_pump_dequeue_budget(), 64);
|
||||||
|
});
|
||||||
|
|
||||||
|
let source = include_str!("../../../../server/src/bin/lesavka-uvc.real.inc");
|
||||||
|
assert!(source.contains("for _ in 0..uvc_pump_dequeue_budget()"));
|
||||||
|
assert!(source.contains("self.stats.pump_budget_yields += 1"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn uvc_stats_snapshot_can_be_disabled_or_written() {
|
fn uvc_stats_snapshot_can_be_disabled_or_written() {
|
||||||
@ -397,6 +415,7 @@ mod uvc_binary {
|
|||||||
last_rejected_oversize_cap: 300_000,
|
last_rejected_oversize_cap: 300_000,
|
||||||
paced_sleeps: 5,
|
paced_sleeps: 5,
|
||||||
paced_sleep_ms: 123,
|
paced_sleep_ms: 123,
|
||||||
|
pump_budget_yields: 11,
|
||||||
last_report: None,
|
last_report: None,
|
||||||
};
|
};
|
||||||
let json = uvc_stats_snapshot_json(&stats, 333_333);
|
let json = uvc_stats_snapshot_json(&stats, 333_333);
|
||||||
@ -407,6 +426,7 @@ mod uvc_binary {
|
|||||||
assert!(json.contains("\"last_rejected_oversize_cap\":300000"));
|
assert!(json.contains("\"last_rejected_oversize_cap\":300000"));
|
||||||
assert!(json.contains("\"paced_sleeps\":5"));
|
assert!(json.contains("\"paced_sleeps\":5"));
|
||||||
assert!(json.contains("\"paced_sleep_ms\":123"));
|
assert!(json.contains("\"paced_sleep_ms\":123"));
|
||||||
|
assert!(json.contains("\"pump_budget_yields\":11"));
|
||||||
|
|
||||||
let kernel_path = tempfile::NamedTempFile::new().expect("kernel stats");
|
let kernel_path = tempfile::NamedTempFile::new().expect("kernel stats");
|
||||||
fs::write(
|
fs::write(
|
||||||
|
|||||||
@ -152,6 +152,36 @@ fn synthetic_probe_keeps_bundled_network_ingress_and_rct_comparison_markers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synthetic_probe_deep_capture_accepts_its_device_source_mode() {
|
||||||
|
assert!(
|
||||||
|
PROBE_SRC.contains("if args.deep_capture and args.source != \"device\":")
|
||||||
|
&& PROBE_SRC.contains("--deep-capture requires --source device"),
|
||||||
|
"deep capture must accept the parser's UVC device source instead of requiring an unavailable source name"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!PROBE_SRC.contains("--deep-capture requires --source uvc"),
|
||||||
|
"the probe must not demand a source name its argument parser rejects"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synthetic_probe_capture_has_a_wall_clock_timeout() {
|
||||||
|
for expected in [
|
||||||
|
"timeout=max(5.0, args.duration + 10.0)",
|
||||||
|
"except subprocess.TimeoutExpired:",
|
||||||
|
"capture_timed_out = True",
|
||||||
|
"ffmpeg_rc = 124",
|
||||||
|
"raw_path.touch(exist_ok=True)",
|
||||||
|
"\"capture_timed_out\": capture_timed_out",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
PROBE_SRC.contains(expected),
|
||||||
|
"receiver capture must terminate and report evidence even when malformed UVC packets produce no decoded timeline: {expected}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn synthetic_injector_enters_the_public_bundled_media_rpc() {
|
fn synthetic_injector_enters_the_public_bundled_media_rpc() {
|
||||||
for expected in [
|
for expected in [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user