mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 03:06:37 +00:00
f7f55024ab
The suite intermittently hung on CI, most recently for the full 2 minute time limit on lifetimeIsCapped(). A spindump from a failed run shows the process suspended rather than spinning (0.247s of CPU over 190s) with a background priority worker thread sitting runnable for roughly 145 seconds without ever being scheduled. That band is exactly what swift-clocks' TestClock depends on to make progress: both advance() and checkSuspension() await Task.megaYield(), which serially awaits 20 detached tasks at background priority. On an oversubscribed runner those never run, so the test waits forever instead of failing. The same mechanism, less starved, is why two of the passing tests took 1.0s and 0.77s for what is otherwise pure logic. checkSuspension() made it worse, since asking whether a timer was armed cost a megaYield per poll. ManualClock replaces it. It holds a single pending sleep, since that is all a test needs to step through: the sleep registers a continuation, advance(by:) resumes it synchronously once its deadline passes, and waitForScheduledSleep() suspends until the timer is armed instead of polling for it. Nothing in the path touches background priority and no loop's iteration count depends on the scheduler. The suite now runs in 0.019s, and under a locally saturated CPU the old version took 1.005s against the new one's 0.037s. Also drops the now unused Clocks package, removes the 2 minute time limit that was papering over the starvation, re-enables callIsTimingOut() which the same flakiness had disabled, and has lifetimeIsCapped() assert the 90 second cap directly rather than inferring it from when the timer fires.
152 lines
5.3 KiB
Swift
152 lines
5.3 KiB
Swift
//
|
|
// Copyright 2026 Element Creations Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import Synchronization
|
|
|
|
/// A clock whose sleeps only finish when the test advances it.
|
|
final nonisolated class ManualClock: Clock {
|
|
struct Instant: InstantProtocol {
|
|
let offset: Duration
|
|
|
|
func advanced(by duration: Duration) -> Self {
|
|
Self(offset: offset + duration)
|
|
}
|
|
|
|
func duration(to other: Self) -> Duration {
|
|
other.offset - offset
|
|
}
|
|
|
|
static func < (lhs: Self, rhs: Self) -> Bool {
|
|
lhs.offset < rhs.offset
|
|
}
|
|
}
|
|
|
|
private struct Sleep {
|
|
let deadline: Instant
|
|
let continuation: CheckedContinuation<Void, any Error>
|
|
}
|
|
|
|
private struct State {
|
|
var now = Instant(offset: .zero)
|
|
/// A test steps through one timer at a time, so a single sleep can be pending.
|
|
var sleep: Sleep?
|
|
var sleepObserver: CheckedContinuation<Duration, Never>?
|
|
}
|
|
|
|
private let state = Mutex(State())
|
|
|
|
let minimumResolution: Duration = .zero
|
|
|
|
var now: Instant {
|
|
state.withLock { $0.now }
|
|
}
|
|
|
|
/// Suspends until ``advance(by:)`` moves the clock to `deadline`, or the calling task is
|
|
/// cancelled, in which case this throws a `CancellationError`. Deadlines that have already
|
|
/// passed return without suspending. `tolerance` is ignored.
|
|
func sleep(until deadline: Instant, tolerance: Duration? = nil) async throws {
|
|
try await withTaskCancellationHandler {
|
|
try Task.checkCancellation()
|
|
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
switch schedule(deadline: deadline, continuation: continuation) {
|
|
case .elapsed:
|
|
continuation.resume()
|
|
case .scheduled(let duration, let observer):
|
|
observer?.resume(returning: duration)
|
|
|
|
// Cancellation that landed while scheduling found nothing to cancel.
|
|
if Task.isCancelled {
|
|
cancelSleep()
|
|
}
|
|
}
|
|
}
|
|
} onCancel: {
|
|
cancelSleep()
|
|
}
|
|
}
|
|
|
|
/// Suspends until a sleep is pending and returns how long it has left to run. A sleep that is
|
|
/// already pending returns straight away, so a scheduled sleep can't be missed.
|
|
///
|
|
/// Await this before advancing a timer that runs in an unstructured task — advancing the clock
|
|
/// before the sleep has been scheduled would leave it sleeping past the new deadline.
|
|
@discardableResult
|
|
func waitForScheduledSleep() async -> Duration {
|
|
await withCheckedContinuation { continuation in
|
|
let duration = state.withLock { state -> Duration? in
|
|
guard let sleep = state.sleep else {
|
|
state.sleepObserver = continuation
|
|
return nil
|
|
}
|
|
|
|
return state.now.duration(to: sleep.deadline)
|
|
}
|
|
|
|
if let duration {
|
|
continuation.resume(returning: duration)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Moves ``now`` forward, resuming the pending sleep if it has come due. Resuming is all this
|
|
/// does — wait on the work the sleep unblocks rather than assuming it has run by the time this
|
|
/// returns.
|
|
func advance(by duration: Duration) {
|
|
let dueSleep = state.withLock { state -> Sleep? in
|
|
state.now = state.now.advanced(by: duration)
|
|
|
|
guard let sleep = state.sleep, sleep.deadline <= state.now else {
|
|
return nil
|
|
}
|
|
|
|
state.sleep = nil
|
|
return sleep
|
|
}
|
|
|
|
dueSleep?.continuation.resume()
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private enum Scheduling {
|
|
/// The deadline had already passed, the sleep doesn't suspend.
|
|
case elapsed
|
|
/// The sleep is now pending, along with the ``waitForScheduledSleep()`` caller to notify.
|
|
case scheduled(Duration, observer: CheckedContinuation<Duration, Never>?)
|
|
}
|
|
|
|
/// Stores the sleep's continuation, handing the caller whatever needs resuming so that it
|
|
/// happens outside of the lock.
|
|
private func schedule(deadline: Instant, continuation: CheckedContinuation<Void, any Error>) -> Scheduling {
|
|
state.withLock { state in
|
|
guard deadline > state.now else {
|
|
return .elapsed
|
|
}
|
|
|
|
precondition(state.sleep == nil, "The clock only supports a single pending sleep.")
|
|
|
|
state.sleep = Sleep(deadline: deadline, continuation: continuation)
|
|
|
|
let observer = state.sleepObserver
|
|
state.sleepObserver = nil
|
|
|
|
return .scheduled(state.now.duration(to: deadline), observer: observer)
|
|
}
|
|
}
|
|
|
|
private func cancelSleep() {
|
|
let sleep = state.withLock { state -> Sleep? in
|
|
let sleep = state.sleep
|
|
state.sleep = nil
|
|
return sleep
|
|
}
|
|
|
|
sleep?.continuation.resume(throwing: CancellationError())
|
|
}
|
|
}
|