Repository navigation
refactor(moq-mux): TS stats count a shared PID once - #7294
Conversation
Quest-Claim: a2b17bb644456b04
Programs reassembles each listed PMT PID once and counts its bad CRCs there. Every other count of a PID comes from one program at a time; when it stops reading the PID, the next reader takes over its TR 101 290 intervals and counts on from the total it leaves. This replaces the per-program merge, the retired-beyond-running subtraction, and the read/began CRC bookkeeping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PID's row and PTS cadence come from a program carrying it, its PCR checks from one taking its clock there, and its PMT interval from one reading its PMT there, so a PID one program clocks and another carries keeps both. A PMT moves counts as the program stood before it, so the tail a dropped route flushes, or a refusal on the way to a PID, counts once. Handed-over timers are signed, so an interval that began before the successor's clock keeps its age. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pre-PMT snapshot also takes the program's TR 101 290 timers and clock, so a PTS interval running when a PMT drops the stream carries on in the program still carrying it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 28 minutes. View limit details
No actionable comments were generated in the recent review.
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: d6c3cacc4b67ad22aa934726e4be3fa1fe6e2d82.
P2 — A stalled owner's clock hides shared-PID timing errors (rs/moq-mux/src/container/ts/programs.rs:290–299, 827–837). Two programs can carry the same elementary PID while using different PCR PIDs. After both observe its last PTS, stop the first program's PCR but keep the second clock advancing and both programs listed. counter permanently retains the first carrier, whose Health::tick is now frozen, so the multiplex reports zero PTS errors even after the second program has counted the outage. The previous max-merger retained those errors. Use a timing source/ownership policy that remains valid when one program clock stalls, preserving the accumulated baseline, and add a differential regression with distinct PCR PIDs and a >700 ms shared-PTS outage.
Direction: separating Stream/Clock/Table ownership and centralizing PMT CRC assembly removes substantial merge/subtract complexity; pre-PMT snapshots and signed timer offsets address real handover hazards. However, sharing a PID does not imply that its readers' program clocks advance alike.
Verification limits: GitHub-only static review of ownership, retirement, PMT assembly, timer transfer and regressions, with an independent pass. No tests or benchmarks were run; reported passes and performance measurements were not reproduced.
|
Re: P2 — A stalled owner's clock hides shared-PID timing errors Decision (2026-10-31, taken overnight on the maintainer's standing authorization): merge as is, and plan the root-cause fix as a follow-up quest. Here is why this doesn't block the merge:
The real fix is TR 101 290 timing that keeps working when a PCR stalls, for single and shared programs alike (for example by falling back to the multiplex clock). That needs a timing-source change across (Written by Claude Opus 5.5) |
|
quest land removed this pull request from the queue: the batch check failed (exit 101). js: selecting docs
..js: selecting reader
...
----------------------------------------------------------------------
Ran 24 tests in 0.382s
OK
python3 sh/js/audit.test.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.069s
OK
python3 sh/rs/test-lints.test.py
.
----------------------------------------------------------------------
Ran 1 test in 2.539s
OK
python3 sh/gh/nightly.test.py
..............
----------------------------------------------------------------------
Ran 14 tests in 1.181s
OK
python3 sh/gh/pages-clean.test.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.052s
OK
python3 sh/gh/bench-report.test.py
............
----------------------------------------------------------------------
Ran 12 tests in 0.440s
OK
bench-ffi: 2 measured entries
bench-rust: failure; no history point
bench-ffi: failure; no history point
bench-rust: failure; no history point
no completed benchmark reports; no history point
bench-rust: 1 measured entries
bench-rust: failure; no history point
no completed benchmark reports; no history point
Paired comparison of c2 against c1: 1 confirmed regressions, 1 targets not compared
- `rust/kio/lock/4`: +40.0% (rounds +30.0%, +40.0%, +50.0%)
- `rust/gst/moq-gst/catalog` not compared: base build failed
Paired comparison of c2 against c1: 0 confirmed regressions, 0 targets not compared
python3 sh/gh/ts-paths.test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK
sh/go/publish-wrapper.test.sh
publish-wrapper test: no rsync on PATH, skipping
bash .claude/hooks/direnv.test.sh
direnv hook: could not load the dev shell, see the direnv output above.
direnv hook: regression tests passed
python3 sh/rs/uring.test.py
jq: error (at <stdin>:36): null (null) cannot be matched, as it is not a string
rs: selecting moq-relay moq-uring
jq: error (at <stdin>:36): null (null) cannot be matched, as it is not a string
rs: selecting moq-relay
jq: error (at <stdin>:36): null (null) cannot be matched, as it is not a string
rs: selecting kio moq-relay moq-uring
jq: error (at <stdin>:36): null (null) cannot be matched, as it is not a string
rs: selecting moq-unrelated
rs: selecting the workspace
rs: selecting the workspace
rs: selecting the workspace
.
----------------------------------------------------------------------
Ran 1 test in 0.280s
OK
ci: check rs failed
ci: check rs_shapes failed
ci: seconds per module
75 check rs (failed)
77 check rs_shapes (failed)
278 check js
52 test js
13 check markdown
2 check gh
46 test gh
68 check flake
0 check drill
3 check quest
0 check ts_paths
14 check shell
0 check large_files
1 check toml
0 check nix
0 check justfile
392 wall time
error: recipe `ci` failed on line 104 with exit code 101
. |
|
Codex land fixer: @moq/p2p:build | ✍️ Rewriting package.json... @moq/p2p:build | 🔗 Converted @moq/qmux: workspace:^ → ^0.3.3 @moq/p2p:build | 📄 Copying README.md... @moq/p2p:build | 🔍 Running publint... @moq/p2p:build | 📦 Package built successfully in dist/ @moq/p2p:build | Done in 238ms @moq/pattern:build | ✍️ Rewriting package.json... @moq/pattern:build | 📄 Copying README.md... @moq/pattern:build | 🔍 Running publint... @moq/pattern:build | 📦 Package built successfully in dist/ @moq/pattern:build | ✍️ Generating jsr.json... @moq/pattern:build | 📦 jsr.json written @moq/pattern:build | Done in 233ms @moq/publish:build | �[36mvite v8.3.4 �[32mbuilding client environment for production...�[36m�[39m @moq/publish:build | transforming... @moq/publish:build | ✓ 139 modules transformed. @moq/publish:build | rendering chunks... @moq/publish:build | computing gzip size... @moq/publish:build | dist/assets/capture-worker.js 0.64 kB @moq/publish:build | dist/assets/capture-worklet.js 1.37 kB @moq/publish:build | dist/capture-worker-BJ0xkcGF.js 0.81 kB │ gzip: 0.50 kB │ map: 1.00 kB @moq/publish:build | dist/index.js 1.06 kB │ gzip: 0.54 kB │ map: 1.57 kB @moq/publish:build | dist/support/index.js 1.16 kB │ gzip: 0.65 kB │ map: 4.26 kB @moq/publish:build | dist/support/element.js 1.36 kB │ gzip: 0.54 kB │ map: 3.20 kB @moq/publish:build | dist/capture-worklet-CRiEEPvQ.js 1.54 kB │ gzip: 0.70 kB │ map: 1.74 kB @moq/publish:build | dist/video-CKv257p3.js 4.83 kB │ gzip: 1.94 kB │ map: 19.44 kB @moq/publish:build | dist/microphone-BIV2nutQ.js 8.09 kB │ gzip: 2.43 kB │ map: 35.77 kB @moq/publish:build | dist/element.js 10.88 kB │ gzip: 3.19 kB │ map: 40.35 kB @moq/publish:build | dist/ui/element.js 33.96 kB │ gzip: 8.52 kB │ map: 54.90 kB @moq/publish:build | dist/encoder-m79Iaxwg.js 58.93 kB │ gzip: 17.31 kB │ map: 277.56 kB @moq/publish:build | dist/src-BlFDRcol.js 483.22 kB │ gzip: 111.10 kB │ map: 1,872.47 kB @moq/publish:build | @moq/publish:build | �[32m✓ built in 230ms�[39m @moq/publish:build | ✍️ Rewriting package.json... @moq/publish:build | 🔗 Converted @moq/clock: workspace:^ → ^0.1.0 @moq/publish:build | 🔗 Converted @moq/hang: workspace:^ → ^0.5.2 @moq/publish:build | 🔗 Converted @moq/json: workspace:^ → ^0.4.2 @moq/publish:build | 🔗 Converted @moq/net: workspace:^ → ^0.4.2 @moq/publish:build | 🔗 Converted @moq/signals: workspace:^ → ^0.2.5 @moq/publish:build | 🔗 Converted @moq/ui: workspace:^ → ^0.1.0 @moq/publish:build | 🔗 Converted @moq/video: workspace:^ → ^0.1.0 @moq/publish:build | 📄 Copying README.md... @moq/publish:build | 🔍 Running publint... @moq/publish:build | 📦 Package built successfully in dist/ @moq/publish:build | Done in 1.28s @moq/qmux:build | ✍️ Rewriting package.json... @moq/qmux:build | 🔗 Converted @moq/clock: workspace:^ → ^0.1.0 @moq/qmux:build | 📄 Copying README.md... @moq/qmux:build | 🔍 Running publint... @moq/qmux:build | 📦 Package built successfully in dist/ @moq/qmux:build | ✍️ Generating jsr.json... @moq/qmux:build | 📦 jsr.json written @moq/qmux:build | Done in 972ms @moq/room:build | ✍️ Rewriting package.json... @moq/room:build | 🔗 Converted @moq/clock: workspace:^ → ^0.1.0 @moq/room:build | 🔗 Converted @moq/hang: workspace:^ → ^0.5.2 @moq/room:build | 🔗 Converted @moq/json: workspace:^ → ^0.4.2 @moq/room:build | 🔗 Converted @moq/net: workspace:^ → ^0.4.2 @moq/room:build | 🔗 Converted @moq/publish: workspace:^ → ^0.5.2 @moq/room:build | 🔗 Converted @moq/signals: workspace:^ → ^0.2.5 @moq/room:build | 🔗 Converted @moq/watch: workspace:^ → ^0.6.2 @moq/room:build | 📄 Copying README.md... @moq/room:build | 🔍 Running publint... @moq/room:build | 📦 Package built successfully in dist/ @moq/room:build | ✍️ Generating jsr.json... @moq/room:build | 📦 jsr.json written @moq/room:build | Done in 1.50s @moq/signals:build | ✍️ Rewriting package.json... @moq/signals:build | 🔗 Converted @moq/clock: workspace:^ → ^0.1.0 @moq/signals:build | 📄 Copying README.md... @moq/signals:build | 🔍 Running publint... @moq/signals:build | 📦 Package built successfully in dist/ @moq/signals:build | ✍️ Generating jsr.json... @moq/signals:build | 📦 jsr.json written @moq/signals:build | Done in 627ms @moq/ui:build | �[36mvite v8.3.4 �[32mbuilding client environment for production...�[36m�[39m @moq/ui:build | transforming... @moq/ui:build | ✓ 6 modules transformed. @moq/ui:build | rendering chunks... @moq/ui:build | computing gzip size... @moq/ui:build | dist/index.js 7.90 kB │ gzip: 3.00 kB │ map: 23.92 kB @moq/ui:build | @moq/ui:build | �[32m✓ built in 24ms�[39m @moq/ui:build | ✍️ Rewriting package.json... @moq/ui:build | 🔗 Converted @moq/signals: workspace:^ → ^0.2.5 @moq/ui:build | 📄 Copying README.md... @moq/ui:build | 🔍 Running publint... @moq/ui:build | 📦 Package built successfully in dist/ @moq/ui:build | Done in 625ms @moq/video:build | ✍️ Rewriting package.json... @moq/video:build | 🔗 Converted @moq/signals: workspace:^ → ^0.2.5 @moq/video:build | 📄 Copying README.md... @moq/video:build | 🔍 Running publint... @moq/video:build | 📦 Package built successfully in dist/ @moq/video:build | ✍️ Generating jsr.json... @moq/video:build | 📦 jsr.json written @moq/video:build | Done in 336ms @moq/watch:build | �[36mvite v8.3.4 �[32mbuilding client environment for production...�[36m�[39m @moq/watch:build | transforming... @moq/watch:build |quest: fixer agent exited 101 (written by Codex) |
|
quest land removed this pull request from the queue: the batch check failed (exit 101). web 1.1.1 (1.1.2 available)
Got dependencies!
14 packages have newer versions incompatible with dependency constraints.
Try `dart pub outdated` for more information.
Resolving dependencies...
Downloading packages...
_fe_analyzer_shared 105.0.0 (111.0.0 available)
analyzer 14.1.0 (14.7.0 available)
code_assets 2.0.0 (2.1.0 available)
coverage 1.15.1 (1.16.0 available)
crypto 3.0.7 (3.0.8 available)
frontend_server_client 4.0.0 (4.2.0 available)
http_multi_server 3.2.2 (3.2.3 available)
! moq_ffi 0.0.0-dev from path ../moq_ffi (overridden)
shelf 1.4.2 (1.4.3 available)
shelf_static 1.1.3 (1.2.0 available)
test 1.32.0 (1.33.0 available)
test_api 0.7.14 (0.7.15 available)
test_core 0.6.20 (0.6.21 available)
vm_service 15.3.0 (15.4.0 available)
web 1.1.1 (1.1.2 available)
Got dependencies!
14 packages have newer versions incompatible with dependency constraints.
Try `dart pub outdated` for more information.
Publishing moq 0.1.0 to https://pub.dev:
├── CHANGELOG.md (<1 KB)
├── LICENSE (1 KB)
├── MIGRATE.md (16 KB)
├── README.md (1 KB)
├── analysis_options.yaml (<1 KB)
├── bench
│ └── ffi.dart (5 KB)
├── lib
│ ├── audio.dart (2 KB)
│ ├── json.dart (2 KB)
│ ├── media.dart (4 KB)
│ ├── moq.dart (<1 KB)
│ ├── src
│ │ ├── aliases.dart (7 KB)
│ │ ├── client.dart (10 KB)
│ │ ├── durations.dart (<1 KB)
│ │ ├── epoch.dart (<1 KB)
│ │ ├── server.dart (5 KB)
│ │ ├── stream.dart (2 KB)
│ │ └── version.dart (<1 KB)
│ └── video.dart (3 KB)
├── pubspec.yaml (<1 KB)
└── test
├── cancellation_test.dart (7 KB)
├── docs
│ ├── prelude.dart (<1 KB)
│ └── samples.dart (4 KB)
└── moq_test.dart (24 KB)
Total compressed archive size: 30 KB.
Validating package...
Package validation found the following hint:
* Non-dev dependencies are overridden in pubspec.yaml.
This indicates you are not testing your package against the same versions of its
dependencies that users will have when they use it.
This might be necessary for packages with cyclic dependencies.
Please be extra careful when publishing.
The server may enforce additional checks.
Package has 0 warnings and 1 hint.
ci: test rs failed
ci: check rs_shapes failed
ci: seconds per module
141 check py
74 test py
511 check rs
306 test rs (failed)
840 check rs_shapes (failed)
231 check c
208 check cpp
21 check obs_compile
240 check js
57 test js
35 check markdown
4 check gh
122 test gh
158 check kt
78 check flake
0 check swift
304 check go
1 check drill
2 check quest
13 check obs
9 check ts_paths
24 check shell
3 check large_files
3 check toml
4 check nix
0 check justfile
230 check dart
1888 wall time
error: recipe `ci` failed on line 106 with exit code 101
. |
|
codex / Codex land fixer: (written by codex / Codex) |
Finishes quest/m1/ts-reliability/shared-pid-stats.md and deletes it.
Problem
Under
--program all, each program'sImportcounts the PIDs it reads, andPrograms::statsmerged those copies: the larger count on each shared PID, then whatever a retiring program had counted beyond the programs still running. Most of the review churn on moq-dev/moq#6689 came from that merge-and-subtract logic.It also left a timing gap. A program that starts reading a shared PID mid-outage never sees the PTS that began the outage, so it has no PTS timer there. Once the earlier reader retires, the outage stops counting
PTS_error.Approach
The shared parser counts each PID once:
Programsnow reassembles every PMT PID the latest PAT lists, once for all programs, and counts bad CRCs there. That removes the per-programcrc_errorsmerge and fix(moq-mux): TS programs follow the PAT as it changes #6689'sread/began/resumedbookkeeping indescribe, along withImport::assembling_pmtandSectionReassembler::{resumed, assembling}.Tallyper PID and per way the PID is read (health::Checks). Each tally takes its count from one program at a time:Stream: the row and the PTS cadence, from a program carrying the PID (or else one reading it), plus the PAT interval on PID 0.Clock: the PCR checks, from a program taking its clock there.Table: the PMT interval, from a program reading its PMT there.settleorretirehands the tally to the earliest running program that still reads it so. The next program counts on from the total the last one left, and takes over the TR 101 290 intervals it was timing (Health::hand_over). The handed-over timers are signed, so an interval that began before the next program's clock started keeps its age.Programssnapshots a program's stats and timers before routing it a packet on its PMT PID. So a picture that a dropped route flushes, while another program still holds it, counts once, as fix(moq-mux): TS programs follow the PAT as it changes #6689 already did for a retirement's finish. A refusal counted on the way to a PID counts too.stats()sums the tallies, and no longer rebuilds totals from per-program copies.Counts::{merge, add, beyond}and theretiredtotals are gone.#6689's outcomes stand, and their tests pass unchanged: a retired program's final picture still counts, a missing PMT still refuses after 0.5 s of program clock, and an unfinished section on a PMT PID still counts its bad CRC once.
Choices made without asking
Streamtally prefers a program carrying the PID over one only reading it (as a clock or a PMT), so a delivered stream's row comes from a program that decodes it.ts_programsmeasures now adds one stats snapshot per PMT packet routed to a program, plus a PID-indexed PMT flag in place of!waiting.is_empty(). Back-to-back runs againstmainstay within about ±4%, with no consistent direction (for example4/4mbpsread 1892 µs vs 1969 µs, then 1932 µs vs 1860 µs).stats()and retirement are not in the bench.stats()now reads only the programs that own a tally, where it used to read every program.Tests
New tests in
programs::test. Each fails under the mutation named:a_pts_outage_counts_on_across_a_handover: a PTS outage on a shared PID reads 2PTS_errorwhether the first program stays, retires, or drops the PID. The second program starts 0.5 s after the last PTS. With no handover, or with the pre-PMT timers ignored, it reads 0 where 2 is expected. With the carried timer clamped at the next program's clock origin, it also fails.a_pid_read_as_a_clock_and_a_stream_counts_both: program 1 takes its clock from 0x61 and program 2 carries it. The row counts program 2's 8 frames, andpcr_repetition_errorcounts program 1's gap. It fails without the carrier preference.a_picture_a_dropped_route_flushed_counts_once: program 1's PMT drops a shared H.264 PID while program 2 holds the same picture, and the picture counts once. It fails without the pre-PMT reading.a_stream_refused_without_a_pcr_counts_once: a section stream refused for want of a PCR keepsrefusedat 1 while both programs run and after one leaves. It fails when a program's counts start from its reading after the PMT.just checkpasses, with 4227 Rust tests.API
No public API or wire change.
ts::stats::Snapshotkeeps its shape.Conflicts
moq-dev/moq#7293 (restart-catalog) also edits
container/ts/import.rsandquest/m1/ts-reliability/README.md.git merge-treeagainst its head merges cleanly.Follow-ups
Decisions
main. This PR makes a shared PID behave like that single-program case, and a stream shared by two programs should be timed by one clock, so a stalled PCR is an input fault in its own right. The root-cause fix belongs in a follow-up: timing that survives a stalled PCR, for single and shared programs alike. Reply: refactor(moq-mux): TS stats count a shared PID once #7294 (comment)Review
Codex (
quest review, codex gpt-6.1-sol, medium effort), session01a18bc6-98cb-7e40-8e47-c2421114afff, base9edd0e4b90:ea1dbe0a00, 3 findings, all fixed:Checkstallies and a carrier preference.5b73bd9607, 1 finding, fixed: preserve shared PTS timing when a PMT removes the owner's route. The pre-PMT snapshot now carries the timers.d6c3cacc4b: no findings.Head
fa7b313520mergesmainwith no hand-resolved conflict (quest's merge driver took the link-list edit), so it needs no new review.just check origin/mainpasses on it, with 4247 Rust tests.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code