initial commit

This commit is contained in:
tomer doron 2019-01-07 20:42:18 -08:00
commit d01123e590
14 changed files with 1520 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
.xcode

29
Package.swift Normal file
View File

@ -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"]
),
]
)

12
README.md Normal file
View File

@ -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

View File

@ -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)
}
}

View File

@ -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) {}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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])
}
}

View File

@ -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()
}
}
}
}

View File

@ -0,0 +1,2 @@
print("##### example 1 #####")
Example1.main()

View File

@ -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)
}
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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()
}
}