Repository navigation
Harden six tests that failed on loaded machines - #127
Merged
Merged
Conversation
The fake iPad stamped each datagram with the wall clock when it read it. A test thread descheduled past a repair deadline read the repair late, and Reassembler::add_media ticks before inserting, so the frame expired with its repair already in the socket buffer. Each pass of the receive loop now advances the iPad's clock by at most 20 ms. A pass waits at most 1 ms on macOS and 2 ms elsewhere (about 16 ms with Windows' timer), so on a machine with time to spare this is the wall clock, and a repair the host sends late is still late. The capture clock was tried and is wrong the other way: draining a backlog moves it hundreds of ms at once, past the deadlines of the gaps the backlog shows (11-28 requests per run against base's 0 under injected 300 ms receiver stalls). The failure seen in a full workspace run on devbox1 was the host itself: at load ~590 its sender went silent for 348 ms and 605 ms with frames waiting for repairs, and the fake iPad asked for two keyframes, as the assembler specifies once the 100 ms stall allowance is spent. Each silence of the PC longer than that allowance while an incomplete frame waits may now cost one request; without one the bound stays one. A slow encoder's quiet gaps between whole frames do not count.
On a starved machine libx265 stayed silent past the supervisor's 3 s encoder watchdog, the pipeline restarted, and the restarted stream numbered its pictures from 1 again: "HEVC counters must advance: [39, 1]" on mainpc, reproduced here with ETERNAL_FAULT_ENCODER_AFTER. Counters must now rise within each epoch, and every epoch after the handoff must start with an HEVC IRAP carrying parameter sets and the keyframe flag, which the test did not check for a restarted stream before. The reassembler's 100 ms expiry runs on the stream's capture clock, like the pairing and preferred-FPS tests, and the 30 s wait is a 120 s backstop: at load ~545 libx265 took 9 s for its first picture and made 18 in 30 s.
The recovery gap started at the first 150 ms read timeout after 30 decoded frames. The fault fires at encoder frame 45, so on a machine slow enough to pause 150 ms between frames the gap also counted the 15 frames streamed before the crash. It now runs from the first epoch's last datagram to the second epoch's first, the freeze a user sees, and the 5 s bound is unchanged. The reassembler's 100 ms expiry runs on the stream's capture clock.
The test waited 300 ms for a silent site and required the answer within 450 ms. It now waits 1 s and requires it within 1.5 s: still at the deadline rather than at a later limit (the 3 s FAILURE_CHECK, ureq's 10 s connect timeout) or after a second wait as long as the first, with 500 ms instead of 150 ms for the return to be scheduled.
Lossy: a host silence is excused only while the encoder also stood still with no finished frames waiting for the sender, and silences add up per waiting frame the way the iPad's stall allowance does. At most three such stall episodes are excused in a run. HEVC: at most one restart, the encoder watchdog's, may start a second epoch, and the last epoch alone must decode the frames.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hardens six tests that failed on loaded machines or CI runners without anything being wrong in the product. Each fix keeps what the test proves, and the injected regressions below still fail. Load figures are devbox1's 1-minute load average (10 cores, shared with other builds) at the start of each run.
lossy_stream_recovers_and_adapts: the keyframe-storm assertionWhat failed: one run of a full
cargo test --workspaceon devbox1 got 2 keyframe requests against a bound of 1.Cause: the test, not a change in #116, #118, #119 or #123. Those PRs didn't touch this test's UDP keyframe path; #118's hold and larger pending window make requests rarer. The failed run's own trace shows the host's sender silent for 348 ms, then 605 ms, at load ~590. Each time a frame was waiting for its repair, the repairs arrived late, and the fake iPad gave up once the 100 ms stall allowance was spent, as the assembler specifies.
There is a second, test-only way to fail. The fake iPad stamped each datagram with the wall clock when it read it, and
Reassembler::add_mediaticks before inserting. So a test thread descheduled past a deadline expired a frame whose repair was already in its socket buffer.Fix (
host/tests/e2e_synthetic.rs):encode_frame_countandtransport_packets_sentfrom the pipeline stats). A sender that stops on NACKs while the encoder keeps going builds up a backlog, its four-frame channel full plus one frame held, so its silences never count.MAX_STALL_ALLOWANCEdoes. An episode is a frame whose starved silences pass the 100 ms allowance, so two 60–90 ms silences on one frame count too. The previous version counted only a single silence over 100 ms.Reassembler::waiting_frames()(proto/src/reassembly.rs) is a new read-only accessor for the frames still missing fragments. Nothing else in the reassembler changed.Injected regressions (temporary patches, not in this PR; a 64-deep decode queue in these runs so only the storm bound decides):
send_repairson 1 NACK in 15, encoder running: the new test fails, 3 requests and 0 stall episodes. The previous version of this PR passed the same injection: 3 requests ≤ 1 + 49 silences.Loops: 30/30 passed at load 28–695 with 24 extra
yesloops running. Every run had 0 requests; 2 runs counted one stall episode. After merging the candidate, 5/5 more passed at load 144–169.hevc_stream_negotiates_and_decodesWhat failed: on mainpc under load,
HEVC counters must advance: [39, 1].Cause: the known 3 s encoder-watchdog limitation. libx265 starved past it, the supervisor restarted the pipeline, and the new epoch numbers its pictures from 1 again.
Fix:
encoder silent for …). The test reads that reason from the supervisor's "automatic restart scheduled" log line.WANT_DECODED_FRAMES. Short epochs no longer add up.Injected faults (temporary patches):
[3, 4]and restart reason "injected repeated encoder fault". The previous version passed it with epochs[3, 4, 5, 6].[1, 2].Loops: 30/30 passed at load 720–796 with 24 extra
yesloops, every run a single epoch. After the merge, 5/5 more passed.Reassembler clock in the HEVC and encoder-crash tests
Both tests used to give
Reassembler::add_fragmentthe wall-clock read time. They now give itstream_time(origin, &header), the host's capture time for the frame (e2e_synthetic.rs, in each test's media branch). So the 100 ms expiry runs on the capture clock, as it already does in the pairing and preferred-FPS tests. A frame expires only once frames captured 100 ms after it arrive, never because the test thread was descheduled between two of its fragments. Neither test negotiates repair, so the repair-mode trap above doesn't apply.encoder_crash_auto_restarts_and_stream_recoversWhat failed: once at load ~590. The failure output wasn't kept, and 10 base runs at load ~700 passed.
Cause found by reading the test: the recovery gap started at the first 150 ms read timeout after 30 decoded frames. The fault fires at encoder frame 45, so on a machine slow enough to pause 150 ms between frames, the gap also counted the 15 frames streamed before the crash.
Fix: the gap now runs from the first epoch's last datagram to the second epoch's first, which is the freeze a user sees. The 5 s bound is unchanged.
Loops: 30/30 passed at load 22–1017. Each run had exactly one restart, for the injected fault.
updates::tests::the_failure_message_check_is_validated_and_boundedCause: the test required a 300 ms deadline to return within 450 ms. That leaves 150 ms of scheduling slack, which is tight on a loaded Windows runner.
Fix (
host/src/updates/tests.rs): a 1 s deadline that must return within 1.3 s. That still rules out returning early, returning at a later limit (the 3 sFAILURE_CHECK, ureq's 10 s connect timeout), or a second wait as long as the first.Loops: 30/30 passed at load 796–819; each run took 1.01 s.
AudioStreamTests(System E2E)What failed: run 40840994468 on
release/0.3.4-candidate(which includes #113),AudioStreamTests.swift:23: Disconnect did not appear within 5 s.Cause: a race between the test and the display controls' 5 s auto-hide. Neither the app nor the simulator was late; the launch and the first "PC audio playing" took the usual time.
Fix (
ios/EternalMonitorUITests/AudioStreamTests.swift):waitForHUD's loop is nowwaitForControl. Until a 10 s deadline, it reveals the controls if the element is gone, then waits up to 2 s for the condition. The Disconnect check and the "Fill the screen" check, which had the same pattern, use it. The test still requires the session's Disconnect control and the pinned Fit setting after the live mute. The 5 s in the old wait was only a test timeout; no user-facing guarantee depends on it.Run:
scripts/test_ios.sh -only-testing:EternalMonitorUITests/AudioStreamTestspassed on Xcode 16.4 with an iOS 18.6 iPad Pro 11 (M4) simulator, CI's pairing, at load 812–899.CI:
sudo mdutil -a -i offWhat failed: "Prepare the disposable streaming runner" in System E2E. This happened in 2 of about 140 runs (40910597311 and 40597737798, first attempts).
/and/System/Volumes/Datawere switched off, but/System/Volumes/PrebootreturnedError: unexpected indexing state. kMDConfigSearchLevelTransitioning, so the step exited 1.Fix (
.github/workflows/ci.yml): the command is retried up to five times, 5 s apart. A retry rather thancontinue-on-error, because:continue-on-errorwould hide that failing too;Checks
On devbox1 at 1782fe9. The later merge of
release/0.3.4-candidateat 791625c (#125) brings only workflow, installer and docs files.cargo fmt --checkpasses.cargo clippy --workspace --all-targets -- -D warningspasses.cargo test --workspacepasses: 835 tests, 0 failed.