Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/SWBBuildService/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ private struct GetIndexingHeaderInfoMsg: MessageHandler {
}

extension MessageHandler {
fileprivate func handleIndexingInfoRequest<T: IndexingInfoRequest>(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<T: IndexingInfoRequest>(serializationQueue: ActorLock, request: Request, message: T, _ transformResponse: @escaping @Sendable (T, WorkspaceContext, BuildRequest, BuildRequestContext, BuildDescription, ConfiguredTarget, ElapsedTimer<ContinuousClock>) -> any SWBProtocol.Message) async throws -> VoidResponse {
let elapsedTimer = ElapsedTimer()

// FIXME: Move this to use ActiveBuild.
Expand Down
49 changes: 49 additions & 0 deletions Sources/SWBTestSupport/MockClock.swift
Original file line number Diff line number Diff line change
@@ -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<Instant>

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
}
}
}
}
28 changes: 16 additions & 12 deletions Sources/SWBUtil/ElapsedTimer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClockType: Clock>: 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<T>(_ body: () throws -> T) rethrows -> (elapsedTime: ElapsedTimerInterval, result: T) {
let timer = ElapsedTimer()
public static func measure<T>(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<T>(_ body: () async throws -> T) async rethrows -> (elapsedTime: ElapsedTimerInterval, result: T) {
let timer = ElapsedTimer()
public static func measure<T>(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
}
}

Expand Down
18 changes: 10 additions & 8 deletions Sources/SWBUtil/RateLimiter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClockType: Clock>: ~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
Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions Tests/SWBUtilTests/ElapsedTimerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 26 additions & 20 deletions Tests/SWBUtilTests/RateLimiterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ 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
for _ in 0..<3 {
if limiter.hasNextIntervalPassed() {
count += 1
}
try await Task.sleep(for: .seconds(1))
try await clock.sleep(for: .seconds(1))
}

#expect(count > 1)
Expand All @@ -37,16 +38,17 @@ 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
for _ in 0..<3 {
if limiter.hasNextIntervalPassed() {
count += 1
}
try await Task.sleep(for: .seconds(1))
try await clock.sleep(for: .seconds(1))
}

#expect(count > 0)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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))
}
}
}
Loading