Split out of #68.
HTTPClient admits requests through AsyncSemaphore(limit: 6) (HTTPClient.swift:30). The semaphore is strictly FIFO: signal() wakes waiters.removeFirst(), and there is no notion of who is waiting.
Home's background passes are bounded but they are bounded at or above that limit. precomputeProviderCounts runs a 10 000-item scan and then resolves ~33 providers at maxConcurrent = 4, precomputeGenreCaches also at 4, and loadProviderBackdrops at 6. They start on a 3 s / 8 s / 13 s stagger and keep running while a library grid opens as a cover over Home.
So a user's tap can arrive at a saturated limiter and wait for permits held by .utility work that nobody is looking at. Task priority does not help here: the priority applies to the Swift task, not to the position in the semaphore queue.
Slimmer payloads (#68) shorten how long each background request holds a permit, but they do not give the tap precedence.
Worth considering:
- A second, small permit budget reserved for user-initiated requests, or a two-queue
AsyncSemaphore where signal() prefers the interactive waiter.
- Suspending the precompute passes while a cover is presented, which is cheaper but only covers the navigation case.
Not yet measured: how often the limiter is actually saturated at the moment of a tap. That should be established before picking between the two, since the second option is much smaller if navigation is the only case that matters.
Split out of #68.
HTTPClientadmits requests throughAsyncSemaphore(limit: 6)(HTTPClient.swift:30). The semaphore is strictly FIFO:signal()wakeswaiters.removeFirst(), and there is no notion of who is waiting.Home's background passes are bounded but they are bounded at or above that limit.
precomputeProviderCountsruns a 10 000-item scan and then resolves ~33 providers atmaxConcurrent = 4,precomputeGenreCachesalso at 4, andloadProviderBackdropsat 6. They start on a 3 s / 8 s / 13 s stagger and keep running while a library grid opens as a cover over Home.So a user's tap can arrive at a saturated limiter and wait for permits held by
.utilitywork that nobody is looking at. Task priority does not help here: the priority applies to the Swift task, not to the position in the semaphore queue.Slimmer payloads (#68) shorten how long each background request holds a permit, but they do not give the tap precedence.
Worth considering:
AsyncSemaphorewheresignal()prefers the interactive waiter.Not yet measured: how often the limiter is actually saturated at the moment of a tap. That should be established before picking between the two, since the second option is much smaller if navigation is the only case that matters.