Repository navigation
Description
Symptom (Apple Vision Pro, on power)
Downloads stall or crawl after the headset has been off, and the problem goes back to before the downloads overhaul. It was first noticed around 1.7.2 (build 1501) and reproduced on 1.8.0 (build 1523).
- After the headset is taken off and put back on, the running downloads show no progress immediately afterwards.
- Two downloads that were already running crawl at about 100 KB/s each.
- Pausing one lets a waiting row start, and that row ramps up to 4–6 MB/s within about 20 s, while the other old row stays slow.
What the build-1523 diagnostic report shows
- The slowness is real, not a display error. Each slow task's own byte counter advances at only 55–62 KB/s, so the row speed readout is accurate. Downloads: trustworthy aggregate speed after headset sleep (#379) #406 (Downloads: make aggregate speed trustworthy after headset sleep #379) makes the aggregate speed header honest, but does not change transfer speed.
- The fast row is not a new download. It is a paused row with a 1 GiB checkpoint, resumed with new range tasks on the same background session, the same server and the same HTTP/2 path. The old row's tasks stay slow beside it. This points to task-level scheduling or connection state, not a bandwidth limit or the range-resume path itself.
- Not the cause: session configuration (
isDiscretionary = falseandsessionSendsLaunchEvents = trueon bothmainand the overhaul), task priority (default), checkpoint copies (finished promptly) and append size (4 MiB blocks). - Missing from the report: the retained window has no foreground, background-wake or wake-expiry events, so it cannot date the doff/don, and the rows carry no record of how their tasks were created.
Leading hypothesis
Range tasks created while the app is in the background (for example, the next segment created during an off-head wake) are treated as discretionary by the system, whatever the session flag says, and may stay deprioritised after the app returns to the foreground. The app's foreground reconciliation, on both main and the overhaul, checks only whether a tracked task exists. It counts every tracked task as live, so automatic restart skips those rows. Nothing checks whether a surviving task is suspended or making progress.
On the overhaul, P6 expired-wake fencing (BackgroundDownloadWakeCoordinator.isWindingDown includes expiredOperations) may add delay before refill. It is not the historical cause.
Proposed work
- Instrumentation first. Add AppDiagnostics events for task creation, foreground health and reissue, recording for each attempt:
- session kind and the scene state when the task was created;
- task state and priority;
- byte-counter deltas;
- durable offset and range length;
- server route and response status;
- wake-expiry and expired-operation counts.
- A bounded foreground task-health pass. On foreground:
- reconcile actual task IDs against tracked ownership;
- resume tasks that are legitimately suspended, after durability prerequisites are met;
- pump admission;
- sample byte deltas for 30–60 s.
For a task confirmed stalled, reissue its range once, fenced, on the same session, restarting from the durable checkpoint. Do not use producing resume data: closed-range resume blobs are deliberately rejected.
- Then evaluate. Decide whether a default (foreground) session lane is worth it as a controlled fallback. That needs session-qualified task identities and a safe background handoff.
Repro for the instrumented build
- Use two downloads of finished static versions.
- Leave them running 60 s with the headset on.
- Take off the headset, still on power, for 10 minutes.
- Put it back on, bring Labstream to the foreground and leave it untouched for 60 s.
- Export the diagnostic report immediately.
- Pause and resume the same slow row, then export again.
If the slowness persists, a sysdiagnose would show how the system daemon is scheduling the tasks.
Related: #379 / #406 (speed readout), #403 (overhaul acceptance).
Activity
jlipworth commentedon Oct 18, 2026
Why it lasts so long: static range tasks request closed 512 MiB segments, two per row. A task that stays deprioritised keeps its whole segment, and at about 60 KB/s a 512 MiB segment takes roughly 2.5 hours. The app only creates a replacement task when the segment completes, so the slowness persists for hours instead of clearing after a resume hiccup. The report also rules out a resume-time pause: bytes kept arriving the whole time, just slowly, and the resumed row's 1 GiB checkpoint copy began and ended in the same second.
Option to evaluate alongside the foreground task-health pass: smaller segments, such as 32–64 MiB. Even with no other change, a deprioritised task would be replaced within minutes. Costs to measure: more requests and more frequent checkpoint appends (B85 write amplification).
jlipworth commentedon Oct 18, 2026
The maintainer also saw the same very low per-download throughput on iOS with 1.8.0 (build 1523), so this is not specific to visionOS. That is consistent with the hypothesis: on iOS, background URLSession tasks created while the app is backgrounded are discretionary. The foreground health pass and reissue live in the shared
BackgroundDownloadSession, so they apply to iOS/iPadOS and macOS as well. Device acceptance should include an iPhone run: lock the phone or background Labstream for at least 10 minutes with two static downloads running, then foreground it and leave it for 2 minutes before exporting diagnostics.Add bounded foreground range reissue (Refs #409)
Fix foreground range recovery races and result timing (Refs #409)
Fix train coverage and retry deferred foreground refills (Refs #409)
Retain deferred static range refill authority (Refs #409)
Retire foreground refill intents once their attempt is terminal (Refs #…
jlipworth commentedon Oct 18, 2026
Included in 1.8.0 (build 1537), shipped to TestFlight internal from
downloads/overhaul@ 12466ea. Device acceptance is pending; the issue stays open until then.jlipworth commentedon Nov 5, 2026
Landed on
mainin #425 (0b7f3fcb, tagv1.8.0). Any device or live acceptance gate already noted here still applies; the issue stays open until it's checked.