diff --git a/Benchmarks/Clocks/Clocks.swift b/Benchmarks/Clocks/Clocks.swift index 270b4ff..d4fd0d9 100644 --- a/Benchmarks/Clocks/Clocks.swift +++ b/Benchmarks/Clocks/Clocks.swift @@ -15,7 +15,7 @@ import BenchmarkSupport func benchmarks() { Benchmark.defaultDesiredDuration = .seconds(2) - Benchmark.defaultDesiredIterations = 1_000 + Benchmark.defaultDesiredIterations = 10_000 Benchmark.defaultThroughputScalingFactor = .kilo Benchmark("InternalUTCClock.now") { benchmark in @@ -24,6 +24,12 @@ func benchmarks() { } } + Benchmark("BenchmarkClock.now") { benchmark in + for _ in 0 ..< benchmark.throughputScalingFactor.rawValue { + BenchmarkSupport.blackHole(BenchmarkClock.now) + } + } + Benchmark("Foundation.Date") { benchmark in for _ in 0 ..< benchmark.throughputScalingFactor.rawValue { BenchmarkSupport.blackHole(Foundation.Date()) diff --git a/Sources/DateTime/BenchmarkClock.swift b/Sources/DateTime/BenchmarkClock.swift new file mode 100644 index 0000000..2bfd979 --- /dev/null +++ b/Sources/DateTime/BenchmarkClock.swift @@ -0,0 +1,178 @@ +// Copyright 2002 Ordo One AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +// An implementation of a clock suitable for benchmarking using clock_gettime_nsec_np() on macOS +// which is ~2-3 x less overhead. + +// swiftlint:disable line_length identifier_name + +// Largely adopted by Swift's ContinuousClock +// https://github.com/apple/swift/blob/48987de3d3ab228eed4867949795c188759df234/stdlib/public/Concurrency/ContinuousClock.swift#L49 + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#else +#error("Unsupported Platform") +#endif + +public struct BenchmarkClock { + /// A continuous point in time used for `BenchmarkClock`. + public struct Instant: Codable, Sendable { + internal var _value: Swift.Duration + + internal init(_value: Swift.Duration) { + self._value = _value + } + } + + public init() {} +} + +public extension Clock where Self == BenchmarkClock { + /// A clock that measures time that always increments but does not stop + /// incrementing while the system is asleep. + /// + /// try await Task.sleep(until: .now + .seconds(3), clock: .continuous) + /// + static var internalUTC: BenchmarkClock { return BenchmarkClock() } +} + +extension BenchmarkClock: Clock { + /// The current continuous instant. + public var now: BenchmarkClock.Instant { + BenchmarkClock.now + } + + /// The minimum non-zero resolution between any two calls to `now`. + public var minimumResolution: Swift.Duration { +#if os(macOS) + return Duration.nanoseconds(1) +#elseif os(Linux) + var resolution = timespec() + + let result = clock_getres(CLOCK_REALTIME, &resolution) + + guard result == 0 else { + fatalError("Failed to get realtime clock resolution in clock_getres(), errno = \(errno)") + } + + let seconds = Int64(resolution.tv_sec) + let attoseconds = Int64(resolution.tv_nsec) * 1_000_000_000 + + return Duration(secondsComponent: seconds, attosecondsComponent: attoseconds) +#else +#error("Unsupported Platform") +#endif + } + + /// The current continuous instant. + public static var now: BenchmarkClock.Instant { +#if os(macOS) + let nanos = clock_gettime_nsec_np(CLOCK_UPTIME_RAW) // to get ns resolution on macOS + + let seconds: UInt64 = nanos / 1_000_000_000 + let attoseconds: UInt64 = (nanos % 1_000_000_000) * 1_000_000_000 + return BenchmarkClock.Instant(_value: Duration(secondsComponent: Int64(seconds), + attosecondsComponent: Int64(attoseconds))) +#elseif os(Linux) + var timespec = timespec() + let result = clock_gettime(CLOCK_REALTIME, ×pec) + + guard result == 0 else { + fatalError("Failed to get current time in clock_gettime(), errno = \(errno)") + } + let seconds = Int64(timespec.tv_sec) + let attoseconds = Int64(timespec.tv_nsec) * 1_000_000_000 + + return BenchmarkClock.Instant(_value: Duration(secondsComponent: Int64(seconds), + attosecondsComponent: Int64(attoseconds))) +#else +#error("Unsupported Platform") +#endif + } + + /// Suspend task execution until a given deadline within a tolerance. + /// If no tolerance is specified then the system may adjust the deadline + /// to coalesce CPU wake-ups to more efficiently process the wake-ups in + /// a more power efficient manner. + /// + /// If the task is canceled before the time ends, this function throws + /// `CancellationError`. + /// + /// This function doesn't block the underlying thread. + public func sleep( + until deadline: Instant, tolerance: Swift.Duration? = nil + ) async throws { + try await Task.sleep(until: deadline, tolerance: tolerance, clock: .internalUTC) + } +} + +extension BenchmarkClock.Instant: InstantProtocol { + public static var now: BenchmarkClock.Instant { BenchmarkClock.now } + + public func advanced(by duration: Swift.Duration) -> BenchmarkClock.Instant { + return BenchmarkClock.Instant(_value: _value + duration) + } + + public func duration(to other: BenchmarkClock.Instant) -> Swift.Duration { + other._value - _value + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(_value) + } + + public static func == ( + _ lhs: BenchmarkClock.Instant, _ rhs: BenchmarkClock.Instant + ) -> Bool { + return lhs._value == rhs._value + } + + public static func < ( + _ lhs: BenchmarkClock.Instant, _ rhs: BenchmarkClock.Instant + ) -> Bool { + return lhs._value < rhs._value + } + + @inlinable + public static func + ( + _ lhs: BenchmarkClock.Instant, _ rhs: Swift.Duration + ) -> BenchmarkClock.Instant { + lhs.advanced(by: rhs) + } + + @inlinable + public static func += ( + _ lhs: inout BenchmarkClock.Instant, _ rhs: Swift.Duration + ) { + lhs = lhs.advanced(by: rhs) + } + + @inlinable + public static func - ( + _ lhs: BenchmarkClock.Instant, _ rhs: Swift.Duration + ) -> BenchmarkClock.Instant { + lhs.advanced(by: .zero - rhs) + } + + @inlinable + public static func -= ( + _ lhs: inout BenchmarkClock.Instant, _ rhs: Swift.Duration + ) { + lhs = lhs.advanced(by: .zero - rhs) + } + + @inlinable + public static func - ( + _ lhs: BenchmarkClock.Instant, _ rhs: BenchmarkClock.Instant + ) -> Swift.Duration { + rhs.duration(to: lhs) + } +} diff --git a/Sources/DateTime/Convenience.swift b/Sources/DateTime/Convenience.swift new file mode 100644 index 0000000..8a18d37 --- /dev/null +++ b/Sources/DateTime/Convenience.swift @@ -0,0 +1,5 @@ +public extension Duration { + func nanoseconds() -> Int64 { + return (self.components.seconds * 1_000_000_000) + (self.components.attoseconds / 1_000_000_000) + } +} diff --git a/Tests/DateTimeTests/DateTime.swift b/Tests/DateTimeTests/DateTime.swift index 87165d5..18baba3 100644 --- a/Tests/DateTimeTests/DateTime.swift +++ b/Tests/DateTimeTests/DateTime.swift @@ -12,15 +12,30 @@ import XCTest final class DateTimeTests: XCTestCase { func testExample() throws { let time = InternalUTCClock().measure { - print("Time now: \(InternalUTCClock.now)") - print("Time now: \(InternalUTCClock.now._value.components)") - print("Resolution: \(InternalUTCClock().minimumResolution)") + print("InternalUTCClock Time now: \(InternalUTCClock.now)") + print("InternalUTCClock Time now: \(InternalUTCClock.now._value.components)") + print("InternalUTCClock Resolution: \(InternalUTCClock().minimumResolution)") } - print("Elapsed time in nanoseconds \(time.components.attoseconds / 1_000_000_000)") + print("InternalUTCClock Elapsed time in nanoseconds \(time.components.attoseconds / 1_000_000_000)") let time2 = InternalUTCClock().measure { _ = 47 } - print("Elapsed time in nanoseconds for empty closure \(time2.components.attoseconds / 1_000_000_000)") + print("InternalUTCClock Elapsed time in nanoseconds for empty closure \(time2.components.attoseconds / 1_000_000_000)") + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + } + func testBenchmarkClock() throws { + let time = BenchmarkClock().measure { + print("BenchmarkClock Time now: \(BenchmarkClock.now)") + print("BenchmarkClock Time now: \(BenchmarkClock.now._value.components)") + print("BenchmarkClock Resolution: \(BenchmarkClock().minimumResolution)") + } + print("BenchmarkClock Elapsed time in nanoseconds \(time.components.attoseconds / 1_000_000_000)") + let time2 = BenchmarkClock().measure { + _ = 47 + } + print("BenchmarkClock Elapsed time in nanoseconds for empty closure \(time2.components.attoseconds / 1_000_000_000)") // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results.