fix(display): 上次超时的显示器排到最后读取
超时的内核调用返回前占着唯一执行槽,固定顺序读取时排在后面的显示器只会持续得到 busy。上一轮超时的显示器改为最后读取,仍每轮重试。
This commit is contained in:
1 parent
dc03cb3b0e
commit
a7b894f386
3 files changed
+44
-3
No files matched your search
+2
-1
@@ -631,7 +631,8 @@ that the display applied it. No software dimming or speculative compatibility wr
|
||||
Blocking native work uses one bounded serial execution slot with a two-second caller deadline.
|
||||
A timed-out kernel call retains the slot until it returns, so retries cannot accumulate stuck work.
|
||||
Each display read gets its own cancellation ticket, so one timed-out display does not skip the
|
||||
others in the same refresh. Cancellation is checked between bus operations; late results cannot repopulate a closed or replaced
|
||||
others in the same refresh; a display that timed out last time is read last, so while its stuck call
|
||||
still holds the execution slot, responsive displays are not starved by `.busy`. Cancellation is checked between bus operations; late results cannot repopulate a closed or replaced
|
||||
view. Protocol, timeout, cancellation and write-confirmation tests use simulated devices. To generate
|
||||
a repeatable, read-only report on actual hardware (no Set VCP):
|
||||
|
||||
|
||||
@@ -133,7 +133,11 @@ final class DisplayController {
|
||||
isRefreshing = true
|
||||
defer { if generation == epoch { isRefreshing = false; cancellation = nil } }
|
||||
var updated: [UInt32: [DisplayControl: DDCResult]] = [:]
|
||||
for display in catalog where !display.isBuiltIn {
|
||||
// 超时的内核调用返回前占着唯一的执行槽,紧随其后的读取只会得到 busy;
|
||||
// 上一轮超时的显示器排到最后,响应正常的显示器不会被它持续挤掉。
|
||||
let external = catalog.filter { !$0.isBuiltIn }
|
||||
let timedOut = { (display: DisplayInfo) in self.readings[display.id]?.values.contains(.timedOut) == true }
|
||||
for display in external.filter({ !timedOut($0) }) + external.filter(timedOut) {
|
||||
// 超时会取消本台的令牌;每台独立,避免一台无响应导致后面的显示器跳过读取。
|
||||
let ticket = DDCCancellation()
|
||||
cancellation = ticket
|
||||
|
||||
@@ -21,13 +21,23 @@ private actor TestDisplayBackend: DisplayDDCBackend {
|
||||
var gates: [CheckedContinuation<[DisplayControl: DDCResult], Never>] = []
|
||||
var tickets: [DDCCancellation] = []
|
||||
var timingOutTargets: Set<UInt32> = []
|
||||
var readOrder: [UInt32] = []
|
||||
var slotHeld = false
|
||||
var timeoutsHoldSlot = false
|
||||
nonisolated let resets = ResetCounter()
|
||||
func read(_ target: DisplayTarget, cancellation: DDCCancellation) async -> [DisplayControl: DDCResult] {
|
||||
reads += 1
|
||||
tickets.append(cancellation)
|
||||
readOrder.append(target.id)
|
||||
// 与 NativeDisplayDDC.execute 一致:超时的调用返回前占着执行槽,紧随其后的请求得到 busy
|
||||
if slotHeld {
|
||||
slotHeld = false
|
||||
return [.brightness: .busy]
|
||||
}
|
||||
// 与 NativeDisplayDDC.execute 一致:超时时取消传入的令牌
|
||||
if timingOutTargets.contains(target.id) {
|
||||
cancellation.cancel()
|
||||
slotHeld = timeoutsHoldSlot
|
||||
return [.brightness: .timedOut]
|
||||
}
|
||||
if gateReads { return await withCheckedContinuation { gates.append($0) } }
|
||||
@@ -39,10 +49,15 @@ private actor TestDisplayBackend: DisplayDDCBackend {
|
||||
}
|
||||
nonisolated func resetConnections() { resets.increment() }
|
||||
func block() { gateReads = true }
|
||||
func timeOut(_ id: UInt32) { timingOutTargets.insert(id) }
|
||||
func timeOut(_ id: UInt32, holdingSlot: Bool = false) {
|
||||
timingOutTargets.insert(id)
|
||||
timeoutsHoldSlot = holdingSlot
|
||||
}
|
||||
func finish(_ value: UInt16) { gates.removeFirst().resume(returning: [.brightness: .value(DDCValue(current: value, maximum: 100))]) }
|
||||
func setWriteResult(_ value: DDCResult) { writeResult = value }
|
||||
func count() -> Int { reads }
|
||||
func order() -> [UInt32] { readOrder }
|
||||
func releaseSlot() { slotHeld = false }
|
||||
func writeCount() -> Int { writes }
|
||||
func wasCancelled(_ index: Int) -> Bool { tickets[index].isCancelled }
|
||||
}
|
||||
@@ -80,6 +95,27 @@ struct DisplayControllerTests {
|
||||
#expect(controller.readings[8]?[.brightness] == .value(DDCValue(current: 75, maximum: 100)))
|
||||
}
|
||||
|
||||
@Test func persistentlyTimedOutDisplayIsReadLastSoOthersGetTheSlot() async throws {
|
||||
let backend = TestDisplayBackend()
|
||||
await backend.timeOut(7, holdingSlot: true)
|
||||
let second = DisplayInfo(target: DisplayTarget(id: 8, identity: "fixture-second"), name: "Second Display",
|
||||
isBuiltIn: false, isMain: false, summary: "2560×1440 · 60Hz")
|
||||
let controller = DisplayController(backend: backend, catalog: [display, second])
|
||||
defer { controller.stop() }
|
||||
controller.setVisible(true)
|
||||
try await waitForReads(backend, 2)
|
||||
try await settle(controller)
|
||||
// 首轮按目录顺序:7 超时后占着执行槽,8 只得到 busy
|
||||
#expect(await backend.order() == [7, 8])
|
||||
#expect(controller.readings[8]?[.brightness] == .busy)
|
||||
await backend.releaseSlot()
|
||||
await controller.refresh()
|
||||
// 下一轮 7 排到最后,8 先拿到执行槽;7 仍会重试,不被永久放弃
|
||||
#expect(await backend.order() == [7, 8, 8, 7])
|
||||
#expect(controller.readings[8]?[.brightness] == .value(DDCValue(current: 75, maximum: 100)))
|
||||
#expect(controller.readings[7]?[.brightness] == .timedOut)
|
||||
}
|
||||
|
||||
@Test func wakeDropsMatchedConnectionsButVisibilityChangesKeepThem() async throws {
|
||||
let backend = TestDisplayBackend()
|
||||
let controller = DisplayController(backend: backend, catalog: [display])
|
||||
|
||||
Reference in new issue
Block a user