From 42372a85980f8f27baa2dac3ba8a457dded19b2e Mon Sep 17 00:00:00 2001 From: Rauhul Varma Date: Thu, 29 Jul 2021 14:18:33 -0700 Subject: [PATCH] Introduce FloatingPointCounter motivation: It is not currently possible to record floating point values via the swift-metrics API even if the metrics backend supports it. modifications: Adds a `FloatingPointCounter` type to allow users to accumulate non-integral metrics backed by a `FloatingPointCounterHandler`. Introduces a default implementation for creating and destroying `FloatingPointCounterHandler`s for metric backends that do not natively support floating point counters. On such backends, `FloatingPointCounter` is backed by a `AccumulatingRoundingFloatingPointCounter` which accumulates floating point values internally and record increments to a wrapped `CounterHandler` after crossing integer boundaries. result: Users can create `FloatingPointCounter`s to record floating point values and get enhanced behavior for backends that support floating point values. --- Sources/CoreMetrics/Metrics.swift | 251 +++++++++++++++++- .../CoreMetricsTests+XCTest.swift | 6 + Tests/MetricsTests/CoreMetricsTests.swift | 108 ++++++++ Tests/MetricsTests/MetricsTests.swift | 14 +- Tests/MetricsTests/TestMetrics.swift | 10 +- 5 files changed, 376 insertions(+), 13 deletions(-) diff --git a/Sources/CoreMetrics/Metrics.swift b/Sources/CoreMetrics/Metrics.swift index edf1528..b2a2c8e 100644 --- a/Sources/CoreMetrics/Metrics.swift +++ b/Sources/CoreMetrics/Metrics.swift @@ -12,6 +12,10 @@ // //===----------------------------------------------------------------------===// +// MARK: Testing API + +internal var _enableAssertions = true + // MARK: User API extension Counter { @@ -90,6 +94,83 @@ extension Counter: CustomStringConvertible { } } +extension FloatingPointCounter { + /// Create a new `FloatingPointCounter`. + /// + /// - parameters: + /// - label: The label for the `FloatingPointCounter`. + /// - dimensions: The dimensions for the `FloatingPointCounter`. + public convenience init(label: String, dimensions: [(String, String)] = []) { + let handler = MetricsSystem.factory.makeFloatingPointCounter(label: label, dimensions: dimensions) + self.init(label: label, dimensions: dimensions, handler: handler) + } + + /// Signal the underlying metrics library that this FloatingPointCounter will never be updated again. + /// In response the library MAY decide to eagerly release any resources held by this `FloatingPointCounter`. + @inlinable + public func destroy() { + MetricsSystem.factory.destroyFloatingPointCounter(self.handler) + } +} + +/// A FloatingPointCounter is a cumulative metric that represents a single monotonically increasing FloatingPointCounter whose value can only increase or be reset to zero. +/// For example, you can use a FloatingPointCounter to represent the number of requests served, tasks completed, or errors. +/// FloatingPointCounter is not supported by all metrics backends, however a default implementation is provided which accumulates floating point increments and records increments to a standard Counter after crossing integer boundaries. +/// +/// This is the user-facing FloatingPointCounter API. +/// +/// Its behavior depends on the `FloatingCounterHandler` implementation. +public class FloatingPointCounter { + @usableFromInline + var handler: FloatingPointCounterHandler + public let label: String + public let dimensions: [(String, String)] + + /// Alternative way to create a new `FloatingPointCounter`, while providing an explicit `FloatingPointCounterHandler`. + /// + /// - warning: This initializer provides an escape hatch for situations where one must use a custom factory instead of the global one. + /// We do not expect this API to be used in normal circumstances, so if you find yourself using it make sure it's for a good reason. + /// + /// - SeeAlso: Use `init(label:dimensions:)` to create `FloatingPointCounter` instances using the configured metrics backend. + /// + /// - parameters: + /// - label: The label for the `FloatingPointCounter`. + /// - dimensions: The dimensions for the `FloatingPointCounter`. + /// - handler: The custom backend. + public init(label: String, dimensions: [(String, String)], handler: FloatingPointCounterHandler) { + self.label = label + self.dimensions = dimensions + self.handler = handler + } + + /// Increment the FloatingPointCounter. + /// + /// - parameters: + /// - by: Amount to increment by. + @inlinable + public func increment(by amount: DataType) { + self.handler.increment(by: Double(amount)) + } + + /// Increment the FloatingPointCounter by one. + @inlinable + public func increment() { + self.increment(by: 1.0) + } + + /// Reset the FloatingPointCounter back to zero. + @inlinable + public func reset() { + self.handler.reset() + } +} + +extension FloatingPointCounter: CustomStringConvertible { + public var description: String { + return "FloatingPointCounter(\(self.label), dimensions: \(self.dimensions))" + } +} + public extension Recorder { /// Create a new `Recorder`. /// @@ -422,6 +503,7 @@ public enum MetricsSystem { /// The `MetricsFactory` is the bridge between the `MetricsSystem` and the metrics backend implementation. /// `MetricsFactory`'s role is to initialize concrete implementations of the various metric types: /// * `Counter` -> `CounterHandler` +/// * `FloatingPointCounter` -> `FloatingPointCounterHandler` /// * `Recorder` -> `RecorderHandler` /// * `Timer` -> `TimerHandler` /// @@ -451,6 +533,13 @@ public protocol MetricsFactory { /// - dimensions: The dimensions for the `CounterHandler`. func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler + /// Create a backing `FloatingPointCounterHandler`. + /// + /// - parameters: + /// - label: The label for the `FloatingPointCounterHandler`. + /// - dimensions: The dimensions for the `FloatingPointCounterHandler`. + func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler + /// Create a backing `RecorderHandler`. /// /// - parameters: @@ -473,6 +562,13 @@ public protocol MetricsFactory { /// - handler: The handler to be destroyed. func destroyCounter(_ handler: CounterHandler) + /// Invoked when the corresponding `FloatingPointCounter`'s `destroy()` function is invoked. + /// Upon receiving this signal the factory may eagerly release any resources related to this counter. + /// + /// - parameters: + /// - handler: The handler to be destroyed. + func destroyFloatingPointCounter(_ handler: FloatingPointCounterHandler) + /// Invoked when the corresponding `Recorder`'s `destroy()` function is invoked. /// Upon receiving this signal the factory may eagerly release any resources related to this recorder. /// @@ -488,6 +584,106 @@ public protocol MetricsFactory { func destroyTimer(_ handler: TimerHandler) } +/// Wraps a CounterHandler, adding support for incrementing by floating point values by storing an accumulated floating point value and recording increments to the underlying CounterHandler after crossing integer boundaries. +internal class AccumulatingRoundingFloatingPointCounter: FloatingPointCounterHandler { + private let lock = Lock() + private let counterHandler: CounterHandler + internal var fraction: Double = 0 + + init(label: String, dimensions: [(String, String)]) { + self.counterHandler = MetricsSystem + .factory.makeCounter(label: label, dimensions: dimensions) + } + + func increment(by amount: Double) { + // Drop values in illegal values (Asserting in debug builds) + guard !amount.isNaN else { + assert(!_enableAssertions, "cannot increment by NaN") + return + } + + guard !amount.isInfinite else { + assert(!_enableAssertions, "cannot increment by infinite quantities") + return + } + + guard amount.sign == .plus else { + assert(!_enableAssertions, "cannot increment by negative values") + return + } + + guard !amount.isZero else { + return + } + + // If amount is in Int64.max..<+Inf + if amount.exponent >= 63 { + // Ceil to Int64.max + self.lock.withLockVoid { + self.counterHandler.increment(by: .max) + } + } else { + // Split amount into integer and fraction components + var (increment, fraction) = self.integerAndFractionComponents(of: amount) + self.lock.withLockVoid { + // Add the fractional component to the accumulated fraction. + self.fraction += fraction + // self.fraction may have cross an integer boundary, Split it + // and add any integer component. + let (integer, fraction) = integerAndFractionComponents(of: self.fraction) + increment += integer + self.fraction = fraction + // Increment the handler by the total integer component. + if increment > 0 { + self.counterHandler.increment(by: increment) + } + } + } + } + + @inline(__always) + private func integerAndFractionComponents(of value: Double) -> (Int64, Double) { + let integer = Int64(value) + let fraction = value - value.rounded(.towardZero) + return (integer, fraction) + } + + func reset() { + self.lock.withLockVoid { + self.fraction = 0 + self.counterHandler.reset() + } + } + + func destroy() { + MetricsSystem.factory.destroyCounter(self.counterHandler) + } +} + +extension MetricsFactory { + /// Create a default backing `FloatingPointCounterHandler` for backends which do not naively support floating point counters. + /// + /// The created FloatingPointCounterHandler is a wrapper around a backend's CounterHandler which accumulates floating point increments and records increments to an underlying CounterHandler after crossing integer boundaries. + /// + /// - parameters: + /// - label: The label for the `FloatingPointCounterHandler`. + /// - dimensions: The dimensions for the `FloatingPointCounterHandler`. + public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler { + return AccumulatingRoundingFloatingPointCounter(label: label, dimensions: dimensions) + } + + /// Invoked when the corresponding `FloatingPointCounter`'s `destroy()` function is invoked. + /// Upon receiving this signal the factory may eagerly release any resources related to this counter. + /// + /// `destroyFloatingPointCounter` must be implemented if `makeFloatingPointCounter` is implemented. + /// + /// - parameters: + /// - handler: The handler to be destroyed. + public func destroyFloatingPointCounter(_ handler: FloatingPointCounterHandler) { + (handler as? AccumulatingRoundingFloatingPointCounter)?.destroy() + } +} + /// A `CounterHandler` represents a backend implementation of a `Counter`. /// /// This type is an implementation detail and should not be used directly, unless implementing your own metrics backend. @@ -510,6 +706,28 @@ public protocol CounterHandler: AnyObject { func reset() } +/// A `FloatingPointCounterHandler` represents a backend implementation of a `FloatingPointCounter`. +/// +/// This type is an implementation detail and should not be used directly, unless implementing your own metrics backend. +/// To use the SwiftMetrics API, please refer to the documentation of `FloatingPointCounter`. +/// +/// # Implementation requirements +/// +/// To implement your own `FloatingPointCounterHandler` you should respect a few requirements that are necessary so applications work +/// as expected regardless of the selected `FloatingPointCounterHandler` implementation. +/// +/// - The `FloatingPointCounterHandler` must be a `class`. +public protocol FloatingPointCounterHandler: AnyObject { + /// Increment the counter. + /// + /// - parameters: + /// - by: Amount to increment by. + func increment(by: Double) + + /// Reset the counter back to zero. + func reset() +} + /// A `RecorderHandler` represents a backend implementation of a `Recorder`. /// /// This type is an implementation detail and should not be used directly, unless implementing your own metrics backend. @@ -578,6 +796,10 @@ public final class MultiplexMetricsHandler: MetricsFactory { return MuxCounter(factories: self.factories, label: label, dimensions: dimensions) } + public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler { + return MuxFloatingPointCounter(factories: self.factories, label: label, dimensions: dimensions) + } + public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { return MuxRecorder(factories: self.factories, label: label, dimensions: dimensions, aggregate: aggregate) } @@ -592,6 +814,12 @@ public final class MultiplexMetricsHandler: MetricsFactory { } } + public func destroyFloatingPointCounter(_ handler: FloatingPointCounterHandler) { + for factory in self.factories { + factory.destroyFloatingPointCounter(handler) + } + } + public func destroyRecorder(_ handler: RecorderHandler) { for factory in self.factories { factory.destroyRecorder(handler) @@ -619,6 +847,21 @@ public final class MultiplexMetricsHandler: MetricsFactory { } } + private class MuxFloatingPointCounter: FloatingPointCounterHandler { + let counters: [FloatingPointCounterHandler] + public init(factories: [MetricsFactory], label: String, dimensions: [(String, String)]) { + self.counters = factories.map { $0.makeFloatingPointCounter(label: label, dimensions: dimensions) } + } + + func increment(by amount: Double) { + self.counters.forEach { $0.increment(by: amount) } + } + + func reset() { + self.counters.forEach { $0.reset() } + } + } + private class MuxRecorder: RecorderHandler { let recorders: [RecorderHandler] public init(factories: [MetricsFactory], label: String, dimensions: [(String, String)], aggregate: Bool) { @@ -647,7 +890,7 @@ public final class MultiplexMetricsHandler: MetricsFactory { } /// Ships with the metrics module, used for initial bootstrapping. -public final class NOOPMetricsHandler: MetricsFactory, CounterHandler, RecorderHandler, TimerHandler { +public final class NOOPMetricsHandler: MetricsFactory, CounterHandler, FloatingPointCounterHandler, RecorderHandler, TimerHandler { public static let instance = NOOPMetricsHandler() private init() {} @@ -656,6 +899,10 @@ public final class NOOPMetricsHandler: MetricsFactory, CounterHandler, RecorderH return self } + public func makeFloatingPointCounter(label: String, dimensions: [(String, String)]) -> FloatingPointCounterHandler { + return self + } + public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { return self } @@ -665,10 +912,12 @@ public final class NOOPMetricsHandler: MetricsFactory, CounterHandler, RecorderH } public func destroyCounter(_: CounterHandler) {} + public func destroyFloatingPointCounter(_: FloatingPointCounterHandler) {} public func destroyRecorder(_: RecorderHandler) {} public func destroyTimer(_: TimerHandler) {} public func increment(by: Int64) {} + public func increment(by: Double) {} public func reset() {} public func record(_: Int64) {} public func record(_: Double) {} diff --git a/Tests/MetricsTests/CoreMetricsTests+XCTest.swift b/Tests/MetricsTests/CoreMetricsTests+XCTest.swift index 2f0274b..26cfbfd 100644 --- a/Tests/MetricsTests/CoreMetricsTests+XCTest.swift +++ b/Tests/MetricsTests/CoreMetricsTests+XCTest.swift @@ -27,6 +27,12 @@ extension MetricsTests { return [ ("testCounters", testCounters), ("testCounterBlock", testCounterBlock), + ("testDefaultFloatingPointCounter_ignoresNan", testDefaultFloatingPointCounter_ignoresNan), + ("testDefaultFloatingPointCounter_ignoresInfinity", testDefaultFloatingPointCounter_ignoresInfinity), + ("testDefaultFloatingPointCounter_ignoresNegativeValues", testDefaultFloatingPointCounter_ignoresNegativeValues), + ("testDefaultFloatingPointCounter_ignoresZero", testDefaultFloatingPointCounter_ignoresZero), + ("testDefaultFloatingPointCounter_ceilsExtremeValues", testDefaultFloatingPointCounter_ceilsExtremeValues), + ("testDefaultFloatingPointCounter_accumulatesFloatingPointDecimalValues", testDefaultFloatingPointCounter_accumulatesFloatingPointDecimalValues), ("testRecorders", testRecorders), ("testRecordersInt", testRecordersInt), ("testRecordersFloat", testRecordersFloat), diff --git a/Tests/MetricsTests/CoreMetricsTests.swift b/Tests/MetricsTests/CoreMetricsTests.swift index eeaaca0..fe4cf19 100644 --- a/Tests/MetricsTests/CoreMetricsTests.swift +++ b/Tests/MetricsTests/CoreMetricsTests.swift @@ -53,6 +53,114 @@ class MetricsTests: XCTestCase { XCTAssertEqual(counter.values.count, 0, "expected number of entries to match") } + func testDefaultFloatingPointCounter_ignoresNan() throws { + // bootstrap with our test metrics + let metrics = TestMetrics() + MetricsSystem.bootstrapInternal(metrics) + // disable assertions to test fallback path + _enableAssertions = false + let label = "\(#function)-fp-counter-\(UUID())" + let fpCounter = FloatingPointCounter(label: label) + let counter = metrics.counters[label] as! TestCounter + fpCounter.increment(by: Double.nan) + fpCounter.increment(by: Double.signalingNaN) + XCTAssertEqual(counter.values.count, 0, "expected nan values to be ignored") + // reenable assertions + _enableAssertions = true + } + + func testDefaultFloatingPointCounter_ignoresInfinity() throws { + // bootstrap with our test metrics + let metrics = TestMetrics() + MetricsSystem.bootstrapInternal(metrics) + // disable assertions to test fallback path + _enableAssertions = false + let label = "\(#function)-fp-counter-\(UUID())" + let fpCounter = FloatingPointCounter(label: label) + let counter = metrics.counters[label] as! TestCounter + fpCounter.increment(by: Double.infinity) + fpCounter.increment(by: -Double.infinity) + XCTAssertEqual(counter.values.count, 0, "expected infinite values to be ignored") + // reenable assertions + _enableAssertions = true + } + + func testDefaultFloatingPointCounter_ignoresNegativeValues() throws { + // bootstrap with our test metrics + let metrics = TestMetrics() + MetricsSystem.bootstrapInternal(metrics) + // disable assertions to test fallback path + _enableAssertions = false + let label = "\(#function)-fp-counter-\(UUID())" + let fpCounter = FloatingPointCounter(label: label) + let counter = metrics.counters[label] as! TestCounter + fpCounter.increment(by: -100) + XCTAssertEqual(counter.values.count, 0, "expected negative values to be ignored") + // reenable assertions + _enableAssertions = true + } + + func testDefaultFloatingPointCounter_ignoresZero() throws { + // bootstrap with our test metrics + let metrics = TestMetrics() + MetricsSystem.bootstrapInternal(metrics) + // disable assertions to test fallback path + _enableAssertions = false + let label = "\(#function)-fp-counter-\(UUID())" + let fpCounter = FloatingPointCounter(label: label) + let counter = metrics.counters[label] as! TestCounter + fpCounter.increment(by: 0) + fpCounter.increment(by: -0) + XCTAssertEqual(counter.values.count, 0, "expected zero values to be ignored") + // reenable assertions + _enableAssertions = true + } + + func testDefaultFloatingPointCounter_ceilsExtremeValues() { + // bootstrap with our test metrics + let metrics = TestMetrics() + MetricsSystem.bootstrapInternal(metrics) + let label = "\(#function)-fp-counter-\(UUID())" + let fpCounter = FloatingPointCounter(label: label) + let counter = metrics.counters[label] as! TestCounter + // Just larger than Int64 + fpCounter.increment(by: Double(sign: .plus, exponent: 63, significand: 1)) + // Much larger than Int64 + fpCounter.increment(by: Double.greatestFiniteMagnitude) + let values = counter.values.map { $0.1 } + XCTAssertEqual(values.count, 2, "expected number of entries to match") + XCTAssertEqual(values, [Int64.max, Int64.max], "expected extremely large values to be replaced with Int64.max") + } + + func testDefaultFloatingPointCounter_accumulatesFloatingPointDecimalValues() { + // bootstrap with our test metrics + let metrics = TestMetrics() + MetricsSystem.bootstrapInternal(metrics) + let label = "\(#function)-fp-counter-\(UUID())" + let fpCounter = FloatingPointCounter(label: label) + let rawFpCounter = fpCounter.handler as! AccumulatingRoundingFloatingPointCounter + let counter = metrics.counters[label] as! TestCounter + + // Increment by a small value (perfectly representable) + fpCounter.increment(by: 0.75) + XCTAssertEqual(counter.values.count, 0, "expected number of entries to match") + + // Increment by a small value that should grow the accumulated buffer past 1.0 (perfectly representable) + fpCounter.increment(by: 1.5) + var values = counter.values.map { $0.1 } + XCTAssertEqual(values.count, 1, "expected number of entries to match") + XCTAssertEqual(values, [2], "expected entries to match") + XCTAssertEqual(rawFpCounter.fraction, 0.25, "") + + // Increment by a large value that should leave a fraction in the accumulator + // 1110506744053.76 + fpCounter.increment(by: Double(sign: .plus, exponent: 40, significand: 1.01)) + values = counter.values.map { $0.1 } + XCTAssertEqual(values.count, 2, "expected number of entries to match") + XCTAssertEqual(values, [2, 1_110_506_744_054], "expected entries to match") + XCTAssertEqual(rawFpCounter.fraction, 0.010009765625, "expected fractional accumulated value") + } + func testRecorders() throws { // bootstrap with our test metrics let metrics = TestMetrics() diff --git a/Tests/MetricsTests/MetricsTests.swift b/Tests/MetricsTests/MetricsTests.swift index 9c33f63..4f24e49 100644 --- a/Tests/MetricsTests/MetricsTests.swift +++ b/Tests/MetricsTests/MetricsTests.swift @@ -131,25 +131,25 @@ class MetricsExtensionsTests: XCTestCase { let testTimer = timer.handler as! TestTimer testTimer.preferDisplayUnit(.nanoseconds) - XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000 * 1000 * 1000, accuracy: 1.0, "expected value to match") + XCTAssertEqual(testTimer.retrieveValueInPreferredUnit(atIndex: 0), value * 1000 * 1000 * 1000, accuracy: 1.0, "expected value to match") testTimer.preferDisplayUnit(.microseconds) - XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000 * 1000, accuracy: 0.001, "expected value to match") + XCTAssertEqual(testTimer.retrieveValueInPreferredUnit(atIndex: 0), value * 1000 * 1000, accuracy: 0.001, "expected value to match") testTimer.preferDisplayUnit(.milliseconds) - XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000, accuracy: 0.000001, "expected value to match") + XCTAssertEqual(testTimer.retrieveValueInPreferredUnit(atIndex: 0), value * 1000, accuracy: 0.000001, "expected value to match") testTimer.preferDisplayUnit(.seconds) - XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value, accuracy: 0.000000001, "expected value to match") + XCTAssertEqual(testTimer.retrieveValueInPreferredUnit(atIndex: 0), value, accuracy: 0.000000001, "expected value to match") testTimer.preferDisplayUnit(.minutes) - XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / 60, accuracy: 0.000000001, "expected value to match") + XCTAssertEqual(testTimer.retrieveValueInPreferredUnit(atIndex: 0), value / 60, accuracy: 0.000000001, "expected value to match") testTimer.preferDisplayUnit(.hours) - XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / (60 * 60), accuracy: 0.000000001, "expected value to match") + XCTAssertEqual(testTimer.retrieveValueInPreferredUnit(atIndex: 0), value / (60 * 60), accuracy: 0.000000001, "expected value to match") testTimer.preferDisplayUnit(.days) - XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / (60 * 60 * 24), accuracy: 0.000000001, "expected value to match") + XCTAssertEqual(testTimer.retrieveValueInPreferredUnit(atIndex: 0), value / (60 * 60 * 24), accuracy: 0.000000001, "expected value to match") } } diff --git a/Tests/MetricsTests/TestMetrics.swift b/Tests/MetricsTests/TestMetrics.swift index f46bb02..9796fd7 100644 --- a/Tests/MetricsTests/TestMetrics.swift +++ b/Tests/MetricsTests/TestMetrics.swift @@ -24,18 +24,18 @@ internal final class TestMetrics: MetricsFactory { var recorders = [String: RecorderHandler]() var timers = [String: TimerHandler]() - public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler { + func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler { return self.make(label: label, dimensions: dimensions, registry: &self.counters, maker: TestCounter.init) } - public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { + func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler { let maker = { (label: String, dimensions: [(String, String)]) -> RecorderHandler in TestRecorder(label: label, dimensions: dimensions, aggregate: aggregate) } return self.make(label: label, dimensions: dimensions, registry: &self.recorders, maker: maker) } - public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler { + func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler { return self.make(label: label, dimensions: dimensions, registry: &self.timers, maker: TestTimer.init) } @@ -154,7 +154,7 @@ internal class TestTimer: TimerHandler, Equatable { } } - func retriveValueInPreferredUnit(atIndex i: Int) -> Double { + func retrieveValueInPreferredUnit(atIndex i: Int) -> Double { return self.lock.withLock { let value = values[i].1 guard let displayUnit = self.displayUnit else { @@ -171,7 +171,7 @@ internal class TestTimer: TimerHandler, Equatable { print("recording \(duration) \(self.label)") } - public static func == (lhs: TestTimer, rhs: TestTimer) -> Bool { + static func == (lhs: TestTimer, rhs: TestTimer) -> Bool { return lhs.id == rhs.id } }