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.
This commit is contained in:
parent
99a068b962
commit
42372a8598
|
|
@ -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<DataType: BinaryFloatingPoint>(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) {}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue