Summary
Thread Performance Checker reports a priority inversion on the software playback path: the demux consumer (.userInitiated) blocks on an NSCondition waiting for the read-ahead producer (.utility) to deliver packets.
Thread running at User-initiated quality-of-service class waiting on a lower QoS
thread running at Utility quality-of-service class. Investigate ways to avoid
priority inversions
Sources/AetherEngine/Native/SoftwarePacketReadAhead.swift:231
The two sides
Producer — SoftwarePacketReadAhead.swift:38:
private let worker = DispatchQueue(label: "engine.sw.packet-prefetch", qos: .utility)
Consumer — SoftwarePlaybackHost.swift:120, which calls readAhead.read(...) at SoftwarePlaybackHost.swift:2336:
private let demuxQueue = DispatchQueue(label: "engine.sw.demux", qos: .userInitiated)
The block is the wait loop at SoftwarePacketReadAhead.swift:231:
while count == 0, !ended, failure == nil, !closed, !seeking,
token == generation, isCurrent() {
condition.wait()
}
NSCondition does not donate the waiter's priority, so whenever the FIFO runs dry the .userInitiated consumer is gated on a .utility producer with no priority boost.
When it shows up
Reproducible whenever reads are slow enough for the buffer to empty — for us that is 1080p MKV streamed over WebDAV, where the producer is network-bound rather than CPU-bound. It fires around startup and after seeks, i.e. exactly when the buffer is empty by construction.
The prefetch queue is genuinely elective while the FIFO has depth, so .utility is the right resting state; the inversion is only about the window where the consumer is actually blocked on it.
Possible directions
Whichever fits the design best:
- Raise the read-ahead worker to
.userInitiated — simplest, at the cost of the elective-work property when the FIFO is full.
- Keep
.utility at rest and boost only while a consumer is blocked: dispatch produce() as a DispatchWorkItem and have the blocked consumer wait() on it, which propagates the waiter's QoS, instead of waiting on the bare condition.
- Re-dispatch the pending
produce() at the consumer's QoS when the wait loop is about to be entered with count == 0.
Option 2 keeps the intent of #27 (the elective decode must yield to real-time playback) while removing the inversion in the one window where the work is not elective any more.
Environment
- AetherEngine 6.76.0
- iPhone 13 Pro / iOS 26.5, Debug build with Thread Performance Checker enabled
- Software path, remote source over HTTP (WebDAV), MKV / H.264
Summary
Thread Performance Checker reports a priority inversion on the software playback path: the demux consumer (
.userInitiated) blocks on anNSConditionwaiting for the read-ahead producer (.utility) to deliver packets.The two sides
Producer —
SoftwarePacketReadAhead.swift:38:Consumer —
SoftwarePlaybackHost.swift:120, which callsreadAhead.read(...)atSoftwarePlaybackHost.swift:2336:The block is the wait loop at
SoftwarePacketReadAhead.swift:231:NSConditiondoes not donate the waiter's priority, so whenever the FIFO runs dry the.userInitiatedconsumer is gated on a.utilityproducer with no priority boost.When it shows up
Reproducible whenever reads are slow enough for the buffer to empty — for us that is 1080p MKV streamed over WebDAV, where the producer is network-bound rather than CPU-bound. It fires around startup and after seeks, i.e. exactly when the buffer is empty by construction.
The prefetch queue is genuinely elective while the FIFO has depth, so
.utilityis the right resting state; the inversion is only about the window where the consumer is actually blocked on it.Possible directions
Whichever fits the design best:
.userInitiated— simplest, at the cost of the elective-work property when the FIFO is full..utilityat rest and boost only while a consumer is blocked: dispatchproduce()as aDispatchWorkItemand have the blocked consumerwait()on it, which propagates the waiter's QoS, instead of waiting on the bare condition.produce()at the consumer's QoS when the wait loop is about to be entered withcount == 0.Option 2 keeps the intent of #27 (the elective decode must yield to real-time playback) while removing the inversion in the one window where the work is not elective any more.
Environment