Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/scripts/ci-linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ ci_run 'Build (reductive traits on)' \
swift build --build-tests --quiet \
--traits DisableDebugLogging,DisableErrorLogging "$@"
ci_run 'Test (debug)' \
swift test --quiet -Xswiftc -DNETWORK_INTERNAL_TESTS "$@"
swift test --quiet "$@"
ci_run 'Test (release)' \
swift test --quiet -c release -Xswiftc -DNETWORK_INTERNAL_TESTS "$@"
swift test --quiet -c release "$@"

ci_finish
2 changes: 1 addition & 1 deletion .github/workflows/scripts/ci-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ xcodebuild_step 'watchOS build' 'generic/platform=watchos'
xcodebuild_step 'tvOS build' 'generic/platform=tvos'
xcodebuild_step 'visionOS build' 'generic/platform=visionos'

ci_run 'swift test' xcrun swift test --quiet -Xswiftc -DNETWORK_INTERNAL_TESTS "$@"
ci_run 'swift test' xcrun swift test --quiet "$@"

ci_finish
7 changes: 0 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,6 @@ let allApplePlatforms: [Platform] = [

// Logging levels, qlog output, and QUIC signposts are configured via package
// traits. See the `traits:` list on the `Package(...)` initializer below.
//
// Test-only hooks in the library, are guarded by `NETWORK_INTERNAL_TESTS`.
// Pass it on the command line instead:
//
// swift test -Xswiftc -DNETWORK_INTERNAL_TESTS
//
// Tests that depend on those hooks skip themselves when it is absent.
let settings: [SwiftSetting] = [
.define("IMPORT_SWIFTTLS"),
.define("EXPORT_SWIFTTLS"),
Expand Down
7 changes: 0 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,6 @@ Unit tests can also be run by filtering a specific class or function:
% swift test --filter SwiftNetworkUDPTests.testUDPEcho
```

Some tests depend on hooks that are compiled into the library only on demand, because they cost a small amount of performance on hot paths.
Pass the define on the command line to compile them in:

```
% swift test -Xswiftc -DNETWORK_INTERNAL_TESTS
```

All unit tests are run automatically upon creation or update of a Pull Request. See [CONTRIBUTING](https://github.com/apple/swift-network-evolution/blob/main/CONTRIBUTING.md) for details.

### Versioning
Expand Down
31 changes: 31 additions & 0 deletions Sources/SwiftNetwork/Context/NetworkContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ public final class NetworkContext: NetworkContextProtocol, @unchecked Sendable {
func unschedule(reference: TimerReference)
/// A Boolean value that indicates whether the current code is running in the scheduler.
var runningInScheduler: Bool { get }
/// The scheduler's current notion of the continuous clock.
///
/// The scheduler owns time because it owns the timers: one that holds scheduled tasks
/// instead of arming an OS timer has to report the time it fires them at, or a deadline it
/// just ran would still look like it is in the future.
var now: NetworkClock.Instant { get }
/// The scheduler's current notion of the absolute clock.
///
/// Separate from `now` because the two clocks diverge across system sleep, and `Pacer`
/// reads the difference between them as a domain offset.
var nowAbsolute: NetworkClock.Instant { get }
}

/// Indicates the privacy level for the context.
Expand Down Expand Up @@ -389,6 +400,13 @@ extension NetworkContext {
// TODO: Not supported by DispatchQueue
fatalError("Unsupported")
}
/// The real clock. This is the one scheduler allowed to read it.
var now: NetworkClock.Instant {
NetworkClock.Instant.systemNow
}
var nowAbsolute: NetworkClock.Instant {
NetworkClock.Instant.systemNowAbsolute
}
}
}

Expand Down Expand Up @@ -441,6 +459,19 @@ extension NetworkContext {
}

#if !NETWORK_PRIVATE || NETWORK_STANDALONE
/// The context's current notion of the continuous clock.
///
/// Time comes from whatever runs the context's timers, so a scheduler that holds scheduled
/// tasks instead of arming an OS timer reports the time it fires them at.
var now: NetworkClock.Instant {
scheduler.now
}

/// The context's current notion of the absolute clock.
var nowAbsolute: NetworkClock.Instant {
scheduler.nowAbsolute
}

func resetTimer(for reference: TimerReference, to time: FutureTime) {
switch time {
case .unschedule:
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftNetwork/Protocols/IPProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2044,7 +2044,7 @@ public struct IPProtocol: NetworkProtocol {
&self.instanceType,
log: self.log,
frames: &inboundFrames,
now: NetworkClock.Instant.now
now: self.context.now
)
guard !inboundFrames.isEmpty else {
log.error("Dropped inbound packets, checking for more")
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftNetwork/QUIC/Pacer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ struct Pacer: ~Copyable {
guard let path else {
return
}
let continuousTime = NetworkClock.Instant.now
let absoluteTime: NetworkClock.Instant = .nowAbsolute
let continuousTime = path.parentProtocol.now
let absoluteTime = path.parentProtocol.nowAbsolute

if packetSentTime == .zero {
packetSentTime = absoluteTime
Expand Down
13 changes: 9 additions & 4 deletions Sources/SwiftNetwork/QUIC/QLog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -588,11 +588,14 @@ final class QLog {
private var eventsList: [Event]
private var topLevelObject: [String: Any]
private var disableTimestamps: Bool = false
private let startTime = NetworkClock.Instant.now
private let context: NetworkContext
private let startTime: NetworkClock.Instant
var configuration: QLogConfiguration?

public init(configuration: QLogConfiguration? = nil) {
public init(configuration: QLogConfiguration? = nil, context: NetworkContext) {
self.configuration = configuration
self.context = context
self.startTime = context.now
self.eventsList = []
self.topLevelObject = [:]
self.topLevelObject["qlog_version"] = "draft-01"
Expand Down Expand Up @@ -734,8 +737,9 @@ final class QLog {
bytesInFlight: UInt64 = UInt64.max,
slowStartThresh: UInt64 = UInt64.max,
packetsInFlight: UInt64 = UInt64.max,
timestamp: NetworkClock.Instant = .now
timestamp: NetworkClock.Instant? = nil
) {
let timestamp = timestamp ?? context.now
metricsUpdated(
minRTT: .zero,
smoothedRTT: .zero,
Expand Down Expand Up @@ -800,8 +804,9 @@ final class QLog {
oldState: QLogCongestionState?,
newState: QLogCongestionState?,
trigger: QLogCongestionTrigger?,
timestamp: NetworkClock.Instant = .now
timestamp: NetworkClock.Instant? = nil
) {
let timestamp = timestamp ?? context.now
let congestionEvent = EventCongestionStateUpdated(
oldCongestionState: oldState,
newCongestionState: newState,
Expand Down
34 changes: 26 additions & 8 deletions Sources/SwiftNetwork/QUIC/QUICConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol,

var currentInboundReceiveTimestamp: NetworkClock.Instant?
var currentSendTimestamp: NetworkClock.Instant?
/// The absolute-clock reading taken alongside whichever timestamp above is set.
///
/// `Pacer` converts between the two clock domains by subtracting one reading from the other, so
/// the pair must be sampled together; sampled a moment apart, the gap between the reads folds
/// into the offset. Stamping it here also keeps `getSendTime` from reading either clock.
var currentAbsoluteTimestamp: NetworkClock.Instant?

@_optimize(speed)
@inline(always)
Expand All @@ -263,10 +269,16 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol,
} else if let currentSendTimestamp {
return currentSendTimestamp
} else {
return NetworkClock.Instant.now
return context.now
}
}

@_optimize(speed)
@inline(always)
var nowAbsolute: NetworkClock.Instant {
currentAbsoluteTimestamp ?? context.nowAbsolute
}

var lastPacketReceivedTimestamp: NetworkClock.Instant = .zero
var lastAckElicitingPacketSentTimestamp: NetworkClock.Instant = .zero
var lastShorthandTimestamp: NetworkClock.Instant = .zero
Expand Down Expand Up @@ -592,7 +604,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol,
}
// Only setup qlog if the directory is set
if let qlogConfiguration {
self.qLog = QLog(configuration: qlogConfiguration)
self.qLog = QLog(configuration: qlogConfiguration, context: context)
log.info("qlog setup with configuration: \(qlogConfiguration)")
}
#endif
Expand Down Expand Up @@ -1562,8 +1574,9 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol,
public func serviceReceivedDatagrams(path pathID: MultiplexingPathIdentifier) {
let inboundInterval = QUICSignpost.inboundStarting(id: signpostID)

// Save a timestamp to avoid calculating `now` again during processing
currentInboundReceiveTimestamp = .now
// Save the timestamps to avoid calculating `now` again during processing
currentInboundReceiveTimestamp = context.now
currentAbsoluteTimestamp = context.nowAbsolute

// Start anew with pendingItems for applicationPendingItems
// Detect if any received packet contains a QUIC Frame that unblocks
Expand All @@ -1577,6 +1590,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol,
defer {
recovery.endBatch(connection: self)
currentInboundReceiveTimestamp = nil
currentAbsoluteTimestamp = nil
}

if !pendingReassemblyDequeue.isEmpty {
Expand Down Expand Up @@ -2411,12 +2425,14 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol,

let outboundInterval = QUICSignpost.outboundStarting(id: signpostID)

// Save a timestamp to avoid calculating `now` again during processing
currentSendTimestamp = .now
// Save the timestamps to avoid calculating `now` again during processing
currentSendTimestamp = context.now
currentAbsoluteTimestamp = context.nowAbsolute
defer {
// Always reset
QUICSignpost.outboundStopping(outboundInterval)
currentSendTimestamp = nil
currentAbsoluteTimestamp = nil
}

accessStreamDataToSend(flow: flowID) { streamData in
Expand Down Expand Up @@ -3324,10 +3340,12 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol,
} else if packetBurst >= Constants.packetBurstCount {
// The packet burst count has been reached, check the time
//
// Deliberately the live clock rather than `self.now`: under a batch `self.now` is
// Deliberately the context's clock rather than `self.now`: under a batch `self.now` is
// pinned, and `startSendingTimestamp` came from it, so comparing the two would
// always give zero and the cap could never be reached.
if startSendingTimestamp.duration(to: .now) >= Constants.maxPacketBurstDuration {
if startSendingTimestamp.duration(to: context.now)
>= Constants.maxPacketBurstDuration
{
// The maximum burst time has been reached
shouldEndBurst = true
} else {
Expand Down
88 changes: 16 additions & 72 deletions Sources/SwiftNetwork/Utilities/NetworkClock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,70 +241,20 @@ public struct NetworkDuration: DurationProtocol, Hashable, Equatable, CustomStri
}
}

/// A continuous clock with a compact representation that tests can advance manually.
/// A continuous clock with a compact representation.
///
/// Mimics `Swift.ContinuousClock`, with two differences:
/// 1. It uses `NetworkDuration` internally so its size is 8 bytes.
/// 2. Tests can replace the OS clock with one they advance by hand,
/// which makes time-dependent behaviour deterministic.
/// Mimics `Swift.ContinuousClock`, except that it uses `NetworkDuration` internally so its size is
/// 8 bytes. A test makes time deterministic by supplying a scheduler that reports instants of its
/// own, not by replacing this clock.
#if !NETWORK_EMBEDDED
@_spi(Essentials)
// Availability due to `SwiftNetwork`'s `System.Time` (used by `Instant.now`)
// Availability due to `SwiftNetwork`'s `System.Time` (used by `Instant.systemNow`)
@available(Network 0.1.0, *)
#endif
public struct NetworkClock: Clock {
public struct Instant: InstantProtocol, CustomStringConvertible {
var time: NetworkDuration

#if NETWORK_INTERNAL_TESTS
// Backing storage for the manual clock used by tests.
//
// This is a `static let` box rather than a `static var` on purpose.
// Reading a mutable static emits a `swift_beginAccess` call for the
// dynamic exclusivity check. A `let` does not.
private final class ManualTime: @unchecked Sendable {
var continuous: Instant = .zero
var absolute: Instant = .zero
}
private static let manualTime = ManualTime()
#endif

internal static func useSystemTime() {
#if NETWORK_INTERNAL_TESTS
manualTime.continuous = .zero
manualTime.absolute = .zero
#endif
}

internal static func useManualTime(
_ continuous: Instant,
absolute: Instant? = nil
) {
#if NETWORK_INTERNAL_TESTS
let absolute = absolute ?? continuous
precondition(continuous > .zero, "manual time must be greater than zero")
precondition(absolute > .zero, "manual time must be greater than zero")
manualTime.continuous = continuous
manualTime.absolute = absolute
#else
fatalError("The manual clock requires building with -DNETWORK_INTERNAL_TESTS")
#endif
}

internal static func advanceManualTime(by duration: NetworkDuration) {
#if NETWORK_INTERNAL_TESTS
precondition(duration >= .zero, "manual time must not go backwards")
precondition(
manualTime.continuous > .zero,
"advanceManualTime(by:) requires useManualTime() first"
)
manualTime.continuous = manualTime.continuous.advanced(by: duration)
manualTime.absolute = manualTime.absolute.advanced(by: duration)
#else
fatalError("The manual clock requires building with -DNETWORK_INTERNAL_TESTS")
#endif
}

public func advanced(by duration: NetworkDuration) -> Self {
NetworkClock.Instant(self.time + duration)
}
Expand Down Expand Up @@ -345,24 +295,18 @@ public struct NetworkClock: Clock {
self.time = time
}

public static var now: Instant {
#if NETWORK_INTERNAL_TESTS
let manual = manualTime.continuous
if _slowPath(manual != .zero) {
return manual
}
#endif
return Instant(microseconds: Int64(System.Time.now()))
/// The system continuous clock, which no test can control.
///
/// Reach for the context's `now` instead, or an instant the caller already holds.
/// `NetworkContext.DefaultScheduler` is the one reader of this property in the library, so a
/// test that supplies an external scheduler reports a time of its own.
package static var systemNow: Instant {
Instant(microseconds: Int64(System.Time.now()))
}

public static var nowAbsolute: Instant {
#if NETWORK_INTERNAL_TESTS
let manual = manualTime.absolute
if _slowPath(manual != .zero) {
return manual
}
#endif
return Instant(nanoseconds: Int64(System.Time.nowAbsoluteNanoseconds()))
/// The system absolute clock. See `systemNow`.
package static var systemNowAbsolute: Instant {
Instant(nanoseconds: Int64(System.Time.nowAbsoluteNanoseconds()))
}

public static var zero: Instant {
Expand All @@ -382,7 +326,7 @@ public struct NetworkClock: Clock {
}

public var now: Instant {
Instant.now
Instant.systemNow
}

public var minimumResolution: NetworkDuration {
Expand Down
Loading
Loading