initial commit
This commit is contained in:
commit
d01123e590
|
|
@ -0,0 +1,5 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
.xcode
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// swift-tools-version:4.2
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "metrics",
|
||||
products: [
|
||||
.library(name: "CoreMetrics", targets: ["CoreMetrics"]),
|
||||
.library(name: "Metrics", targets: ["Metrics"]),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "CoreMetrics",
|
||||
dependencies: []
|
||||
),
|
||||
.target(
|
||||
name: "Metrics",
|
||||
dependencies: ["CoreMetrics"]
|
||||
),
|
||||
.target(
|
||||
name: "Examples",
|
||||
dependencies: ["Metrics"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "MetricsTests",
|
||||
dependencies: ["Metrics"]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# SSWG metrics api
|
||||
|
||||
## Providing Feedback
|
||||
|
||||
Feedback that would really be great is:
|
||||
- if anything, what does this proposal *not cover* that you will definitely need
|
||||
- if anything, what could we remove from this and still be happy?
|
||||
- API-wise: what do you like, what don't you like?
|
||||
|
||||
Feel free to post this as message on the SSWG forum and/or github issues in this repo.
|
||||
|
||||
## Open Questions
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
import Darwin
|
||||
#else
|
||||
import Glibc
|
||||
#endif
|
||||
|
||||
/// A threading lock based on `libpthread` instead of `libdispatch`.
|
||||
///
|
||||
/// This object provides a lock on top of a single `pthread_mutex_t`. This kind
|
||||
/// of lock is safe to use with `libpthread`-based threading models, such as the
|
||||
/// one used by NIO.
|
||||
internal final class Lock {
|
||||
fileprivate let mutex: UnsafeMutablePointer<pthread_mutex_t> = UnsafeMutablePointer.allocate(capacity: 1)
|
||||
|
||||
/// Create a new lock.
|
||||
public init() {
|
||||
let err = pthread_mutex_init(self.mutex, nil)
|
||||
precondition(err == 0)
|
||||
}
|
||||
|
||||
deinit {
|
||||
let err = pthread_mutex_destroy(self.mutex)
|
||||
precondition(err == 0)
|
||||
self.mutex.deallocate()
|
||||
}
|
||||
|
||||
/// Acquire the lock.
|
||||
///
|
||||
/// Whenever possible, consider using `withLock` instead of this method and
|
||||
/// `unlock`, to simplify lock handling.
|
||||
public func lock() {
|
||||
let err = pthread_mutex_lock(self.mutex)
|
||||
precondition(err == 0)
|
||||
}
|
||||
|
||||
/// Release the lock.
|
||||
///
|
||||
/// Whenver possible, consider using `withLock` instead of this method and
|
||||
/// `lock`, to simplify lock handling.
|
||||
public func unlock() {
|
||||
let err = pthread_mutex_unlock(self.mutex)
|
||||
precondition(err == 0)
|
||||
}
|
||||
}
|
||||
|
||||
extension Lock {
|
||||
/// Acquire the lock for the duration of the given block.
|
||||
///
|
||||
/// This convenience method should be preferred to `lock` and `unlock` in
|
||||
/// most situations, as it ensures that the lock will be released regardless
|
||||
/// of how `body` exits.
|
||||
///
|
||||
/// - Parameter body: The block to execute while holding the lock.
|
||||
/// - Returns: The value returned by the block.
|
||||
@inlinable
|
||||
public func withLock<T>(_ body: () throws -> T) rethrows -> T {
|
||||
self.lock()
|
||||
defer {
|
||||
self.unlock()
|
||||
}
|
||||
return try body()
|
||||
}
|
||||
|
||||
// specialise Void return (for performance)
|
||||
@inlinable
|
||||
public func withLockVoid(_ body: () throws -> Void) rethrows {
|
||||
try self.withLock(body)
|
||||
}
|
||||
}
|
||||
|
||||
/// A threading lock based on `libpthread` instead of `libdispatch`.
|
||||
///
|
||||
/// This object provides a lock on top of a single `pthread_mutex_t`. This kind
|
||||
/// of lock is safe to use with `libpthread`-based threading models, such as the
|
||||
/// one used by NIO.
|
||||
internal final class ReadWriteLock {
|
||||
fileprivate let rwlock: UnsafeMutablePointer<pthread_rwlock_t> = UnsafeMutablePointer.allocate(capacity: 1)
|
||||
|
||||
/// Create a new lock.
|
||||
public init() {
|
||||
let err = pthread_rwlock_init(self.rwlock, nil)
|
||||
precondition(err == 0)
|
||||
}
|
||||
|
||||
deinit {
|
||||
let err = pthread_rwlock_destroy(self.rwlock)
|
||||
precondition(err == 0)
|
||||
self.rwlock.deallocate()
|
||||
}
|
||||
|
||||
/// Acquire a reader lock.
|
||||
///
|
||||
/// Whenever possible, consider using `withLock` instead of this method and
|
||||
/// `unlock`, to simplify lock handling.
|
||||
public func lockRead() {
|
||||
let err = pthread_rwlock_rdlock(self.rwlock)
|
||||
precondition(err == 0)
|
||||
}
|
||||
|
||||
/// Acquire a writer lock.
|
||||
///
|
||||
/// Whenever possible, consider using `withLock` instead of this method and
|
||||
/// `unlock`, to simplify lock handling.
|
||||
public func lockWrite() {
|
||||
let err = pthread_rwlock_wrlock(self.rwlock)
|
||||
precondition(err == 0)
|
||||
}
|
||||
|
||||
/// Release the lock.
|
||||
///
|
||||
/// Whenver possible, consider using `withLock` instead of this method and
|
||||
/// `lock`, to simplify lock handling.
|
||||
public func unlock() {
|
||||
let err = pthread_rwlock_unlock(self.rwlock)
|
||||
precondition(err == 0)
|
||||
}
|
||||
}
|
||||
|
||||
extension ReadWriteLock {
|
||||
/// Acquire the reader lock for the duration of the given block.
|
||||
///
|
||||
/// This convenience method should be preferred to `lock` and `unlock` in
|
||||
/// most situations, as it ensures that the lock will be released regardless
|
||||
/// of how `body` exits.
|
||||
///
|
||||
/// - Parameter body: The block to execute while holding the lock.
|
||||
/// - Returns: The value returned by the block.
|
||||
@inlinable
|
||||
public func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
|
||||
self.lockRead()
|
||||
defer {
|
||||
self.unlock()
|
||||
}
|
||||
return try body()
|
||||
}
|
||||
|
||||
/// Acquire the writer lock for the duration of the given block.
|
||||
///
|
||||
/// This convenience method should be preferred to `lock` and `unlock` in
|
||||
/// most situations, as it ensures that the lock will be released regardless
|
||||
/// of how `body` exits.
|
||||
///
|
||||
/// - Parameter body: The block to execute while holding the lock.
|
||||
/// - Returns: The value returned by the block.
|
||||
@inlinable
|
||||
public func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
|
||||
self.lockWrite()
|
||||
defer {
|
||||
self.unlock()
|
||||
}
|
||||
return try body()
|
||||
}
|
||||
|
||||
// specialise Void return (for performance)
|
||||
@inlinable
|
||||
public func withReaderLockVoid(_ body: () throws -> Void) rethrows {
|
||||
try self.withReaderLock(body)
|
||||
}
|
||||
|
||||
// specialise Void return (for performance)
|
||||
@inlinable
|
||||
public func withWriterLockVoid(_ body: () throws -> Void) rethrows {
|
||||
try self.withWriterLock(body)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
public protocol Counter: AnyObject {
|
||||
func increment<DataType: BinaryInteger>(_ value: DataType)
|
||||
}
|
||||
|
||||
public extension Counter {
|
||||
@inlinable
|
||||
func increment() {
|
||||
self.increment(1)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol Recorder: AnyObject {
|
||||
func record<DataType: BinaryInteger>(_ value: DataType)
|
||||
func record<DataType: BinaryFloatingPoint>(_ value: DataType)
|
||||
}
|
||||
|
||||
public protocol Timer: AnyObject {
|
||||
func recordNanoseconds(_ duration: Int64)
|
||||
}
|
||||
|
||||
public extension Timer {
|
||||
@inlinable
|
||||
func recordMicroseconds<DataType: BinaryInteger>(_ duration: DataType) {
|
||||
self.recordNanoseconds(Int64(duration) * 1000)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func recordMicroseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
|
||||
self.recordNanoseconds(Int64(duration * 1000))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func recordMilliseconds<DataType: BinaryInteger>(_ duration: DataType) {
|
||||
self.recordNanoseconds(Int64(duration) * 1_000_000)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func recordMilliseconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
|
||||
self.recordNanoseconds(Int64(duration * 1_000_000))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func recordSeconds<DataType: BinaryInteger>(_ duration: DataType) {
|
||||
self.recordNanoseconds(Int64(duration) * 1_000_000_000)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func recordSeconds<DataType: BinaryFloatingPoint>(_ duration: DataType) {
|
||||
self.recordNanoseconds(Int64(duration * 1_000_000_000))
|
||||
}
|
||||
}
|
||||
|
||||
public protocol MetricsHandler {
|
||||
func makeCounter(label: String, dimensions: [(String, String)]) -> Counter
|
||||
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder
|
||||
func makeTimer(label: String, dimensions: [(String, String)]) -> Timer
|
||||
}
|
||||
|
||||
public extension MetricsHandler {
|
||||
@inlinable
|
||||
func makeCounter(label: String) -> Counter {
|
||||
return self.makeCounter(label: label, dimensions: [])
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func makeRecorder(label: String, aggregate: Bool = true) -> Recorder {
|
||||
return self.makeRecorder(label: label, dimensions: [], aggregate: aggregate)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func makeTimer(label: String) -> Timer {
|
||||
return self.makeTimer(label: label, dimensions: [])
|
||||
}
|
||||
}
|
||||
|
||||
public extension MetricsHandler {
|
||||
@inlinable
|
||||
func makeGauge(label: String, dimensions: [(String, String)] = []) -> Recorder {
|
||||
return self.makeRecorder(label: label, dimensions: dimensions, aggregate: false)
|
||||
}
|
||||
}
|
||||
|
||||
public extension MetricsHandler {
|
||||
@inlinable
|
||||
func withCounter(label: String, dimensions: [(String, String)] = [], then: (Counter) -> Void) {
|
||||
then(self.makeCounter(label: label, dimensions: dimensions))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func withRecorder(label: String, dimensions: [(String, String)] = [], aggregate: Bool = true, then: (Recorder) -> Void) {
|
||||
then(self.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func withTimer(label: String, dimensions: [(String, String)] = [], then: (Timer) -> Void) {
|
||||
then(self.makeTimer(label: label, dimensions: dimensions))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func withGauge(label: String, dimensions: [(String, String)] = [], then: (Recorder) -> Void) {
|
||||
then(self.makeGauge(label: label, dimensions: dimensions))
|
||||
}
|
||||
}
|
||||
|
||||
public enum Metrics {
|
||||
private static let lock = ReadWriteLock()
|
||||
private static var _handler: MetricsHandler = NOOPMetricsHandler.instance
|
||||
|
||||
public static func bootstrap(_ handler: MetricsHandler) {
|
||||
self.lock.withWriterLockVoid {
|
||||
// using a wrapper to avoid redundant and potentially expensive factory calls
|
||||
self._handler = CachingMetricsHandler.wrap(handler)
|
||||
}
|
||||
}
|
||||
|
||||
public static var global: MetricsHandler {
|
||||
return self.lock.withReaderLock { self._handler }
|
||||
}
|
||||
}
|
||||
|
||||
private final class CachingMetricsHandler: MetricsHandler {
|
||||
private let wrapped: MetricsHandler
|
||||
private let lock = Lock() // TODO: consider lock per cache?
|
||||
private var counters = [String: Counter]()
|
||||
private var Recorders = [String: Recorder]()
|
||||
private var timers = [String: Timer]()
|
||||
|
||||
public static func wrap(_ handler: MetricsHandler) -> CachingMetricsHandler {
|
||||
if let caching = handler as? CachingMetricsHandler {
|
||||
return caching
|
||||
} else {
|
||||
return CachingMetricsHandler(handler)
|
||||
}
|
||||
}
|
||||
|
||||
private init(_ wrapped: MetricsHandler) {
|
||||
self.wrapped = wrapped
|
||||
}
|
||||
|
||||
public func makeCounter(label: String, dimensions: [(String, String)]) -> Counter {
|
||||
return self.make(label: label, dimensions: dimensions, cache: &self.counters, maker: self.wrapped.makeCounter)
|
||||
}
|
||||
|
||||
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder {
|
||||
let maker = { (label: String, dimensions: [(String, String)]) -> Recorder in
|
||||
self.wrapped.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
|
||||
}
|
||||
return self.make(label: label, dimensions: dimensions, cache: &self.Recorders, maker: maker)
|
||||
}
|
||||
|
||||
public func makeTimer(label: String, dimensions: [(String, String)]) -> Timer {
|
||||
return self.make(label: label, dimensions: dimensions, cache: &self.timers, maker: self.wrapped.makeTimer)
|
||||
}
|
||||
|
||||
private func make<Item>(label: String, dimensions: [(String, String)], cache: inout [String: Item], maker: (String, [(String, String)]) -> Item) -> Item {
|
||||
let fqn = self.fqn(label: label, dimensions: dimensions)
|
||||
return self.lock.withLock {
|
||||
if let item = cache[fqn] {
|
||||
return item
|
||||
} else {
|
||||
let item = maker(label, dimensions)
|
||||
cache[fqn] = item
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fqn(label: String, dimensions: [(String, String)]) -> String {
|
||||
return [[label], dimensions.compactMap { $0.1 }].flatMap { $0 }.joined(separator: ".")
|
||||
}
|
||||
}
|
||||
|
||||
public final class MultiplexMetricsHandler: MetricsHandler {
|
||||
private let handlers: [MetricsHandler]
|
||||
public init(handlers: [MetricsHandler]) {
|
||||
self.handlers = handlers
|
||||
}
|
||||
|
||||
public func makeCounter(label: String, dimensions: [(String, String)]) -> Counter {
|
||||
return MuxCounter(handlers: self.handlers, label: label, dimensions: dimensions)
|
||||
}
|
||||
|
||||
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder {
|
||||
return MuxRecorder(handlers: self.handlers, label: label, dimensions: dimensions, aggregate: aggregate)
|
||||
}
|
||||
|
||||
public func makeTimer(label: String, dimensions: [(String, String)]) -> Timer {
|
||||
return MuxTimer(handlers: self.handlers, label: label, dimensions: dimensions)
|
||||
}
|
||||
|
||||
private class MuxCounter: Counter {
|
||||
let counters: [Counter]
|
||||
public init(handlers: [MetricsHandler], label: String, dimensions: [(String, String)]) {
|
||||
self.counters = handlers.map { $0.makeCounter(label: label, dimensions: dimensions) }
|
||||
}
|
||||
|
||||
func increment<DataType: BinaryInteger>(_ value: DataType) {
|
||||
self.counters.forEach { $0.increment(value) }
|
||||
}
|
||||
}
|
||||
|
||||
private class MuxRecorder: Recorder {
|
||||
let recorders: [Recorder]
|
||||
public init(handlers: [MetricsHandler], label: String, dimensions: [(String, String)], aggregate: Bool) {
|
||||
self.recorders = handlers.map { $0.makeRecorder(label: label, dimensions: dimensions, aggregate: aggregate) }
|
||||
}
|
||||
|
||||
func record<DataType: BinaryInteger>(_ value: DataType) {
|
||||
self.recorders.forEach { $0.record(value) }
|
||||
}
|
||||
|
||||
func record<DataType: BinaryFloatingPoint>(_ value: DataType) {
|
||||
self.recorders.forEach { $0.record(value) }
|
||||
}
|
||||
}
|
||||
|
||||
private class MuxTimer: Timer {
|
||||
let timers: [Timer]
|
||||
public init(handlers: [MetricsHandler], label: String, dimensions: [(String, String)]) {
|
||||
self.timers = handlers.map { $0.makeTimer(label: label, dimensions: dimensions) }
|
||||
}
|
||||
|
||||
func recordNanoseconds(_ duration: Int64) {
|
||||
self.timers.forEach { $0.recordNanoseconds(duration) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class NOOPMetricsHandler: MetricsHandler, Counter, Recorder, Timer {
|
||||
public static let instance = NOOPMetricsHandler()
|
||||
|
||||
private init() {}
|
||||
|
||||
public func makeCounter(label: String, dimensions: [(String, String)]) -> Counter {
|
||||
return self
|
||||
}
|
||||
|
||||
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder {
|
||||
return self
|
||||
}
|
||||
|
||||
public func makeTimer(label: String, dimensions: [(String, String)]) -> Timer {
|
||||
return self
|
||||
}
|
||||
|
||||
public func increment<DataType: BinaryInteger>(_: DataType) {}
|
||||
public func record<DataType: BinaryInteger>(_: DataType) {}
|
||||
public func record<DataType: BinaryFloatingPoint>(_: DataType) {}
|
||||
public func recordNanoseconds(_: Int64) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import Metrics
|
||||
|
||||
enum Example1 {
|
||||
static func main() {
|
||||
// bootstrap with our example metrics library
|
||||
let metrics = ExampleMetricsLibrary()
|
||||
Metrics.bootstrap(metrics)
|
||||
|
||||
let server = Server()
|
||||
let client = Client(server: server)
|
||||
client.run(iterations: Int.random(in: 10 ... 50))
|
||||
|
||||
print("-----> counters")
|
||||
metrics.counters.forEach { print(" \($0)") }
|
||||
print("-----> recorders")
|
||||
metrics.recorders.forEach { print(" \($0)") }
|
||||
print("-----> timers")
|
||||
metrics.timers.forEach { print(" \($0)") }
|
||||
print("-----> gauges")
|
||||
metrics.gauges.forEach { print(" \($0)") }
|
||||
}
|
||||
|
||||
class Client {
|
||||
private let activeRequestsGauge = Metrics.global.makeGauge(label: "Client::ActiveRequests")
|
||||
private let server: Server
|
||||
init(server: Server) {
|
||||
self.server = server
|
||||
}
|
||||
|
||||
func run(iterations: Int) {
|
||||
let group = DispatchGroup()
|
||||
let requestsCounter = Metrics.global.makeCounter(label: "Client::TotalRequests")
|
||||
let requestTimer = Metrics.global.makeTimer(label: "Client::doSomethig")
|
||||
let resultRecorder = Metrics.global.makeRecorder(label: "Client::doSomethig::result")
|
||||
for _ in 0 ... iterations {
|
||||
group.enter()
|
||||
let start = Date()
|
||||
requestsCounter.increment()
|
||||
self.activeRequests += 1
|
||||
server.doSomethig { result in
|
||||
requestTimer.record(Date().timeIntervalSince(start))
|
||||
resultRecorder.record(result)
|
||||
self.activeRequests -= 1
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
group.wait()
|
||||
}
|
||||
|
||||
private let lock = NSLock()
|
||||
private var _activeRequests = 0
|
||||
var activeRequests: Int {
|
||||
get {
|
||||
return self.lock.withLock { _activeRequests }
|
||||
} set {
|
||||
self.lock.withLock { _activeRequests = newValue }
|
||||
self.activeRequestsGauge.record(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Server {
|
||||
let library = RandomLibrary()
|
||||
let requestsCounter = Metrics.global.makeCounter(label: "Server::TotalRequests")
|
||||
|
||||
func doSomethig(callback: @escaping (Int64) -> Void) {
|
||||
let timer = Metrics.global.makeTimer(label: "Server::doSomethig")
|
||||
let start = Date()
|
||||
requestsCounter.increment()
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 5 ... 500))) {
|
||||
self.library.doSomething()
|
||||
self.library.doSomethingSlow {
|
||||
timer.record(Date().timeIntervalSince(start))
|
||||
callback(Int64.random(in: 0 ... 1000))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension NSLock {
|
||||
func withLock<T>(_ body: () -> T) -> T {
|
||||
self.lock()
|
||||
defer {
|
||||
self.unlock()
|
||||
}
|
||||
return body()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
import Metrics
|
||||
|
||||
class ExampleMetricsLibrary: MetricsHandler {
|
||||
private let config: Config
|
||||
private let lock = NSLock()
|
||||
var counters = [ExampleCounter]()
|
||||
var recorders = [ExampleRecorder]()
|
||||
var gauges = [ExampleGauge]()
|
||||
var timers = [ExampleTimer]()
|
||||
|
||||
init(config: Config = Config()) {
|
||||
self.config = config
|
||||
}
|
||||
|
||||
func makeCounter(label: String, dimensions: [(String, String)]) -> Counter {
|
||||
return self.register(label: label, dimensions: dimensions, registry: &self.counters, maker: ExampleCounter.init)
|
||||
}
|
||||
|
||||
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder {
|
||||
let options = aggregate ? self.config.recorder.aggregationOptions : nil
|
||||
return self.makeRecorder(label: label, dimensions: dimensions, options: options)
|
||||
}
|
||||
|
||||
func makeRecorder(label: String, dimensions: [(String, String)], options: [AggregationOption]?) -> Recorder {
|
||||
guard let options = options else {
|
||||
return self.register(label: label, dimensions: dimensions, registry: &self.gauges, maker: ExampleGauge.init)
|
||||
}
|
||||
let maker = { (label: String, dimensions: [(String, String)]) -> ExampleRecorder in
|
||||
ExampleRecorder(label: label, dimensions: dimensions, options: options)
|
||||
}
|
||||
return self.register(label: label, dimensions: dimensions, registry: &self.recorders, maker: maker)
|
||||
}
|
||||
|
||||
func makeTimer(label: String, dimensions: [(String, String)]) -> Timer {
|
||||
return self.makeTimer(label: label, dimensions: dimensions, options: self.config.timer.aggregationOptions)
|
||||
}
|
||||
|
||||
func makeTimer(label: String, dimensions: [(String, String)], options: [AggregationOption]) -> Timer {
|
||||
let maker = { (label: String, dimensions: [(String, String)]) -> ExampleTimer in
|
||||
ExampleTimer(label: label, dimensions: dimensions, options: options)
|
||||
}
|
||||
return self.register(label: label, dimensions: dimensions, registry: &self.timers, maker: maker)
|
||||
}
|
||||
|
||||
func register<Item>(label: String, dimensions: [(String, String)], registry: inout [Item], maker: (String, [(String, String)]) -> Item) -> Item {
|
||||
let item = maker(label, dimensions)
|
||||
lock.withLock {
|
||||
registry.append(item)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
class Config {
|
||||
let recorder: RecorderConfig
|
||||
let timer: TimerConfig
|
||||
init(recorder: RecorderConfig = RecorderConfig(), timer: TimerConfig = TimerConfig()) {
|
||||
self.recorder = recorder
|
||||
self.timer = timer
|
||||
}
|
||||
}
|
||||
|
||||
class RecorderConfig {
|
||||
let aggregationOptions: [AggregationOption]
|
||||
init(aggregationOptions: [AggregationOption]) {
|
||||
self.aggregationOptions = aggregationOptions
|
||||
}
|
||||
|
||||
init() {
|
||||
self.aggregationOptions = AggregationOption.defaults
|
||||
}
|
||||
}
|
||||
|
||||
class TimerConfig {
|
||||
let aggregationOptions: [AggregationOption]
|
||||
init(aggregationOptions: [AggregationOption]) {
|
||||
self.aggregationOptions = aggregationOptions
|
||||
}
|
||||
|
||||
init() {
|
||||
self.aggregationOptions = AggregationOption.defaults
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExampleCounter: Counter, CustomStringConvertible {
|
||||
let label: String
|
||||
let dimensions: [(String, String)]
|
||||
init(label: String, dimensions: [(String, String)]) {
|
||||
self.label = label
|
||||
self.dimensions = dimensions
|
||||
}
|
||||
|
||||
let lock = NSLock()
|
||||
var value: Int64 = 0
|
||||
func increment<DataType: BinaryInteger>(_ value: DataType) {
|
||||
self.lock.withLock {
|
||||
self.value += Int64(value)
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "counter [label: \(self.label) dimensions:\(self.dimensions) values:\(self.value)]"
|
||||
}
|
||||
}
|
||||
|
||||
class ExampleRecorder: Recorder, CustomStringConvertible {
|
||||
let label: String
|
||||
let dimensions: [(String, String)]
|
||||
let options: [AggregationOption]
|
||||
init(label: String, dimensions: [(String, String)], options: [AggregationOption]) {
|
||||
self.label = label
|
||||
self.dimensions = dimensions
|
||||
self.options = options
|
||||
}
|
||||
|
||||
private let lock = NSLock()
|
||||
var values = [(Int64, Double)]()
|
||||
func record<DataType: BinaryInteger>(_ value: DataType) {
|
||||
self.record(Double(value))
|
||||
}
|
||||
|
||||
func record<DataType: BinaryFloatingPoint>(_ value: DataType) {
|
||||
// this may loose percision, but good enough as an example
|
||||
let v = Double(value)
|
||||
// TODO: sliding window
|
||||
lock.withLock {
|
||||
values.append((Date().nanoSince1970, v))
|
||||
}
|
||||
options.forEach { option in
|
||||
switch option {
|
||||
case .count:
|
||||
self.count += 1
|
||||
case .sum:
|
||||
self.sum += v
|
||||
case .min:
|
||||
if 0 == self.min || v < self.min { self.min = v }
|
||||
case .max:
|
||||
if 0 == self.max || v > self.max { self.max = v }
|
||||
case .quantiles(let items):
|
||||
self.computeQuantiles(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _sum: Double = 0
|
||||
var sum: Double {
|
||||
get {
|
||||
return self.lock.withLock { _sum }
|
||||
}
|
||||
set {
|
||||
self.lock.withLock { _sum = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
private var _count: Int = 0
|
||||
var count: Int {
|
||||
get {
|
||||
return self.lock.withLock { _count }
|
||||
}
|
||||
set {
|
||||
self.lock.withLock { _count = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
private var _min: Double = 0
|
||||
var min: Double {
|
||||
get {
|
||||
return self.lock.withLock { _min }
|
||||
}
|
||||
set {
|
||||
self.lock.withLock { _min = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
private var _max: Double = 0
|
||||
var max: Double {
|
||||
get {
|
||||
return self.lock.withLock { _max }
|
||||
}
|
||||
set {
|
||||
self.lock.withLock { _max = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
private var _quantiels = [Float: Double]()
|
||||
var quantiels: [Float: Double] {
|
||||
get {
|
||||
return self.lock.withLock { _quantiels }
|
||||
}
|
||||
set {
|
||||
self.lock.withLock { _quantiels = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "recorder [label: \(self.label) dimensions:\(self.dimensions) count:\(self.count) sum:\(self.sum) min:\(self.min) max:\(self.max) quantiels:\(self.quantiels) values:\(self.values)]"
|
||||
}
|
||||
|
||||
// TODO: offload calcs to queue
|
||||
private func computeQuantiles(_ items: [Float]) {
|
||||
self.lock.withLock {
|
||||
self._quantiels.removeAll()
|
||||
items.forEach { item in
|
||||
if let result = Sigma.quantiles.method1(self.values.map { Double($0.1) }, probability: Double(item)) {
|
||||
self._quantiels[item] = result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExampleGauge: Recorder, CustomStringConvertible {
|
||||
let label: String
|
||||
let dimensions: [(String, String)]
|
||||
init(label: String, dimensions: [(String, String)]) {
|
||||
self.label = label
|
||||
self.dimensions = dimensions
|
||||
}
|
||||
|
||||
let lock = NSLock()
|
||||
var _value: Double = 0
|
||||
func record<DataType: BinaryInteger>(_ value: DataType) {
|
||||
self.record(Double(value))
|
||||
}
|
||||
|
||||
func record<DataType: BinaryFloatingPoint>(_ value: DataType) {
|
||||
// this may loose percision but good enough as an example
|
||||
self.lock.withLock { _value = Double(value) }
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "gauge [label: \(self.label) dimensions:\(self.dimensions) value:\(self._value)]"
|
||||
}
|
||||
}
|
||||
|
||||
class ExampleTimer: ExampleRecorder, Timer {
|
||||
func recordNanoseconds(_ duration: Int64) {
|
||||
super.record(duration)
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "timer [label: \(self.label) dimensions:\(self.dimensions) count:\(self.count) sum:\(self.sum) min:\(self.min) max:\(self.max) quantiels:\(self.quantiels) values:\(self.values)]"
|
||||
}
|
||||
}
|
||||
|
||||
enum AggregationOption {
|
||||
case count
|
||||
case sum
|
||||
case min
|
||||
case max
|
||||
case quantiles(_ items: [Float])
|
||||
|
||||
public static let defaults: [AggregationOption] = [.count, .sum, .min, .max, .quantiles(defaultQuantiles)]
|
||||
public static let defaultQuantiles: [Float] = [0.25, 0.5, 0.75, 0.9, 0.95, 0.99]
|
||||
}
|
||||
|
||||
private extension Foundation.Date {
|
||||
var nanoSince1970: Int64 {
|
||||
return Int64(self.timeIntervalSince1970 * 1_000_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Foundation.NSLock {
|
||||
func withLock<T>(_ body: () -> T) -> T {
|
||||
self.lock()
|
||||
defer {
|
||||
self.unlock()
|
||||
}
|
||||
return body()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
// copied from https://github.com/evgenyneu/SigmaSwiftStatistics/blob/master/SigmaSwiftStatistics/Quantiles.swift
|
||||
|
||||
//
|
||||
// Created by Alan James Salmoni on 21/12/2016.
|
||||
// Copyright © 2016 Thought Into Design Ltd. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum Sigma {
|
||||
/**
|
||||
|
||||
The class contains nine functions that calculate sample quantiles corresponding to the given probability. The implementation is the same as in R. This is an implementation of the algorithms described in the Hyndman and Fan paper, 1996:
|
||||
|
||||
https://www.jstor.org/stable/2684934
|
||||
https://www.amherst.edu/media/view/129116/original/Sample+Quantiles.pdf
|
||||
|
||||
The documentation of the functions is based on R and Wikipedia:
|
||||
|
||||
https://en.wikipedia.org/wiki/Quantile
|
||||
http://stat.ethz.ch/R-manual/R-devel/library/stats/html/quantile.html
|
||||
|
||||
*/
|
||||
public static let quantiles = SigmaQuantiles()
|
||||
}
|
||||
|
||||
public class SigmaQuantiles {
|
||||
/*
|
||||
|
||||
This method calculates quantiles using the inverse of the empirical distribution function.
|
||||
|
||||
- parameter data: Array of decimal numbers.
|
||||
- parameter probability: the probability value between 0 and 1, inclusive.
|
||||
- returns: sample quantile.
|
||||
|
||||
*/
|
||||
public func method1(_ data: [Double], probability: Double) -> Double? {
|
||||
if probability < 0 || probability > 1 { return nil }
|
||||
let data = data.sorted(by: <)
|
||||
let count = Double(data.count)
|
||||
let k = Int((probability * count))
|
||||
let g = (probability * count) - Double(k)
|
||||
var new_probability = 1.0
|
||||
if g == 0.0 { new_probability = 0.0 }
|
||||
return self.qDef(data, k: k, probability: new_probability)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
This method uses inverted empirical distribution function with averaging.
|
||||
|
||||
- parameter data: Array of decimal numbers.
|
||||
- parameter probability: the probability value between 0 and 1, inclusive.
|
||||
- returns: sample quantile.
|
||||
|
||||
*/
|
||||
public func method2(_ data: [Double], probability: Double) -> Double? {
|
||||
if probability < 0 || probability > 1 { return nil }
|
||||
let data = data.sorted(by: <)
|
||||
let count = Double(data.count)
|
||||
let k = Int(probability * count)
|
||||
let g = (probability * count) - Double(k)
|
||||
var new_probability = 1.0
|
||||
if g == 0.0 { new_probability = 0.5 }
|
||||
return self.qDef(data, k: k, probability: new_probability)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
The 3rd sample quantile method from Hyndman and Fan paper (1996).
|
||||
|
||||
- parameter data: Array of decimal numbers.
|
||||
- parameter probability: the probability value between 0 and 1, inclusive.
|
||||
- returns: sample quantile.
|
||||
|
||||
*/
|
||||
public func method3(_ data: [Double], probability: Double) -> Double? {
|
||||
if probability < 0 || probability > 1 { return nil }
|
||||
let data = data.sorted(by: <)
|
||||
let count = Double(data.count)
|
||||
let m = -0.5
|
||||
let k = Int((probability * count) + m)
|
||||
let g = (probability * count) + m - Double(k)
|
||||
var new_probability = 1.0
|
||||
if g <= 0, k % 2 == 0 { new_probability = 0.0 }
|
||||
return self.qDef(data, k: k, probability: new_probability)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
It uses linear interpolation of the empirical distribution function.
|
||||
|
||||
- parameter data: Array of decimal numbers.
|
||||
- parameter probability: the probability value between 0 and 1, inclusive.
|
||||
- returns: sample quantile.
|
||||
|
||||
*/
|
||||
public func method4(_ data: [Double], probability: Double) -> Double? {
|
||||
if probability < 0 || probability > 1 { return nil }
|
||||
let data = data.sorted(by: <)
|
||||
let count = Double(data.count)
|
||||
let m = 0.0
|
||||
let k = Int((probability * count) + m)
|
||||
let probability = (probability * count) + m - Double(k)
|
||||
return self.qDef(data, k: k, probability: probability)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
This method uses a piecewise linear function where the knots are the values midway through the steps of the empirical distribution function.
|
||||
|
||||
- parameter data: Array of decimal numbers.
|
||||
- parameter probability: the probability value between 0 and 1, inclusive.
|
||||
- returns: sample quantile.
|
||||
|
||||
*/
|
||||
public func method5(_ data: [Double], probability: Double) -> Double? {
|
||||
if probability < 0 || probability > 1 { return nil }
|
||||
let data = data.sorted(by: <)
|
||||
let count = Double(data.count)
|
||||
let m = 0.5
|
||||
let k = Int((probability * count) + m)
|
||||
let probability = (probability * count) + m - Double(k)
|
||||
return self.qDef(data, k: k, probability: probability)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
This method is implemented in Microsoft Excel (PERCENTILE.EXC), Minitab and SPSS. It uses linear interpolation of the expectations for the order statistics for the uniform distribution on [0,1].
|
||||
|
||||
- parameter data: Array of decimal numbers.
|
||||
- parameter probability: the probability value between 0 and 1, inclusive.
|
||||
- returns: sample quantile.
|
||||
|
||||
*/
|
||||
public func method6(_ data: [Double], probability: Double) -> Double? {
|
||||
if probability < 0 || probability > 1 { return nil }
|
||||
let data = data.sorted(by: <)
|
||||
let count = Double(data.count)
|
||||
let m = probability
|
||||
let k = Int((probability * count) + m)
|
||||
let probability = (probability * count) + m - Double(k)
|
||||
return self.qDef(data, k: k, probability: probability)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
This method is implemented in S, Microsoft Excel (PERCENTILE or PERCENTILE.INC) and Google Docs Sheets (PERCENTILE). It uses linear interpolation of the modes for the order statistics for the uniform distribution on [0, 1].
|
||||
|
||||
- parameter data: Array of decimal numbers.
|
||||
- parameter probability: the probability value between 0 and 1, inclusive.
|
||||
- returns: sample quantile.
|
||||
|
||||
*/
|
||||
public func method7(_ data: [Double], probability: Double) -> Double? {
|
||||
if probability < 0 || probability > 1 { return nil }
|
||||
let data = data.sorted(by: <)
|
||||
let count = Double(data.count)
|
||||
let m = 1.0 - probability
|
||||
let k = Int((probability * count) + m)
|
||||
let probability = (probability * count) + m - Double(k)
|
||||
return self.qDef(data, k: k, probability: probability)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
The quantiles returned by the method are approximately median-unbiased regardless of the distribution of x.
|
||||
|
||||
- parameter data: Array of decimal numbers.
|
||||
- parameter probability: the probability value between 0 and 1, inclusive.
|
||||
- returns: sample quantile.
|
||||
|
||||
*/
|
||||
public func method8(_ data: [Double], probability: Double) -> Double? {
|
||||
if probability < 0 || probability > 1 { return nil }
|
||||
let data = data.sorted(by: <)
|
||||
let count = Double(data.count)
|
||||
let m = (probability + 1.0) / 3.0
|
||||
let k = Int((probability * count) + m)
|
||||
let probability = (probability * count) + m - Double(k)
|
||||
return self.qDef(data, k: k, probability: probability)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
The quantiles returned by this method are approximately unbiased for the expected order statistics if x is normally distributed.
|
||||
|
||||
- parameter data: Array of decimal numbers.
|
||||
- parameter probability: the probability value between 0 and 1, inclusive.
|
||||
- returns: sample quantile.
|
||||
|
||||
*/
|
||||
public func method9(_ data: [Double], probability: Double) -> Double? {
|
||||
if probability < 0 || probability > 1 { return nil }
|
||||
let data = data.sorted(by: <)
|
||||
let count = Double(data.count)
|
||||
let m = (0.25 * probability) + (3.0 / 8.0)
|
||||
let k = Int((probability * count) + m)
|
||||
let probability = (probability * count) + m - Double(k)
|
||||
return self.qDef(data, k: k, probability: probability)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Shared function for all quantile methods.
|
||||
|
||||
- parameter data: Array of decimal numbers.
|
||||
- parameter k: the position of the element in the dataset.
|
||||
- parameter probability: the probability value between 0 and 1, inclusive.
|
||||
- returns: sample quantile.
|
||||
|
||||
*/
|
||||
private func qDef(_ data: [Double], k: Int, probability: Double) -> Double? {
|
||||
if data.isEmpty { return nil }
|
||||
if k < 1 { return data[0] }
|
||||
if k >= data.count { return data.last }
|
||||
return ((1.0 - probability) * data[k - 1]) + (probability * data[k])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import Metrics
|
||||
|
||||
class RandomLibrary {
|
||||
let methodCallsCounter = Metrics.global.makeCounter(label: "RandomLibrary::TotalMethodCalls")
|
||||
|
||||
func doSomething() {
|
||||
self.methodCallsCounter.increment()
|
||||
}
|
||||
|
||||
func doSomethingSlow(callback: @escaping () -> Void) {
|
||||
self.methodCallsCounter.increment()
|
||||
Metrics.global.withTimer(label: "RandomLibrary::doSomethingSlow") { timer in
|
||||
let start = Date()
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int.random(in: 5 ... 500))) {
|
||||
timer.record(Date().timeIntervalSince(start))
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
print("##### example 1 #####")
|
||||
Example1.main()
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
@_exported import CoreMetrics
|
||||
@_exported import protocol CoreMetrics.Timer
|
||||
@_exported import Foundation
|
||||
|
||||
public extension MetricsHandler {
|
||||
@inlinable
|
||||
func timed<T>(label: String, dimensions: [(String, String)] = [], body: @escaping () throws -> T) rethrows -> T {
|
||||
let timer = self.makeTimer(label: label, dimensions: dimensions)
|
||||
let start = Date()
|
||||
defer {
|
||||
timer.record(Date().timeIntervalSince(start))
|
||||
}
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
|
||||
public extension Timer {
|
||||
@inlinable
|
||||
func record(_ duration: TimeInterval) {
|
||||
self.recordSeconds(duration)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func record(_ duration: DispatchTimeInterval) {
|
||||
switch duration {
|
||||
case .nanoseconds(let value):
|
||||
self.recordNanoseconds(Int64(value))
|
||||
case .microseconds(let value):
|
||||
self.recordMicroseconds(value)
|
||||
case .milliseconds(let value):
|
||||
self.recordMilliseconds(value)
|
||||
case .seconds(let value):
|
||||
self.recordSeconds(value)
|
||||
case .never:
|
||||
self.record(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
@testable import CoreMetrics
|
||||
import XCTest
|
||||
|
||||
class MetricsTests: XCTestCase {
|
||||
func testCounters() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
let group = DispatchGroup()
|
||||
let name = "counter-\(NSUUID().uuidString)"
|
||||
let counter = Metrics.global.makeCounter(label: name) as! TestCounter
|
||||
let total = Int.random(in: 500 ... 1000)
|
||||
for _ in 0 ... total {
|
||||
group.enter()
|
||||
DispatchQueue(label: "\(name)-queue").async {
|
||||
counter.increment(Int.random(in: 0 ... 1000))
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
group.wait()
|
||||
XCTAssertEqual(counter.values.count - 1, total, "expected number of entries to match")
|
||||
}
|
||||
|
||||
func testCounterBlock() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
// run the test
|
||||
let name = "counter-\(NSUUID().uuidString)"
|
||||
let value = Int.random(in: Int.min ... Int.max)
|
||||
Metrics.global.withCounter(label: name) { $0.increment(value) }
|
||||
let counter = Metrics.global.makeCounter(label: name) as! TestCounter
|
||||
XCTAssertEqual(counter.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match")
|
||||
}
|
||||
|
||||
func testRecorders() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
let group = DispatchGroup()
|
||||
let name = "recorder-\(NSUUID().uuidString)"
|
||||
let recorder = Metrics.global.makeRecorder(label: name) as! TestRecorder
|
||||
let total = Int.random(in: 500 ... 1000)
|
||||
for _ in 0 ... total {
|
||||
group.enter()
|
||||
DispatchQueue(label: "\(name)-queue").async {
|
||||
recorder.record(Int.random(in: Int.min ... Int.max))
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
group.wait()
|
||||
XCTAssertEqual(recorder.values.count - 1, total, "expected number of entries to match")
|
||||
}
|
||||
|
||||
func testRecordersInt() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
let recorder = Metrics.global.makeRecorder(label: "test-recorder") as! TestRecorder
|
||||
let values = (0 ... 999).map { _ in Int32.random(in: Int32.min ... Int32.max) }
|
||||
for i in 0 ... values.count - 1 {
|
||||
recorder.record(values[i])
|
||||
}
|
||||
XCTAssertEqual(values.count, recorder.values.count, "expected number of entries to match")
|
||||
for i in 0 ... values.count - 1 {
|
||||
XCTAssertEqual(Int32(recorder.values[i].1), values[i], "expected value #\(i) to match.")
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordersFloat() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
let recorder = Metrics.global.makeRecorder(label: "test-recorder") as! TestRecorder
|
||||
let values = (0 ... 999).map { _ in Float.random(in: Float(Int32.min) ... Float(Int32.max)) }
|
||||
for i in 0 ... values.count - 1 {
|
||||
recorder.record(values[i])
|
||||
}
|
||||
XCTAssertEqual(values.count, recorder.values.count, "expected number of entries to match")
|
||||
for i in 0 ... values.count - 1 {
|
||||
XCTAssertEqual(Float(recorder.values[i].1), values[i], "expected value #\(i) to match.")
|
||||
}
|
||||
}
|
||||
|
||||
func testRecorderBlock() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
// run the test
|
||||
let name = "recorder-\(NSUUID().uuidString)"
|
||||
let value = Double.random(in: Double(Int.min) ... Double(Int.max))
|
||||
Metrics.global.withRecorder(label: name) { $0.record(value) }
|
||||
let recorder = Metrics.global.makeRecorder(label: name) as! TestRecorder
|
||||
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
|
||||
}
|
||||
|
||||
func testTimers() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
let group = DispatchGroup()
|
||||
let name = "timer-\(NSUUID().uuidString)"
|
||||
let timer = Metrics.global.makeTimer(label: name) as! TestTimer
|
||||
let total = Int.random(in: 500 ... 1000)
|
||||
for _ in 0 ... total {
|
||||
group.enter()
|
||||
DispatchQueue(label: "\(name)-queue").async {
|
||||
timer.recordNanoseconds(Int64.random(in: Int64.min ... Int64.max))
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
group.wait()
|
||||
XCTAssertEqual(timer.values.count - 1, total, "expected number of entries to match")
|
||||
}
|
||||
|
||||
func testTimerBlock() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
// run the test
|
||||
let name = "timer-\(NSUUID().uuidString)"
|
||||
let value = Int64.random(in: Int64.min ... Int64.max)
|
||||
Metrics.global.withTimer(label: name) { $0.recordNanoseconds(value) }
|
||||
let timer = Metrics.global.makeTimer(label: name) as! TestTimer
|
||||
XCTAssertEqual(timer.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(timer.values[0].1, value, "expected value to match")
|
||||
}
|
||||
|
||||
func testTimerVariants() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
// run the test
|
||||
let timer = Metrics.global.makeTimer(label: "test-timer") as! TestTimer
|
||||
// nano
|
||||
let nano = Int64.random(in: 0 ... 5)
|
||||
timer.recordNanoseconds(nano)
|
||||
XCTAssertEqual(timer.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(timer.values[0].1, nano, "expected value to match")
|
||||
// micro
|
||||
let micro = Int64.random(in: 0 ... 5)
|
||||
timer.recordMicroseconds(micro)
|
||||
XCTAssertEqual(timer.values.count, 2, "expected number of entries to match")
|
||||
XCTAssertEqual(timer.values[1].1, micro * 1000, "expected value to match")
|
||||
// milli
|
||||
let milli = Int64.random(in: 0 ... 5)
|
||||
timer.recordMilliseconds(milli)
|
||||
XCTAssertEqual(timer.values.count, 3, "expected number of entries to match")
|
||||
XCTAssertEqual(timer.values[2].1, milli * 1_000_000, "expected value to match")
|
||||
// seconds
|
||||
let sec = Int64.random(in: 0 ... 5)
|
||||
timer.recordSeconds(sec)
|
||||
XCTAssertEqual(timer.values.count, 4, "expected number of entries to match")
|
||||
XCTAssertEqual(timer.values[3].1, sec * 1_000_000_000, "expected value to match")
|
||||
}
|
||||
|
||||
func testGauge() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
// run the test
|
||||
let name = "gauge-\(NSUUID().uuidString)"
|
||||
let value = Double.random(in: -1000 ... 1000)
|
||||
let gauge = Metrics.global.makeGauge(label: name)
|
||||
gauge.record(value)
|
||||
let recorder = gauge as! TestRecorder
|
||||
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
|
||||
}
|
||||
|
||||
func testGaugeBlock() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
// run the test
|
||||
let name = "gauge-\(NSUUID().uuidString)"
|
||||
let value = Double.random(in: -1000 ... 1000)
|
||||
Metrics.global.withGauge(label: name) { $0.record(value) }
|
||||
let recorder = Metrics.global.makeGauge(label: name) as! TestRecorder
|
||||
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
|
||||
}
|
||||
|
||||
func testMUX() throws {
|
||||
// bootstrap with our test metrics
|
||||
let handlers = [TestMetrics(), TestMetrics(), TestMetrics()]
|
||||
Metrics.bootstrap(MultiplexMetricsHandler(handlers: handlers))
|
||||
// run the test
|
||||
let name = NSUUID().uuidString
|
||||
let value = Int.random(in: Int.min ... Int.max)
|
||||
Metrics.global.withCounter(label: name) { counter in
|
||||
counter.increment(value)
|
||||
}
|
||||
handlers.forEach { handler in
|
||||
let counter = handler.counters[name] as! TestCounter
|
||||
XCTAssertEqual(counter.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match")
|
||||
}
|
||||
}
|
||||
|
||||
func testDimensions() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
// run the test
|
||||
let name = "counter-\(NSUUID().uuidString)"
|
||||
let dimensions = [("foo", "bar"), ("baz", "quk")]
|
||||
let counter = Metrics.global.makeCounter(label: name, dimensions: dimensions) as! TestCounter
|
||||
counter.increment()
|
||||
|
||||
XCTAssertEqual(counter.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(counter.values[0].1, 1, "expected value to match")
|
||||
XCTAssertEqual(counter.dimensions.description, dimensions.description, "expected dimensions to match")
|
||||
|
||||
let counter2 = Metrics.global.makeCounter(label: name, dimensions: dimensions) as! TestCounter
|
||||
XCTAssertEqual(counter2, counter, "expected caching to work with dimensions")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
@testable import Metrics
|
||||
import XCTest
|
||||
|
||||
class MetricsExtensionsTests: XCTestCase {
|
||||
func testTimerBlock() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
// run the test
|
||||
let name = "timer-\(NSUUID().uuidString)"
|
||||
let delay = 0.05
|
||||
Metrics.global.timed(label: name) {
|
||||
Thread.sleep(forTimeInterval: delay)
|
||||
}
|
||||
let timer = Metrics.global.makeTimer(label: name) as! TestTimer
|
||||
XCTAssertEqual(1, timer.values.count, "expected number of entries to match")
|
||||
XCTAssertGreaterThan(timer.values[0].1, Int64(delay * 1_000_000_000), "expected delay to match")
|
||||
}
|
||||
|
||||
func testTimerWithTimeInterval() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
// run the test
|
||||
let timer = Metrics.global.makeTimer(label: "test-timer") as! TestTimer
|
||||
let timeInterval = TimeInterval(Double.random(in: 1 ... 500))
|
||||
timer.record(timeInterval)
|
||||
XCTAssertEqual(1, timer.values.count, "expected number of entries to match")
|
||||
XCTAssertEqual(timer.values[0].1, Int64(timeInterval * 1_000_000_000), "expected value to match")
|
||||
}
|
||||
|
||||
func testTimerWithDispatchTime() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
Metrics.bootstrap(metrics)
|
||||
// run the test
|
||||
let timer = Metrics.global.makeTimer(label: "test-timer") as! TestTimer
|
||||
// nano
|
||||
let nano = DispatchTimeInterval.nanoseconds(Int.random(in: 1 ... 500))
|
||||
timer.record(nano)
|
||||
XCTAssertEqual(timer.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(.nanoseconds(Int(timer.values[0].1)), nano, "expected value to match")
|
||||
// micro
|
||||
let micro = DispatchTimeInterval.microseconds(Int.random(in: 1 ... 500))
|
||||
timer.record(micro)
|
||||
XCTAssertEqual(timer.values.count, 2, "expected number of entries to match")
|
||||
XCTAssertEqual(.nanoseconds(Int(timer.values[1].1)), micro, "expected value to match")
|
||||
// milli
|
||||
let milli = DispatchTimeInterval.milliseconds(Int.random(in: 1 ... 500))
|
||||
timer.record(milli)
|
||||
XCTAssertEqual(timer.values.count, 3, "expected number of entries to match")
|
||||
XCTAssertEqual(.nanoseconds(Int(timer.values[2].1)), milli, "expected value to match")
|
||||
// seconds
|
||||
let sec = DispatchTimeInterval.seconds(Int.random(in: 1 ... 500))
|
||||
timer.record(sec)
|
||||
XCTAssertEqual(timer.values.count, 4, "expected number of entries to match")
|
||||
XCTAssertEqual(.nanoseconds(Int(timer.values[3].1)), sec, "expected value to match")
|
||||
// never
|
||||
timer.record(DispatchTimeInterval.never)
|
||||
XCTAssertEqual(timer.values.count, 5, "expected number of entries to match")
|
||||
XCTAssertEqual(timer.values[4].1, 0, "expected value to match")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
@testable import CoreMetrics
|
||||
@testable import protocol CoreMetrics.Timer
|
||||
import Foundation
|
||||
|
||||
internal class TestMetrics: MetricsHandler {
|
||||
private let lock = NSLock() // TODO: consider lock per cache?
|
||||
var counters = [String: Counter]()
|
||||
var recorders = [String: Recorder]()
|
||||
var timers = [String: Timer]()
|
||||
|
||||
public func makeCounter(label: String, dimensions: [(String, String)]) -> Counter {
|
||||
return self.make(label: label, dimensions: dimensions, cache: &self.counters, maker: TestCounter.init)
|
||||
}
|
||||
|
||||
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> Recorder {
|
||||
let maker = { (label: String, dimensions: [(String, String)]) -> Recorder in
|
||||
TestRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
|
||||
}
|
||||
return self.make(label: label, dimensions: dimensions, cache: &self.recorders, maker: maker)
|
||||
}
|
||||
|
||||
public func makeTimer(label: String, dimensions: [(String, String)]) -> Timer {
|
||||
return self.make(label: label, dimensions: dimensions, cache: &self.timers, maker: TestTimer.init)
|
||||
}
|
||||
|
||||
private func make<Item>(label: String, dimensions: [(String, String)], cache: inout [String: Item], maker: (String, [(String, String)]) -> Item) -> Item {
|
||||
let fqn = self.fqn(label: label, dimensions: dimensions)
|
||||
return self.lock.withLock {
|
||||
if let item = cache[fqn] {
|
||||
return item
|
||||
} else {
|
||||
let item = maker(label, dimensions)
|
||||
cache[fqn] = item
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fqn(label: String, dimensions: [(String, String)]) -> String {
|
||||
return [[label], dimensions.compactMap { $0.1 }].flatMap { $0 }.joined(separator: ".")
|
||||
}
|
||||
}
|
||||
|
||||
internal class TestCounter: Counter, Equatable {
|
||||
let id: String
|
||||
let label: String
|
||||
let dimensions: [(String, String)]
|
||||
|
||||
let lock = NSLock()
|
||||
var values = [(Date, Int64)]()
|
||||
|
||||
init(label: String, dimensions: [(String, String)]) {
|
||||
self.id = NSUUID().uuidString
|
||||
self.label = label
|
||||
self.dimensions = dimensions
|
||||
}
|
||||
|
||||
func increment<DataType: BinaryInteger>(_ value: DataType) {
|
||||
self.lock.withLock {
|
||||
self.values.append((Date(), Int64(value)))
|
||||
}
|
||||
print("adding \(value) to \(self.label)")
|
||||
}
|
||||
|
||||
public static func == (lhs: TestCounter, rhs: TestCounter) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
internal class TestRecorder: Recorder, Equatable {
|
||||
let id: String
|
||||
let label: String
|
||||
let dimensions: [(String, String)]
|
||||
let aggregate: Bool
|
||||
|
||||
let lock = NSLock()
|
||||
var values = [(Date, Double)]()
|
||||
|
||||
init(label: String, dimensions: [(String, String)], aggregate: Bool) {
|
||||
self.id = NSUUID().uuidString
|
||||
self.label = label
|
||||
self.dimensions = dimensions
|
||||
self.aggregate = aggregate
|
||||
}
|
||||
|
||||
func record<DataType: BinaryInteger>(_ value: DataType) {
|
||||
self.record(Double(value))
|
||||
}
|
||||
|
||||
func record<DataType: BinaryFloatingPoint>(_ value: DataType) {
|
||||
self.lock.withLock {
|
||||
// this may loose percision but good enough as an example
|
||||
values.append((Date(), Double(value)))
|
||||
}
|
||||
print("recoding \(value) in \(self.label)")
|
||||
}
|
||||
|
||||
public static func == (lhs: TestRecorder, rhs: TestRecorder) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
internal class TestTimer: Timer, Equatable {
|
||||
let id: String
|
||||
let label: String
|
||||
let dimensions: [(String, String)]
|
||||
|
||||
let lock = NSLock()
|
||||
var values = [(Date, Int64)]()
|
||||
|
||||
init(label: String, dimensions: [(String, String)]) {
|
||||
self.id = NSUUID().uuidString
|
||||
self.label = label
|
||||
self.dimensions = dimensions
|
||||
}
|
||||
|
||||
func recordNanoseconds(_ duration: Int64) {
|
||||
self.lock.withLock {
|
||||
values.append((Date(), duration))
|
||||
}
|
||||
print("recoding \(duration) \(self.label)")
|
||||
}
|
||||
|
||||
public static func == (lhs: TestTimer, rhs: TestTimer) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
private extension NSLock {
|
||||
func withLock<T>(_ body: () -> T) -> T {
|
||||
self.lock()
|
||||
defer {
|
||||
self.unlock()
|
||||
}
|
||||
return body()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue