Skip to content

Start every hostile corpus item on its first variant, so a later run in one process reads as the first (#460) - #516

Merged
ramesh130 merged 2 commits into
mainfrom
issue-460-ios-corpus-repeated-runs
Oct 13, 2026
Merged

ramesh130 merged 2 commits into
mainfrom
issue-460-ios-corpus-repeated-runs

Conversation

@ramesh130

Copy link
Copy Markdown
Owner

Closes #460

Cause, with evidence

The cause is not a play request lost under load, and not a gap in the harness's teardown. AVFoundation picks a session's starting variant from a throughput history it keeps for the whole process, and we found no API that resets it. Every earlier corpus run on the fast loopback raises that history, until every ladder entry starts on its top rung. Started there, the severe ladder gap's top rung never plays.

Probes on sp-460 (iOS 27.1), since removed:

  • No teardown gap. Weak references (NSHashTable.weakObjects) to every corpus AVPlayer, AVPlayerItem and AVAsset showed zero still alive at the start of every iteration.
  • The starting rung moves within one process. The origin's request log, per iteration:
    • Iterations 1–3: every hls-ladder-gap entry asked for media.m3u8 (the bottom rung) first. Access log indicatedBitrate = 128000.
    • Iterations 4–6: every entry asked for media-high.m3u8 first.
    • On the top rung, benign and borderline played to the end. Severe fetched media-high.m3u8, segment-high0.aac and segment-high1.aac, then stopped at position 0 with an empty access log.
  • The rate drop is the item-end pause. A key-value observer on rate caught the severe player at rate = 0 with timeControlStatus == .playing and the item .readyToPlay, about 100 ms after play(). That is the shape AVFoundation's pause at an item's end has, not a lost request.
  • The severe top rung does not play in a fresh process either. A probe played each ladder entry's media-high.m3u8 directly, in a fresh process:
    • Benign and borderline played to 8 s.
    • Severe stayed at 0 with no item error, after logging AudioQueueObject.cpp Prime: failed (-50); will stop.
    • Its frames are 71 B × 48 = 3408 B. AAC's bit reservoir allows 6144 bits per channel, which is 1536 B for this stereo stream (ISO/IEC 14496-3). The ×4 frames are 284 B, which is why borderline plays.

So the "lost play()" was AVFoundation refusing media it was never obliged to decode. Whether the record met that refusal depended only on how many sessions the process had already played.

Reproduction (before the fix)

xcodebuild test-without-building -test-iterations 6 -only-testing:SuperPlayerTestkitTests/HostileManifestCorpusTests/testEveryGradedEntryIsPlayedAndItsBehaviourRecorded on sp-460 failed in 3 of 3 runs, always with "hls-ladder-gap (severe)": .neverStarts:

Run Failed iterations
1 5, 6
2 4, 5, 6
3 4, 5, 6

The deterministic form is the fresh-process probe above: started on the severe top rung, a stock AVPlayer never starts, every time.

Remedy, and why it keeps the record honest

observe sets AVPlayerItem.startsOnFirstEligibleVariant = true on every corpus item.

  • It is the start a fresh process takes. Every ladder entry started on its first variant in the first runs of each process. The setting makes later runs start where the first run does.
  • Nothing after the start changes. Any climb is still AVFoundation's choice, and no outcome is reinterpreted.
  • The table still records a stock player. This is a public property of a stock item, not a harness intervention. The table now records the same player on every run in a process.
  • It only affects the ladder rows. Only the hls-ladder-gap entries have more than one variant, so no other row can move.

The other options:

  • (a) Re-issue play(): rejected. The player did not lose a request; it refused the media. Re-issuing would not prime a rung the audio queue refuses. Worse, a harness that restarts a stopped player would hide exactly the kind of silent stop the record exists to show.
  • (b) A process per run: rejected. It buys the same start at the cost of an xcodebuild invocation per run, and it would leave the dependence on process state undocumented.
  • (c) "Valid for one run per process": rejected. The record would read differently depending on where its run fell in the process, with nothing to say so.

What else changed:

After the fix

  • The same command with -test-iterations 10: 10/10 passed, about 40 s each.
  • SP_SIMULATOR=sp-460 ios/check: passed. That is 710 tests on iOS, 12 on tvOS (sp-460-tv) and 38 in ios/MoQ, plus the demo build.
  • No device run. This is a test-harness change and the subject is the simulator's AVFoundation.

Review

Standards

Spec

  • Hard: none.
  • Soft, fixed: the "fourth run" wording; why (a), (b) and (c) were rejected now lives in tracked files (the ADR addendum and docs/testing.md); the comment says that only a multi-iteration run shows the effect; it names a climb inside the budget as the failure to expect.
  • Soft, skipped: a committed test pinning "the severe top rung, started on, never starts". It would put a second recorded observation beside the table, and the corpus media itself is the likely follow-up. The severe rung's frames exceed AAC's reservoir in the shared Kotlin and Swift generator, so that belongs to its own issue.

Not done, and why

🤖 Generated with Claude Code

https://claude.ai/code/session_01MJAhQmFgdRRAKUaMHibQLe

ramesh130 and others added 2 commits October 13, 2026 19:56
…un in a process reads as the first

AVFoundation picks a session's starting rung from a throughput history it keeps per process
and offers no way to reset. A few corpus runs on the loopback raise it until every ladder entry
starts on its top rung, and the severe ladder gap's top rung (frames past AAC's bit reservoir)
never primes: the "lost play()" of #460. A fresh process starts on the first variant listed, so
`startsOnFirstEligibleVariant` pins every run to that start and leaves the climb to AVFoundation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MJAhQmFgdRRAKUaMHibQLe
…the run counts as observed, and cite the reservoir

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MJAhQmFgdRRAKUaMHibQLe
@ramesh130
ramesh130 merged commit 8349f47 into main Oct 13, 2026
3 checks passed
@ramesh130
ramesh130 deleted the issue-460-ios-corpus-repeated-runs branch October 13, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: the hostile corpus record loses a play() after repeated runs in one process (hls-ladder-gap severe never starts)

1 participant