313 lines
12 KiB
Swift
313 lines
12 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift Metrics API open source project
|
|
//
|
|
// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import MetricsTestKit
|
|
import XCTest
|
|
|
|
@testable import CoreMetrics
|
|
@testable import Metrics
|
|
|
|
class MetricsExtensionsTests: XCTestCase {
|
|
func testTimerBlock() throws {
|
|
// bootstrap with our test metrics
|
|
let metrics = TestMetrics()
|
|
MetricsSystem.bootstrapInternal(metrics)
|
|
// run the test
|
|
let name = "timer-\(UUID().uuidString)"
|
|
let delay = 0.05
|
|
Timer.measure(label: name) {
|
|
Thread.sleep(forTimeInterval: delay)
|
|
}
|
|
let timer = try metrics.expectTimer(name)
|
|
XCTAssertEqual(1, timer.values.count, "expected number of entries to match")
|
|
XCTAssertGreaterThan(timer.values[0], Int64(delay * 1_000_000_000), "expected delay to match")
|
|
}
|
|
|
|
func testTimerWithTimeInterval() throws {
|
|
// bootstrap with our test metrics
|
|
let metrics = TestMetrics()
|
|
MetricsSystem.bootstrapInternal(metrics)
|
|
// run the test
|
|
let timer = Timer(label: "test-timer")
|
|
let testTimer = try metrics.expectTimer(timer)
|
|
let timeInterval = TimeInterval(Double.random(in: 1...500))
|
|
timer.record(timeInterval)
|
|
XCTAssertEqual(1, testTimer.values.count, "expected number of entries to match")
|
|
XCTAssertEqual(testTimer.values[0], Int64(timeInterval * 1_000_000_000), "expected value to match")
|
|
}
|
|
|
|
func testTimerWithDispatchTime() throws {
|
|
// bootstrap with our test metrics
|
|
let metrics = TestMetrics()
|
|
MetricsSystem.bootstrapInternal(metrics)
|
|
// run the test
|
|
let timer = Timer(label: "test-timer")
|
|
let testTimer = try metrics.expectTimer(timer)
|
|
// nano
|
|
let nano = DispatchTimeInterval.nanoseconds(Int.random(in: 1...500))
|
|
timer.record(nano)
|
|
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
|
|
XCTAssertEqual(Int(testTimer.values[0]), nano.nano(), "expected value to match")
|
|
// micro
|
|
let micro = DispatchTimeInterval.microseconds(Int.random(in: 1...500))
|
|
timer.record(micro)
|
|
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
|
|
XCTAssertEqual(Int(testTimer.values[1]), micro.nano(), "expected value to match")
|
|
// milli
|
|
let milli = DispatchTimeInterval.milliseconds(Int.random(in: 1...500))
|
|
timer.record(milli)
|
|
XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
|
|
XCTAssertEqual(Int(testTimer.values[2]), milli.nano(), "expected value to match")
|
|
// seconds
|
|
let sec = DispatchTimeInterval.seconds(Int.random(in: 1...500))
|
|
timer.record(sec)
|
|
XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
|
|
XCTAssertEqual(Int(testTimer.values[3]), sec.nano(), "expected value to match")
|
|
// never
|
|
timer.record(DispatchTimeInterval.never)
|
|
XCTAssertEqual(testTimer.values.count, 5, "expected number of entries to match")
|
|
XCTAssertEqual(testTimer.values[4], 0, "expected value to match")
|
|
}
|
|
|
|
func testTimerWithDispatchTimeInterval() throws {
|
|
let metrics = TestMetrics()
|
|
MetricsSystem.bootstrapInternal(metrics)
|
|
|
|
let name = "timer-\(UUID().uuidString)"
|
|
|
|
let timer = Timer(label: name)
|
|
let start = DispatchTime.now()
|
|
let end = DispatchTime(uptimeNanoseconds: start.uptimeNanoseconds + 1000 * 1000 * 1000)
|
|
timer.recordInterval(since: start, end: end)
|
|
|
|
let testTimer = try metrics.expectTimer(timer)
|
|
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
|
|
XCTAssertEqual(
|
|
UInt64(testTimer.values.first!),
|
|
end.uptimeNanoseconds - start.uptimeNanoseconds,
|
|
"expected value to match"
|
|
)
|
|
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
|
|
}
|
|
|
|
func testTimerDuration() throws {
|
|
// Wrapping only the insides of the test case so that the generated
|
|
// tests on Linux in MetricsTests+XCTest don't complain that the func does not exist.
|
|
guard #available(iOS 16, macOS 13, tvOS 15, watchOS 8, *) else {
|
|
throw XCTSkip("Timer.record(_ duration: Duration) is not available on this platform")
|
|
}
|
|
|
|
let metrics = TestMetrics()
|
|
MetricsSystem.bootstrapInternal(metrics)
|
|
|
|
let name = "timer-\(UUID().uuidString)"
|
|
let timer = Timer(label: name)
|
|
|
|
let duration = Duration(secondsComponent: 3, attosecondsComponent: 123_000_000_000_000_000)
|
|
let nanoseconds = duration.components.seconds * 1_000_000_000 + duration.components.attoseconds / 1_000_000_000
|
|
timer.record(duration: duration)
|
|
|
|
// Record a Duration that would overflow,
|
|
// expect Int64.max to be recorded.
|
|
timer.record(duration: Duration(secondsComponent: 10_000_000_000, attosecondsComponent: 123))
|
|
|
|
let testTimer = try metrics.expectTimer(timer)
|
|
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
|
|
XCTAssertEqual(testTimer.values.first, nanoseconds, "expected value to match")
|
|
XCTAssertEqual(testTimer.values[1], Int64.max, "expected to record Int64.max if Durataion overflows")
|
|
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
|
|
}
|
|
|
|
func testTimerUnits() throws {
|
|
let metrics = TestMetrics()
|
|
MetricsSystem.bootstrapInternal(metrics)
|
|
|
|
let name = "timer-\(UUID().uuidString)"
|
|
let value = Int64.random(in: 0...1000)
|
|
|
|
let timer = Timer(label: name)
|
|
timer.recordNanoseconds(value)
|
|
|
|
let testTimer = try metrics.expectTimer(timer)
|
|
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
|
|
XCTAssertEqual(testTimer.values.first, value, "expected value to match")
|
|
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
|
|
|
|
let secondsName = "timer-seconds-\(UUID().uuidString)"
|
|
let secondsValue = Int64.random(in: 0...1000)
|
|
let secondsTimer = Timer(label: secondsName, preferredDisplayUnit: .seconds)
|
|
secondsTimer.recordSeconds(secondsValue)
|
|
|
|
let testSecondsTimer = try metrics.expectTimer(secondsTimer)
|
|
XCTAssertEqual(testSecondsTimer.values.count, 1, "expected number of entries to match")
|
|
XCTAssertEqual(metrics.timers.count, 2, "timer should have been stored")
|
|
}
|
|
|
|
func testPreferDisplayUnit() throws {
|
|
let metrics = TestMetrics()
|
|
MetricsSystem.bootstrapInternal(metrics)
|
|
|
|
let value = Double.random(in: 0...1000)
|
|
let timer = Timer(label: "test", preferredDisplayUnit: .seconds)
|
|
timer.recordSeconds(value)
|
|
|
|
let testTimer = try metrics.expectTimer(timer)
|
|
|
|
testTimer.preferDisplayUnit(.nanoseconds)
|
|
XCTAssertEqual(
|
|
testTimer.valueInPreferredUnit(atIndex: 0),
|
|
value * 1000 * 1000 * 1000,
|
|
accuracy: 1.0,
|
|
"expected value to match"
|
|
)
|
|
|
|
testTimer.preferDisplayUnit(.microseconds)
|
|
XCTAssertEqual(
|
|
testTimer.valueInPreferredUnit(atIndex: 0),
|
|
value * 1000 * 1000,
|
|
accuracy: 0.001,
|
|
"expected value to match"
|
|
)
|
|
|
|
testTimer.preferDisplayUnit(.milliseconds)
|
|
XCTAssertEqual(
|
|
testTimer.valueInPreferredUnit(atIndex: 0),
|
|
value * 1000,
|
|
accuracy: 0.000001,
|
|
"expected value to match"
|
|
)
|
|
|
|
testTimer.preferDisplayUnit(.seconds)
|
|
XCTAssertEqual(
|
|
testTimer.valueInPreferredUnit(atIndex: 0),
|
|
value,
|
|
accuracy: 0.000000001,
|
|
"expected value to match"
|
|
)
|
|
|
|
testTimer.preferDisplayUnit(.minutes)
|
|
XCTAssertEqual(
|
|
testTimer.valueInPreferredUnit(atIndex: 0),
|
|
value / 60,
|
|
accuracy: 0.000000001,
|
|
"expected value to match"
|
|
)
|
|
|
|
testTimer.preferDisplayUnit(.hours)
|
|
XCTAssertEqual(
|
|
testTimer.valueInPreferredUnit(atIndex: 0),
|
|
value / (60 * 60),
|
|
accuracy: 0.000000001,
|
|
"expected value to match"
|
|
)
|
|
|
|
testTimer.preferDisplayUnit(.days)
|
|
XCTAssertEqual(
|
|
testTimer.valueInPreferredUnit(atIndex: 0),
|
|
value / (60 * 60 * 24),
|
|
accuracy: 0.000000001,
|
|
"expected value to match"
|
|
)
|
|
}
|
|
|
|
#if compiler(>=6.0)
|
|
func testTimerMeasure() async throws {
|
|
// bootstrap with our test metrics
|
|
let metrics = TestMetrics()
|
|
MetricsSystem.bootstrapInternal(metrics)
|
|
// run the test
|
|
let name = "timer-\(UUID().uuidString)"
|
|
let delay = Duration.milliseconds(5)
|
|
let timer = Timer(label: name)
|
|
try await timer.measure {
|
|
try await Task.sleep(for: delay)
|
|
}
|
|
|
|
let expectedTimer = try metrics.expectTimer(name)
|
|
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
|
|
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
|
|
}
|
|
|
|
@MainActor
|
|
func testTimerMeasureFromMainActor() async throws {
|
|
// bootstrap with our test metrics
|
|
let metrics = TestMetrics()
|
|
MetricsSystem.bootstrapInternal(metrics)
|
|
// run the test
|
|
let name = "timer-\(UUID().uuidString)"
|
|
let delay = Duration.milliseconds(5)
|
|
let timer = Timer(label: name)
|
|
try await timer.measure {
|
|
try await Task.sleep(for: delay)
|
|
}
|
|
|
|
let expectedTimer = try metrics.expectTimer(name)
|
|
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
|
|
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// https://bugs.swift.org/browse/SR-6310
|
|
extension DispatchTimeInterval {
|
|
func nano() -> Int {
|
|
// This wrapping in a optional is a workaround because DispatchTimeInterval
|
|
// is a non-frozen public enum and Dispatch is built with library evolution
|
|
// mode turned on.
|
|
// This means we should have an `@unknown default` case, but this breaks
|
|
// on non-Darwin platforms.
|
|
// Switching over an optional means that the `.none` case will map to
|
|
// `default` (which means we'll always have a valid case to go into
|
|
// the default case), but in reality this case will never exist as this
|
|
// optional will never be nil.
|
|
let interval = Optional(self)
|
|
switch interval {
|
|
case .nanoseconds(let value):
|
|
return value
|
|
case .microseconds(let value):
|
|
return value * 1000
|
|
case .milliseconds(let value):
|
|
return value * 1_000_000
|
|
case .seconds(let value):
|
|
return value * 1_000_000_000
|
|
case .never:
|
|
return 0
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
}
|
|
|
|
#if swift(>=5.7)
|
|
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
|
|
extension Swift.Duration {
|
|
fileprivate var nanosecondsClamped: Int64 {
|
|
let components = self.components
|
|
|
|
let secondsComponentNanos = components.seconds.multipliedReportingOverflow(by: 1_000_000_000)
|
|
let attosCompononentNanos = components.attoseconds / 1_000_000_000
|
|
let combinedNanos = secondsComponentNanos.partialValue.addingReportingOverflow(attosCompononentNanos)
|
|
|
|
guard
|
|
!secondsComponentNanos.overflow,
|
|
!combinedNanos.overflow
|
|
else {
|
|
return .max
|
|
}
|
|
|
|
return combinedNanos.partialValue
|
|
}
|
|
}
|
|
#endif
|