diff --git a/Sources/SWBBuildService/Messages.swift b/Sources/SWBBuildService/Messages.swift index ea3baaeb..06bda54a 100644 --- a/Sources/SWBBuildService/Messages.swift +++ b/Sources/SWBBuildService/Messages.swift @@ -683,7 +683,7 @@ private struct GetIndexingHeaderInfoMsg: MessageHandler { } extension MessageHandler { - fileprivate func handleIndexingInfoRequest(serializationQueue: ActorLock, request: Request, message: T, _ transformResponse: @escaping @Sendable (T, WorkspaceContext, BuildRequest, BuildRequestContext, BuildDescription, ConfiguredTarget, ElapsedTimer) -> any SWBProtocol.Message) async throws -> VoidResponse { + fileprivate func handleIndexingInfoRequest(serializationQueue: ActorLock, request: Request, message: T, _ transformResponse: @escaping @Sendable (T, WorkspaceContext, BuildRequest, BuildRequestContext, BuildDescription, ConfiguredTarget, ElapsedTimer) -> any SWBProtocol.Message) async throws -> VoidResponse { let elapsedTimer = ElapsedTimer() // FIXME: Move this to use ActiveBuild. diff --git a/Sources/SWBTestSupport/MockClock.swift b/Sources/SWBTestSupport/MockClock.swift new file mode 100644 index 00000000..7cc767aa --- /dev/null +++ b/Sources/SWBTestSupport/MockClock.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SWBUtil +import Synchronization + +/// A mock clock whose current time is controllable. +public struct MockClock: Clock { + public typealias Instant = ContinuousClock.Instant + + private final class State: Sendable { + let now: SWBMutex + + init(now: Instant) { + self.now = .init(now) + } + } + + private let state: State + + public init(now: Instant = .now) { + self.state = .init(now: now) + } + + public var now: Instant { + state.now.withLock({ $0 }) + } + + public var minimumResolution: Duration { + ContinuousClock.continuous.minimumResolution + } + + public func sleep(until deadline: Instant, tolerance: Duration?) async throws { + state.now.withLock { now in + if now < deadline { + now = deadline + } + } + } +} diff --git a/Sources/SWBUtil/ElapsedTimer.swift b/Sources/SWBUtil/ElapsedTimer.swift index 03680c0c..067ec11f 100644 --- a/Sources/SWBUtil/ElapsedTimer.swift +++ b/Sources/SWBUtil/ElapsedTimer.swift @@ -15,47 +15,51 @@ public import struct Foundation.TimeInterval /// Provides a simple nanosecond-precision elapsed timer using a monotonic clock. /// /// This is a completely immutable object and thus is thread safe. -public struct ElapsedTimer: Sendable { +public struct ElapsedTimer: Sendable where ClockType.Duration == Duration { + private let clock: ClockType + /// The beginning of the time interval, measured when the `ElapsedTimer` was initialized. - private let start = ContinuousClock.now + private let start: ClockType.Instant /// Initializes a new timer. /// /// The `ElapsedTimer` object captures the current time at initialization and uses this as the starting point of an elapsed time measurement. /// /// - note: The starting point of the timer is fixed once it is initialized and the object provides no facilities to "restart" the clock (simply create a new instance to do so). - public init() { + public init(clock: ClockType = ContinuousClock.continuous) { + self.clock = clock + self.start = clock.now } /// Computes the length of the time interval, that is, the length of the time interval between now and the point in time when the `ElapsedTimer` was initialized. /// /// This value is guaranteed to be positive. public func elapsedTime() -> ElapsedTimerInterval { - return ElapsedTimerInterval(duration: ContinuousClock.now - start) + return ElapsedTimerInterval(duration: start.duration(to: clock.now)) } /// Time the given closure, returning the elapsed time and the result. - public static func measure(_ body: () throws -> T) rethrows -> (elapsedTime: ElapsedTimerInterval, result: T) { - let timer = ElapsedTimer() + public static func measure(clock: ClockType = ContinuousClock.continuous, _ body: () throws -> T) rethrows -> (elapsedTime: ElapsedTimerInterval, result: T) { + let timer = ElapsedTimer(clock: clock) let result = try body() return (timer.elapsedTime(), result) } /// Time the given closure, returning the elapsed time and the result. - public static func measure(_ body: () async throws -> T) async rethrows -> (elapsedTime: ElapsedTimerInterval, result: T) { - let timer = ElapsedTimer() + public static func measure(clock: ClockType = ContinuousClock.continuous, _ body: () async throws -> T) async rethrows -> (elapsedTime: ElapsedTimerInterval, result: T) { + let timer = ElapsedTimer(clock: clock) let result = try await body() return (timer.elapsedTime(), result) } /// Time the given closure, returning the elapsed time. - public static func measure(_ body: () throws -> Void) rethrows -> ElapsedTimerInterval { - return try measure(body).elapsedTime + public static func measure(clock: ClockType = ContinuousClock.continuous, _ body: () throws -> Void) rethrows -> ElapsedTimerInterval { + return try measure(clock: clock, body).elapsedTime } /// Time the given closure, returning the elapsed time. - public static func measure(_ body: () async throws -> Void) async rethrows -> ElapsedTimerInterval { - return try await measure(body).elapsedTime + public static func measure(clock: ClockType = ContinuousClock.continuous, _ body: () async throws -> Void) async rethrows -> ElapsedTimerInterval { + return try await measure(clock: clock, body).elapsedTime } } diff --git a/Sources/SWBUtil/RateLimiter.swift b/Sources/SWBUtil/RateLimiter.swift index f034de76..6c3b5b58 100644 --- a/Sources/SWBUtil/RateLimiter.swift +++ b/Sources/SWBUtil/RateLimiter.swift @@ -13,15 +13,17 @@ /// Provides a simple utility for implementing rate-limiting mechanisms. /// /// This object is NOT thread-safe. -public struct RateLimiter: ~Copyable { - private let start: ContinuousClock.Instant - private var last: ContinuousClock.Instant +public struct RateLimiter: ~Copyable { + private let clock: ClockType + private let start: ClockType.Instant + private var last: ClockType.Instant /// The length of the time interval to which updates are rate-limited. - public let interval: Duration + public let interval: ClockType.Duration - public init(interval: Duration) { - let now = ContinuousClock.now + public init(interval: ClockType.Duration, clock: ClockType = ContinuousClock.continuous) { + self.clock = clock + let now = clock.now self.interval = interval self.start = now self.last = now @@ -31,8 +33,8 @@ public struct RateLimiter: ~Copyable { /// time this function returned `true`, is greater than the time interval /// with which this object was initialized. public mutating func hasNextIntervalPassed() -> Bool { - let now = ContinuousClock.now - let elapsed = now - last + let now = clock.now + let elapsed = last.duration(to: now) if elapsed >= interval { last = now return true diff --git a/Tests/SWBUtilTests/ElapsedTimerTests.swift b/Tests/SWBUtilTests/ElapsedTimerTests.swift index 3c364148..8e51ec41 100644 --- a/Tests/SWBUtilTests/ElapsedTimerTests.swift +++ b/Tests/SWBUtilTests/ElapsedTimerTests.swift @@ -19,16 +19,18 @@ import SWBTestSupport @Test(.skipHostOS(.freebsd, "Currently hangs on FreeBSD")) func time() async throws { do { - let delta = try await ElapsedTimer.measure { - try await Task.sleep(for: .microseconds(1000)) + let clock = MockClock() + let delta = try await ElapsedTimer.measure(clock: clock) { + try await clock.sleep(for: .microseconds(1001)) return () } #expect(delta.seconds > 1.0 / 1000.0) } do { - let (delta, result) = try await ElapsedTimer.measure { () -> Int in - try await Task.sleep(for: .microseconds(1000)) + let clock = MockClock() + let (delta, result) = try await ElapsedTimer.measure(clock: clock) { () -> Int in + try await clock.sleep(for: .microseconds(1001)) return 22 } #expect(delta.seconds > 1.0 / 1000.0) diff --git a/Tests/SWBUtilTests/RateLimiterTests.swift b/Tests/SWBUtilTests/RateLimiterTests.swift index 3affb9b4..5fe0148b 100644 --- a/Tests/SWBUtilTests/RateLimiterTests.swift +++ b/Tests/SWBUtilTests/RateLimiterTests.swift @@ -19,8 +19,9 @@ import SWBTestSupport fileprivate struct RateLimiterTests { @Test func rateLimiterSeconds() async throws { - let timer = ElapsedTimer() - var limiter = RateLimiter(interval: .seconds(1)) + let clock = MockClock() + let timer = ElapsedTimer(clock: clock) + var limiter = RateLimiter(interval: .seconds(1), clock: clock) #expect(limiter.interval == .nanoseconds(1_000_000_000)) var count = 0 @@ -28,7 +29,7 @@ fileprivate struct RateLimiterTests { if limiter.hasNextIntervalPassed() { count += 1 } - try await Task.sleep(for: .seconds(1)) + try await clock.sleep(for: .seconds(1)) } #expect(count > 1) @@ -37,8 +38,9 @@ fileprivate struct RateLimiterTests { @Test func rateLimiterTwoSeconds() async throws { - let timer = ElapsedTimer() - var limiter = RateLimiter(interval: .seconds(2)) + let clock = MockClock() + let timer = ElapsedTimer(clock: clock) + var limiter = RateLimiter(interval: .seconds(2), clock: clock) #expect(limiter.interval == .nanoseconds(2_000_000_000)) var count = 0 @@ -46,7 +48,7 @@ fileprivate struct RateLimiterTests { if limiter.hasNextIntervalPassed() { count += 1 } - try await Task.sleep(for: .seconds(1)) + try await clock.sleep(for: .seconds(1)) } #expect(count > 0) @@ -55,16 +57,17 @@ fileprivate struct RateLimiterTests { @Test func rateLimiterMilliseconds() async throws { - let timer = ElapsedTimer() - var limiter = RateLimiter(interval: .milliseconds(100)) + let clock = MockClock() + let timer = ElapsedTimer(clock: clock) + var limiter = RateLimiter(interval: .milliseconds(100), clock: clock) #expect(limiter.interval == .nanoseconds(100_000_000)) var count = 0 - for _ in 0..<100 { + for _ in 0..<101 { if limiter.hasNextIntervalPassed() { count += 1 } - try await Task.sleep(for: .microseconds(2001)) + try await clock.sleep(for: .microseconds(2001)) } #expect(count > 1) @@ -73,16 +76,17 @@ fileprivate struct RateLimiterTests { @Test func rateLimiterMicroseconds() async throws { - let timer = ElapsedTimer() - var limiter = RateLimiter(interval: .microseconds(100000)) + let clock = MockClock() + let timer = ElapsedTimer(clock: clock) + var limiter = RateLimiter(interval: .microseconds(100000), clock: clock) #expect(limiter.interval == .nanoseconds(100_000_000)) var count = 0 - for _ in 0..<100 { + for _ in 0..<101 { if limiter.hasNextIntervalPassed() { count += 1 } - try await Task.sleep(for: .microseconds(1001)) + try await clock.sleep(for: .microseconds(1001)) } #expect(count > 0) @@ -91,16 +95,17 @@ fileprivate struct RateLimiterTests { @Test func rateLimiterNanoseconds() async throws { - let timer = ElapsedTimer() - var limiter = RateLimiter(interval: .nanoseconds(100_000_000)) + let clock = MockClock() + let timer = ElapsedTimer(clock: clock) + var limiter = RateLimiter(interval: .nanoseconds(100_000_000), clock: clock) #expect(limiter.interval == .nanoseconds(100_000_000)) var count = 0 - for _ in 0..<100 { + for _ in 0..<101 { if limiter.hasNextIntervalPassed() { count += 1 } - try await Task.sleep(for: .microseconds(1001)) + try await clock.sleep(for: .microseconds(1001)) } #expect(count > 0) @@ -109,12 +114,13 @@ fileprivate struct RateLimiterTests { @Test func rateLimiterNoCrashNever() async throws { - var limiter = RateLimiter(interval: .nanoseconds(UInt64.max)) + let clock = MockClock() + var limiter = RateLimiter(interval: .nanoseconds(UInt64.max), clock: clock) for _ in 0..<2 { let nextIntervalPassed = limiter.hasNextIntervalPassed() #expect(!nextIntervalPassed) - try await Task.sleep(for: .seconds(1)) + try await clock.sleep(for: .seconds(1)) } } }