diff --git a/Package.swift b/Package.swift index 5f4e14e..deca479 100644 --- a/Package.swift +++ b/Package.swift @@ -1,52 +1,27 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.2 import PackageDescription let package = Package( name: "swift-distributed-tracing-baggage", products: [ .library( - name: "Baggage", + name: "InstrumentationBaggage", targets: [ - "Baggage", + "InstrumentationBaggage", ] ), ], - dependencies: [ - .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-distributed-tracing-baggage-core.git", from: "0.1.1"), - ], targets: [ - .target( - name: "Baggage", - dependencies: [ - .product(name: "Logging", package: "swift-log"), - .product(name: "CoreBaggage", package: "swift-distributed-tracing-baggage-core"), - ] - ), + .target(name: "InstrumentationBaggage"), // ==== -------------------------------------------------------------------------------------------------------- // MARK: Tests .testTarget( - name: "BaggageTests", + name: "InstrumentationBaggageTests", dependencies: [ - "Baggage", + .target(name: "InstrumentationBaggage"), ] ), - - // ==== -------------------------------------------------------------------------------------------------------- - // MARK: Performance / Benchmarks - - .target( - name: "BaggageBenchmarks", - dependencies: [ - "Baggage", - "BaggageBenchmarkTools", - ] - ), - .target( - name: "BaggageBenchmarkTools", - dependencies: [] - ), ] ) diff --git a/README.md b/README.md index 41b7922..e6bee5b 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,50 @@ -# 🧳 Distributed Tracing: LoggingContext / Baggage +# 🧳 Distributed Tracing: Baggage [![Swift 5.2](https://img.shields.io/badge/Swift-5.2-ED523F.svg?style=flat)](https://swift.org/download/) -[![Swift 5.1](https://img.shields.io/badge/Swift-5.1-ED523F.svg?style=flat)](https://swift.org/download/) -[![Swift 5.0](https://img.shields.io/badge/Swift-5.0-ED523F.svg?style=flat)](https://swift.org/download/) +[![Swift 5.3](https://img.shields.io/badge/Swift-5.3-ED523F.svg?style=flat)](https://swift.org/download/) +[![Swift 5.4](https://img.shields.io/badge/Swift-5.4-ED523F.svg?style=flat)](https://swift.org/download/) +[![Swift 5.5](https://img.shields.io/badge/Swift-5.5-ED523F.svg?style=flat)](https://swift.org/download/) -`LoggingContext` is a minimal (zero-dependency) "context" library meant to "carry" baggage (metadata) for cross-cutting -tools such as tracers. It is purposefully not tied to any specific use-case (in the spirit of the -[Tracing Plane paper](https://cs.brown.edu/~jcmace/papers/mace18universal.pdf)'s BaggageContext). However, it should -enable a vast majority of use cases cross-cutting tools need to support. Unlike mentioned in the paper, our -`LoggingContext` does not implement its own serialization scheme (today). +> ⚠️ Automatic propagation through task-locals only supported in Swift >= 5.5 + +`Baggage` is a minimal (zero-dependency) context propagation container, intended to "carry" baggage items +for purposes of cross-cutting tools to be built on top of it. + +It is modeled after the concepts explained in [W3C Baggage](https://w3c.github.io/baggage/) and the +in the spirit of [Tracing Plane](https://cs.brown.edu/~jcmace/papers/mace18universal.pdf) 's "Baggage Context" type, +although by itself it does not define a specific serialization format. See https://github.com/apple/swift-distributed-tracing for actual instrument types and implementations which can be used to deploy various cross-cutting instruments all reusing the same baggage type. More information can be found in the [SSWG meeting notes](https://gist.github.com/ktoso/4d160232407e4d5835b5ba700c73de37#swift-baggage-context--distributed-tracing). -## Installation +## Dependency -You can install the `LoggingContext` library through the Swift Package Manager. The library itself is called `Baggage`, -so that's what you'd import in your Swift files. + In order to depend on this library you can use the Swift Package Manager, and add the following dependency to your `Package.swift`: ```swift dependencies: [ - // ... - .package(url: "https://github.com/apple/swift-distributed-tracing-baggage.git", from: "0.1.0"), + .package( + url: "https://github.com/apple/swift-distributed-tracing-baggage.git", + from: "0.1.0" + ) +] +``` + +and depend on the module in your target: + +```swift +targets: [ + .target( + name: "MyAwesomeApp", + dependencies: [ + .product( + name: "InstrumentationBaggage", + package: "swift-distributed-tracing-baggage" + ), + ] + ), + // ... ] ``` @@ -34,8 +56,9 @@ Please refer to in-depth discussion and documentation in the [Swift Distributed Please make sure to run the `./scripts/soundness.sh` script when contributing, it checks formatting and similar things. -You can make ensure it always is run and passes before you push by installing a pre-push hook with git: +You can ensure it always runs and passes before you push by installing a pre-push hook with git: ``` echo './scripts/soundness.sh' > .git/hooks/pre-push +chmod +x .git/hooks/pre-push ``` diff --git a/Sources/Baggage/Baggage+Extensions.swift b/Sources/Baggage/Baggage+Extensions.swift deleted file mode 100644 index 88a1749..0000000 --- a/Sources/Baggage/Baggage+Extensions.swift +++ /dev/null @@ -1,29 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@_exported import CoreBaggage -@_exported import Logging - -extension Baggage { - /// Creates a `DefaultLoggingContext` with the passed in `Logger`. - /// - /// This can be useful when for some reason one was handed only a baggage yet needs to invoke an API that accepts - /// a `LoggingContext`. E.g. when inside a NIO handler wanting to carry the `context.baggage` from the channel handler, - /// however the API we want to call (e.g. `HTTPClient`) needs a `LoggingContext`, this function enables creating - /// a context for the purpose of this call easily inside the parameter being passed. - /// - /// - Parameter logger: to be used in the returned `DefaultLoggingContext`, it will be populated with loggable baggage values. - public func context(logger: Logger) -> DefaultLoggingContext { - return .init(logger: logger, baggage: self) - } -} diff --git a/Sources/Baggage/LogHandler.swift b/Sources/Baggage/LogHandler.swift deleted file mode 100644 index 23d7500..0000000 --- a/Sources/Baggage/LogHandler.swift +++ /dev/null @@ -1,164 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@_exported import CoreBaggage -@_exported import Logging - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Logger with Baggage - -extension Logger { - /// Returns a logger that in addition to any explicit metadata passed to log statements, - /// also includes the `Baggage` adapted into metadata values. - /// - /// The rendering of baggage values into metadata values is performed on demand, - /// whenever a log statement is effective (i.e. will be logged, according to active `logLevel`). - /// - /// Note that when it is known that multiple log statements will be performed with the baggage it is preferable to - /// modify the logger's metadata by issuing `logger.updateMetadata(previous: baggage, latest: baggage)` instead. - /// - /// - SeeAlso: - public func with(_ baggage: Baggage) -> Logger { - return Logger( - label: self.label, - factory: { _ in BaggageMetadataLogHandler(logger: self, baggage: baggage) } - ) - } - - /// Update the logger's metadata in accordance to the passed in baggage. - /// - /// Items which were previously present in the baggage but are now removed will also be removed from the logger's - /// metadata. - /// - /// - Parameters: - /// - previous: baggage which was previously used to set metadata on this logger (pass `.topLevel` if unknown or none) - /// - latest: the current, latest, state of the baggage container; all of it's loggable values will be set as the `Logger`'s metadata - public mutating func updateMetadata(previous: Baggage, latest: Baggage) { - var removedKeys: Set = [] - removedKeys.reserveCapacity(previous.count) - previous.forEach { key, _ in - removedKeys.insert(key) - } - latest.forEach { key, value in - removedKeys.remove(key) - if let convertible = value as? String { - self[metadataKey: key.name] = .string(convertible) - } else if let convertible = value as? CustomStringConvertible { - self[metadataKey: key.name] = .stringConvertible(convertible) - } else { - self[metadataKey: key.name] = .stringConvertible(BaggageValueCustomStringConvertible(value)) - } - } - removedKeys.forEach { removedKey in - self[metadataKey: removedKey.name] = nil - } - } -} - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Baggage (as additional Logger.Metadata) LogHandler - -/// Proxying log handler which adds `Baggage` as metadata when log events are to be emitted. -/// -/// The values stored in the `Baggage` are merged with the existing metadata on the logger. If both contain values for the same key, -/// the `Baggage` values are preferred. -public struct BaggageMetadataLogHandler: LogHandler { - private var underlying: Logger - private let baggage: Baggage - - public init(logger underlying: Logger, baggage: Baggage) { - self.underlying = underlying - self.baggage = baggage - } - - public var logLevel: Logger.Level { - get { - return self.underlying.logLevel - } - set { - self.underlying.logLevel = newValue - } - } - - public func log( - level: Logger.Level, - message: Logger.Message, - metadata: Logger.Metadata?, - source: String, - file: String, - function: String, - line: UInt - ) { - guard self.underlying.logLevel <= level else { - return - } - - var effectiveMetadata = self.baggageAsMetadata() - if let metadata = metadata { - effectiveMetadata.merge(metadata, uniquingKeysWith: { _, r in r }) - } - self.underlying.log(level: level, message, metadata: effectiveMetadata, source: source, file: file, function: function, line: line) - } - - public var metadata: Logger.Metadata { - get { - return [:] - } - set { - newValue.forEach { k, v in - self.underlying[metadataKey: k] = v - } - } - } - - /// Note that this does NOT look up inside the baggage. - /// - /// This is because a context lookup either has to use the specific type key, or iterate over all keys to locate one by name, - /// which may be incorrect still, thus rather than making an potentially slightly incorrect lookup, we do not implement peeking - /// into a baggage with String keys through this handler (as that is not a capability `Baggage` offers in any case. - public subscript(metadataKey metadataKey: Logger.Metadata.Key) -> Logger.Metadata.Value? { - get { - return self.underlying[metadataKey: metadataKey] - } - set { - self.underlying[metadataKey: metadataKey] = newValue - } - } - - private func baggageAsMetadata() -> Logger.Metadata { - var effectiveMetadata: Logger.Metadata = [:] - self.baggage.forEach { key, value in - if let convertible = value as? String { - effectiveMetadata[key.name] = .string(convertible) - } else if let convertible = value as? CustomStringConvertible { - effectiveMetadata[key.name] = .stringConvertible(convertible) - } else { - effectiveMetadata[key.name] = .stringConvertible(BaggageValueCustomStringConvertible(value)) - } - } - - return effectiveMetadata - } -} - -struct BaggageValueCustomStringConvertible: CustomStringConvertible { - let value: Any - - init(_ value: Any) { - self.value = value - } - - var description: String { - return "\(self.value)" - } -} diff --git a/Sources/Baggage/LoggingContext.swift b/Sources/Baggage/LoggingContext.swift deleted file mode 100644 index 8550b5f..0000000 --- a/Sources/Baggage/LoggingContext.swift +++ /dev/null @@ -1,293 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@_exported import CoreBaggage -@_exported import Logging - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: LoggingContext - -/// The `LoggingContext` MAY be adopted by specific "framework contexts" such as e.g. `CoolFramework.Context` in -/// order to allow users to pass such context directly to libraries accepting any context. -/// -/// This allows frameworks and library authors to offer APIs which compose more easily. -/// Please refer to the "Reference Implementation" notes on each of the requirements to know how to implement this protocol correctly. -/// -/// ### Implementation notes -/// Conforming types MUST exhibit Value Semantics (i.e. be a pure `struct`, or implement the Copy-on-Write pattern), -/// in order to implement the `set` requirements of the baggage and logger effectively, and also for ease of understanding, -/// as a reference semantics context type can be very confusing to use when shared between multiple threads, as often is the case in server side environments. -/// -/// It is STRONGLY encouraged to use the `DefaultLoggingContext` as inspiration for a correct implementation of a `LoggingContext`, -/// as the relationship between `Logger` and `Baggage` can be tricky to wrap your head around at first. -public protocol LoggingContext { - /// Get the `Baggage` container. - /// - /// ### Implementation notes - /// Libraries and/or frameworks which conform to this protocol with their "Framework Context" types MUST - /// ensure that a modification of the baggage is properly represented in the associated `logger`. Users expect values - /// from the baggage be visible in their log statements issued via `context.logger.info()`. - /// - /// Please refer to `DefaultLoggingContext`'s implementation for a reference implementation, - /// here a short snippet of how the baggage itself should be implemented: - /// - /// public var baggage: Baggage { - /// willSet { - /// self._logger.updateMetadata(previous: self.baggage, latest: newValue) - /// } - /// } - /// - /// #### Thread Safety - /// Implementations / MUST take care of thread-safety of modifications of the baggage. They can achieve this by such - /// context type being a pure `struct` or by implementing Copy-on-Write semantics for their type, the latter gives - /// many benefits, allowing the context to avoid being copied unless needed to (e.g. if the context type contains - /// many other values, in addition to the baggage). - var baggage: Baggage { get set } - - /// The `Logger` associated with this context carrier. - /// - /// It automatically populates the loggers metadata based on the `Baggage` associated with this context object. - /// - /// ### Implementation notes - /// Libraries and/or frameworks which conform to this protocol with their "Framework Context" types, - /// SHOULD implement this logger by wrapping the "raw" logger associated with `_logger.with(self.baggage)` function, - /// which efficiently handles the bridging of baggage to logging metadata values. - /// - /// If a new logger is set, it MUST populate itself with the latest (current) baggage of the context, - /// this is to ensure that even if users set a new logger (completely "fresh") here, the metadata from the baggage - /// still will properly be logged in other pieces of the application where the context might be passed to. - /// - /// A correct implementation might look like the following: - /// - /// public var _logger: Logger - /// public var logger: Logger { - /// get { - /// return self._logger - /// } - /// set { - /// self._logger = newValue - /// // Since someone could have completely replaced the logger (not just changed the log level), - /// // we have to update the baggage again, since perhaps the new logger has empty metadata. - /// self._logger.updateMetadata(previous: .topLevel, latest: self.baggage) - /// } - /// } - /// } - /// - /// - /// #### Thread Safety - /// Implementations MUST ensure the thread-safety of mutating the logger. This is usually handled best by the - /// framework context itself being a Copy-on-Write type, however the exact safety mechanism is left up to the libraries. - var logger: Logger { get set } -} - -/// A default `LoggingContext` type. -/// -/// It is a carrier of contextual `Baggage` and related `Logger`, allowing to log and trace throughout a system. -/// -/// Any values set on the `baggage` will be made accessible to the logger as call-site metadata, allowing it to log those. -/// -/// ### Logged Metadata and Baggage Items -/// -/// Please refer to your configured log handler documentation about how to configure which metadata values should be logged -/// and which not, as each log handler may handle and configure those differently. The default implementations log *all* -/// metadata/baggage values present, which often is the right thing, however in larger systems one may want to choose a -/// log handler which allows for configuring these details. -/// -/// ### Accepting context types in APIs -/// -/// It is preferred to accept values of `LoggingContext` in public library APIs, e.g.: -/// -/// func call(string: String, context: LoggingContext) -> Thing -/// -/// The context parameter SHOULD be positioned as the *last non-optional not-function parameter*. -/// If unsure how to pass/accept the context, please refer to the project's README for more examples and exact context passing guidelines. -/// -/// - SeeAlso: `CoreBaggage.Baggage` -/// - SeeAlso: `Logging.Logger` -public struct DefaultLoggingContext: LoggingContext { - // We need to store the logger as `_logger` in order to avoid cyclic updates triggering when baggage changes - public var _logger: Logger - public var logger: Logger { - get { - return self._logger - } - set { - self._logger = newValue - // Since someone could have completely replaced the logger (not just changed the log level), - // we have to update the baggage again, since perhaps the new logger has empty metadata. - self._logger.updateMetadata(previous: .topLevel, latest: self.baggage) - } - } - - /// The `Baggage` carried with this context. - /// It's values will automatically be made available to the `logger` as metadata when logging. - /// - /// Baggage values are different from plain logging metadata in that they are intended to be - /// carried across process and node boundaries (serialized and deserialized) and are made - /// available to instruments using `swift-distributed-tracing`. - public var baggage: Baggage { - willSet { - // every time the baggage changes, we need to update the logger; - // values removed from the baggage are also removed from the logger metadata. - // - // This implementation generally is a tradeoff, we bet on logging being performed far more often than baggage - // being changed; We do this logger update eagerly, so even if we never log anything, the logger has to be updated. - // Systems which never or rarely log will take the hit for it here. The alternative tradeoff to map lazily as `logger.with(baggage)` - // is available as well, but users would have to build their own context and specifically make use of that then -- that approach - // allows to not pay the mapping cost up front, but only if a log statement is made (but then again, the cost is paid every time we log something). - self._logger.updateMetadata(previous: self.baggage, latest: newValue) - } - } - - /// Create a default context, which will update the logger any time the context.baggage is modified. - public init(logger: Logger, baggage: Baggage) { - self._logger = logger - self.baggage = baggage - self._logger.updateMetadata(previous: .topLevel, latest: baggage) - } - - /// Create a default context, which will update the logger any time the context.baggage is modified. - public init(context: LoggingContext) { - self._logger = context.logger - self.baggage = context.baggage - self._logger.updateMetadata(previous: .topLevel, latest: self.baggage) - } -} - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: `with...` functions - -extension DefaultLoggingContext { - /// Fluent API allowing for modification of underlying logger when passing the context to other functions. - /// - /// - Parameter logger: Logger that should replace the underlying logger of this context. - /// - Returns: new context, with the passed in `logger` - public func withLogger(_ logger: Logger) -> DefaultLoggingContext { - var copy = self - copy.logger = logger - return copy - } - - /// Fluent API allowing for modification of underlying logger when passing the context to other functions. - /// - /// - Parameter logger: Logger that should replace the underlying logger of this context. - /// - Returns: new context, with the passed in `logger` - public func withLogger(_ function: (inout Logger) -> Void) -> DefaultLoggingContext { - var logger = self.logger - function(&logger) - return .init(logger: logger, baggage: self.baggage) - } - - /// Fluent API allowing for modification of underlying log level when passing the context to other functions. - /// - /// - Parameter logLevel: New log level which should be used to create the new context - /// - Returns: new context, with the passed in `logLevel` used for the underlying logger - public func withLogLevel(_ logLevel: Logger.Level) -> DefaultLoggingContext { - var copy = self - copy.logger.logLevel = logLevel - return copy - } - - /// Fluent API allowing for modification a few baggage values when passing the context to other functions, e.g. - /// - /// makeRequest(url, context: context.withBaggage { - /// $0.traceID = "fake-value" - /// $0.calledFrom = #function - /// }) - /// - /// - Parameter function: - public func withBaggage(_ function: (inout Baggage) -> Void) -> DefaultLoggingContext { - var baggage = self.baggage - function(&baggage) - return self.withBaggage(baggage) - } - - /// Fluent API allowing for replacement of underlying baggage when passing the context to other functions. - /// - /// - Warning: Use with caution, generally it is not recommended to modify an entire baggage, but rather only add a few values to it. - /// - /// - Parameter baggage: baggage that should *replace* the context's current baggage. - /// - Returns: new context, with the passed in baggage - public func withBaggage(_ baggage: Baggage) -> DefaultLoggingContext { - var copy = self - copy.baggage = baggage - return copy - } -} - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Context Initializers - -extension DefaultLoggingContext { - /// Creates a new empty "top level" default baggage context, generally used as an "initial" context to immediately be populated with - /// some values by a framework or runtime. Another use case is for tasks starting in the "background" (e.g. on a timer), - /// which don't have a "request context" per se that they can pick up, and as such they have to create a "top level" - /// baggage for their work. - /// - /// It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests. - /// - /// ### Usage in frameworks and libraries - /// This function is really only intended to be used frameworks and libraries, at the "top-level" where a request's, - /// message's or task's processing is initiated. For example, a framework handling requests, should create an empty - /// context when handling a request only to immediately populate it with useful trace information extracted from e.g. - /// request headers. - /// - /// ### Usage in applications - /// Application code should never have to create an empty context during the processing lifetime of any request, - /// and only should create contexts if some processing is performed in the background - thus the naming of this property. - /// - /// Usually, a framework such as an HTTP server or similar "request handler" would already provide users - /// with a context to be passed along through subsequent calls. - /// - /// If unsure where to obtain a context from, prefer using `.TODO("Not sure where I should get a context from here?")`, - /// such that other developers are informed that the lack of context was not done on purpose, but rather because either - /// not being sure where to obtain a context from, or other framework limitations -- e.g. the outer framework not being - /// context aware just yet. - public static func topLevel(logger: Logger) -> DefaultLoggingContext { - return .init(logger: logger, baggage: .topLevel) - } -} - -extension DefaultLoggingContext { - /// A baggage context intended as a placeholder until a real value can be passed through a function call. - /// - /// It should ONLY be used while prototyping or when the passing of the proper context is not yet possible, - /// e.g. because an external library did not pass it correctly and has to be fixed before the proper context - /// can be obtained where the TO-DO is currently used. - /// - /// ## Crashing on TO-DO context creation - /// You may set the `BAGGAGE_CRASH_TODOS` variable while compiling a project in order to make calls to this function crash - /// with a fatal error, indicating where a to-do baggage context was used. This comes in handy when wanting to ensure that - /// a project never ends up using with code initially was written as "was lazy, did not pass context", yet the - /// project requires context passing to be done correctly throughout the application. Similar checks can be performed - /// at compile time easily using linters (not yet implemented), since it is always valid enough to detect a to-do context - /// being passed as illegal and warn or error when spotted. - /// - /// ## Example - /// - /// frameworkHandler { what in - /// hello(who: "World", baggage: .TODO(logger: logger, "The framework XYZ should be modified to pass us a context here, and we'd pass it along")) - /// } - /// - /// - Parameters: - /// - reason: Informational reason for developers, why a placeholder context was used instead of a proper one, - /// - Returns: Empty "to-do" baggage context which should be eventually replaced with a carried through one, or `background`. - public static func TODO(logger: Logger, _ reason: StaticString? = "", function: String = #function, file: String = #file, line: UInt = #line) -> DefaultLoggingContext { - let baggage = Baggage.TODO(reason, function: function, file: file, line: line) - #if BAGGAGE_CRASH_TODOS - fatalError("BAGGAGE_CRASH_TODOS: at \(file):\(line) (function \(function)), reason: \(reason)", file: file, line: line) - #else - return .init(logger: logger, baggage: baggage) - #endif - } -} diff --git a/Sources/BaggageBenchmarkTools/ArgParser.swift b/Sources/BaggageBenchmarkTools/ArgParser.swift deleted file mode 100644 index 99dc0a2..0000000 --- a/Sources/BaggageBenchmarkTools/ArgParser.swift +++ /dev/null @@ -1,256 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -//===--- ArgParse.swift ---------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -enum ArgumentError: Error { - case missingValue(String) - case invalidType(value: String, type: String, argument: String?) - case unsupportedArgument(String) -} - -extension ArgumentError: CustomStringConvertible { - public var description: String { - switch self { - case .missingValue(let key): - return "missing value for '\(key)'" - case .invalidType(let value, let type, let argument): - return (argument == nil) - ? "'\(value)' is not a valid '\(type)'" - : "'\(value)' is not a valid '\(type)' for '\(argument!)'" - case .unsupportedArgument(let argument): - return "unsupported argument '\(argument)'" - } - } -} - -/// Type-checked parsing of the argument value. -/// -/// - Returns: Typed value of the argument converted using the `parse` function. -/// -/// - Throws: `ArgumentError.invalidType` when the conversion fails. -func checked( - _ parse: (String) throws -> T?, - _ value: String, - argument: String? = nil -) throws -> T { - if let t = try parse(value) { return t } - var type = "\(T.self)" - if type.starts(with: "Optional<") { - let s = type.index(after: type.firstIndex(of: "<")!) - let e = type.index(before: type.endIndex) // ">" - type = String(type[s ..< e]) // strip Optional< > - } - throw ArgumentError.invalidType( - value: value, type: type, argument: argument - ) -} - -/// Parser that converts the program's command line arguments to typed values -/// according to the parser's configuration, storing them in the provided -/// instance of a value-holding type. -class ArgumentParser { - private var result: U - private var validOptions: [String] { - return self.arguments.compactMap { $0.name } - } - - private var arguments: [Argument] = [] - private let programName: String = { - // Strip full path from the program name. - let r = CommandLine.arguments[0].reversed() - let ss = r[r.startIndex ..< (r.firstIndex(of: "/") ?? r.endIndex)] - return String(ss.reversed()) - }() - - private var positionalArgs = [String]() - private var optionalArgsMap = [String: String]() - - /// Argument holds the name of the command line parameter, its help - /// desciption and a rule that's applied to process it. - /// - /// The the rule is typically a value processing closure used to convert it - /// into given type and storing it in the parsing result. - /// - /// See also: addArgument, parseArgument - struct Argument { - let name: String? - let help: String? - let apply: () throws -> Void - } - - /// ArgumentParser is initialized with an instance of a type that holds - /// the results of the parsing of the individual command line arguments. - init(into result: U) { - self.result = result - self.arguments += [ - Argument( - name: "--help", help: "show this help message and exit", - apply: self.printUsage - ), - ] - } - - private func printUsage() { - guard let _ = optionalArgsMap["--help"] else { return } - let space = " " - let maxLength = self.arguments.compactMap { $0.name?.count }.max()! - let padded = { (s: String) in - " \(s)\(String(repeating: space, count: maxLength - s.count)) " - } - let f: (String, String) -> String = { - "\(padded($0))\($1)" - .split(separator: "\n") - .joined(separator: "\n" + padded("")) - } - let positional = f("TEST", "name or number of the benchmark to measure") - let optional = self.arguments.filter { $0.name != nil } - .map { f($0.name!, $0.help ?? "") } - .joined(separator: "\n") - print( - """ - usage: \(self.programName) [--argument=VALUE] [TEST [TEST ...]] - positional arguments: - \(positional) - optional arguments: - \(optional) - """) - exit(0) - } - - /// Parses the command line arguments, returning the result filled with - /// specified argument values or report errors and exit the program if - /// the parsing fails. - public func parse() -> U { - do { - try self.parseArgs() // parse the argument syntax - try self.arguments.forEach { try $0.apply() } // type-check and store values - return self.result - } catch let error as ArgumentError { - fputs("error: \(error)\n", stderr) - exit(1) - } catch { - fflush(stdout) - fatalError("\(error)") - } - } - - /// Using CommandLine.arguments, parses the structure of optional and - /// positional arguments of this program. - /// - /// We assume that optional switch args are of the form: - /// - /// --opt-name[=opt-value] - /// -opt-name[=opt-value] - /// - /// with `opt-name` and `opt-value` not containing any '=' signs. Any - /// other option passed in is assumed to be a positional argument. - /// - /// - Throws: `ArgumentError.unsupportedArgument` on failure to parse - /// the supported argument syntax. - private func parseArgs() throws { - // For each argument we are passed... - for arg in CommandLine.arguments[1 ..< CommandLine.arguments.count] { - // If the argument doesn't match the optional argument pattern. Add - // it to the positional argument list and continue... - if !arg.starts(with: "-") { - self.positionalArgs.append(arg) - continue - } - // Attempt to split it into two components separated by an equals sign. - let components = arg.split(separator: "=") - let optionName = String(components[0]) - guard self.validOptions.contains(optionName) else { - throw ArgumentError.unsupportedArgument(arg) - } - var optionVal: String - switch components.count { - case 1: optionVal = "" - case 2: optionVal = String(components[1]) - default: - // If we do not have two components at this point, we can not have - // an option switch. This is an invalid argument. Bail! - throw ArgumentError.unsupportedArgument(arg) - } - self.optionalArgsMap[optionName] = optionVal - } - } - - /// Add a rule for parsing the specified argument. - /// - /// Stores the type-erased invocation of the `parseArgument` in `Argument`. - /// - /// Parameters: - /// - name: Name of the command line argument. E.g.: `--opt-arg`. - /// `nil` denotes positional arguments. - /// - property: Property on the `result`, to store the value into. - /// - defaultValue: Value used when the command line argument doesn't - /// provide one. - /// - help: Argument's description used when printing usage with `--help`. - /// - parser: Function that converts the argument value to given type `T`. - public func addArgument( - _ name: String?, - _ property: WritableKeyPath, - defaultValue: T? = nil, - help: String? = nil, - parser: @escaping (String) throws -> T? = { _ in nil } - ) { - self.arguments.append( - Argument(name: name, help: help) - { try self.parseArgument(name, property, defaultValue, parser) } - ) - } - - /// Process the specified command line argument. - /// - /// For optional arguments that have a value we attempt to convert it into - /// given type using the supplied parser, performing the type-checking with - /// the `checked` function. - /// If the value is empty the `defaultValue` is used instead. - /// The typed value is finally stored in the `result` into the specified - /// `property`. - /// - /// For the optional positional arguments, the [String] is simply assigned - /// to the specified property without any conversion. - /// - /// See `addArgument` for detailed parameter descriptions. - private func parseArgument( - _ name: String?, - _ property: WritableKeyPath, - _ defaultValue: T?, - _ parse: (String) throws -> T? - ) throws { - if let name = name, let value = optionalArgsMap[name] { - guard !value.isEmpty || defaultValue != nil - else { throw ArgumentError.missingValue(name) } - - self.result[keyPath: property] = value.isEmpty - ? defaultValue! - : try checked(parse, value, argument: name) - } else if name == nil { - self.result[keyPath: property] = self.positionalArgs as! T - } - } -} diff --git a/Sources/BaggageBenchmarkTools/BenchmarkCategory.swift b/Sources/BaggageBenchmarkTools/BenchmarkCategory.swift deleted file mode 100644 index 5f14228..0000000 --- a/Sources/BaggageBenchmarkTools/BenchmarkCategory.swift +++ /dev/null @@ -1,31 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// -// Based on: https://github.com/apple/swift/tree/cf53143a47278c2a465409a67376642515956777/benchmark/utils -// -//===----------------------------------------------------------------------===// - -public enum BenchmarkCategory: String { - // Most benchmarks are assumed to be "stable" and will be regularly tracked at - // each commit. A handful may be marked unstable if continually tracking them is - // counterproductive. - case unstable - - // Explicit skip marker - case skip - - // --- custom --- - case logging -} diff --git a/Sources/BaggageBenchmarkTools/BenchmarkTools.swift b/Sources/BaggageBenchmarkTools/BenchmarkTools.swift deleted file mode 100644 index 85644d3..0000000 --- a/Sources/BaggageBenchmarkTools/BenchmarkTools.swift +++ /dev/null @@ -1,230 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// -// Based on: https://github.com/apple/swift/tree/cf53143a47278c2a465409a67376642515956777/benchmark/utils -// -//===----------------------------------------------------------------------===// - -#if os(Linux) -import Glibc -#else -import Darwin -#endif - -extension BenchmarkCategory: CustomStringConvertible { - public var description: String { - return self.rawValue - } -} - -extension BenchmarkCategory: Comparable { - public static func < (lhs: BenchmarkCategory, rhs: BenchmarkCategory) -> Bool { - return lhs.rawValue < rhs.rawValue - } -} - -public struct BenchmarkPlatformSet: OptionSet { - public let rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - public static let darwin = BenchmarkPlatformSet(rawValue: 1 << 0) - public static let linux = BenchmarkPlatformSet(rawValue: 1 << 1) - - public static var currentPlatform: BenchmarkPlatformSet { - #if os(Linux) - return .linux - #else - return .darwin - #endif - } - - public static var allPlatforms: BenchmarkPlatformSet { - return [.darwin, .linux] - } -} - -public struct BenchmarkInfo { - /// The name of the benchmark that should be displayed by the harness. - public var name: String - - /// Shadow static variable for runFunction. - private var _runFunction: (Int) -> Void - - /// A function that invokes the specific benchmark routine. - public var runFunction: ((Int) -> Void)? { - if !self.shouldRun { - return nil - } - return self._runFunction - } - - /// A set of category tags that describe this benchmark. This is used by the - /// harness to allow for easy slicing of the set of benchmarks along tag - /// boundaries, e.x.: run all string benchmarks or ref count benchmarks, etc. - public var tags: Set - - /// The platforms that this benchmark supports. This is an OptionSet. - private var unsupportedPlatforms: BenchmarkPlatformSet - - /// Shadow variable for setUpFunction. - private var _setUpFunction: (() -> Void)? - - /// An optional function that if non-null is run before benchmark samples - /// are timed. - public var setUpFunction: (() -> Void)? { - if !self.shouldRun { - return nil - } - return self._setUpFunction - } - - /// Shadow static variable for computed property tearDownFunction. - private var _tearDownFunction: (() -> Void)? - - /// An optional function that if non-null is run after samples are taken. - public var tearDownFunction: (() -> Void)? { - if !self.shouldRun { - return nil - } - return self._tearDownFunction - } - - public var legacyFactor: Int? - - public init( - name: String, runFunction: @escaping (Int) -> Void, tags: [BenchmarkCategory], - setUpFunction: (() -> Void)? = nil, - tearDownFunction: (() -> Void)? = nil, - unsupportedPlatforms: BenchmarkPlatformSet = [], - legacyFactor: Int? = nil - ) { - self.name = name - self._runFunction = runFunction - self.tags = Set(tags) - self._setUpFunction = setUpFunction - self._tearDownFunction = tearDownFunction - self.unsupportedPlatforms = unsupportedPlatforms - self.legacyFactor = legacyFactor - } - - /// Returns true if this benchmark should be run on the current platform. - var shouldRun: Bool { - return !self.unsupportedPlatforms.contains(.currentPlatform) - } -} - -extension BenchmarkInfo: Comparable { - public static func < (lhs: BenchmarkInfo, rhs: BenchmarkInfo) -> Bool { - return lhs.name < rhs.name - } - - public static func == (lhs: BenchmarkInfo, rhs: BenchmarkInfo) -> Bool { - return lhs.name == rhs.name - } -} - -extension BenchmarkInfo: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(self.name) - } -} - -// Linear function shift register. -// -// This is just to drive benchmarks. I don't make any claim about its -// strength. According to Wikipedia, it has the maximal period for a -// 32-bit register. -struct LFSR { - // Set the register to some seed that I pulled out of a hat. - var lfsr: UInt32 = 0xB789_78E7 - - mutating func shift() { - self.lfsr = (self.lfsr >> 1) ^ (UInt32(bitPattern: -Int32(self.lfsr & 1)) & 0xD000_0001) - } - - mutating func randInt() -> Int64 { - var result: UInt32 = 0 - for _ in 0 ..< 32 { - result = (result << 1) | (self.lfsr & 1) - self.shift() - } - return Int64(bitPattern: UInt64(result)) - } -} - -var lfsrRandomGenerator = LFSR() - -// Start the generator from the beginning -public func SRand() { - lfsrRandomGenerator = LFSR() -} - -public func Random() -> Int64 { - return lfsrRandomGenerator.randInt() -} - -@inlinable // FIXME(inline-always) -@inline(__always) -public func CheckResults( - _ resultsMatch: Bool, - file: StaticString = #file, - function: StaticString = #function, - line: Int = #line -) { - guard _fastPath(resultsMatch) else { - print("Incorrect result in \(function), \(file):\(line)") - abort() - } -} - -public func False() -> Bool { return false } - -/// This is a dummy protocol to test the speed of our protocol dispatch. -public protocol SomeProtocol { func getValue() -> Int } -struct MyStruct: SomeProtocol { - init() {} - func getValue() -> Int { return 1 } -} - -public func someProtocolFactory() -> SomeProtocol { return MyStruct() } - -// Just consume the argument. -// It's important that this function is in another module than the tests -// which are using it. -@inline(never) -public func blackHole(_: T) {} - -// Return the passed argument without letting the optimizer know that. -@inline(never) -public func identity(_ x: T) -> T { - return x -} - -// Return the passed argument without letting the optimizer know that. -// It's important that this function is in another module than the tests -// which are using it. -@inline(never) -public func getInt(_ x: Int) -> Int { return x } - -// The same for String. -@inline(never) -public func getString(_ s: String) -> String { return s } - -// The same for Substring. -@inline(never) -public func getSubstring(_ s: Substring) -> Substring { return s } diff --git a/Sources/BaggageBenchmarkTools/DriverUtils.swift b/Sources/BaggageBenchmarkTools/DriverUtils.swift deleted file mode 100644 index a621504..0000000 --- a/Sources/BaggageBenchmarkTools/DriverUtils.swift +++ /dev/null @@ -1,722 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -//===--- DriverUtils.swift ------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -#if os(Linux) -import Glibc -#else -import Darwin -// import LibProc -#endif - -public struct BenchResults { - typealias T = Int - private let samples: [T] - let maxRSS: Int? - let stats: Stats - - init(_ samples: [T], maxRSS: Int?) { - self.samples = samples.sorted() - self.maxRSS = maxRSS - self.stats = self.samples.reduce(into: Stats(), Stats.collect) - } - - /// Return measured value for given `quantile`. - /// - /// Equivalent to quantile estimate type R-1, SAS-3. See: - /// https://en.wikipedia.org/wiki/Quantile#Estimating_quantiles_from_a_sample - subscript(_ quantile: Double) -> T { - let index = Swift.max( - 0, - Int((Double(self.samples.count) * quantile).rounded(.up)) - 1 - ) - return self.samples[index] - } - - var sampleCount: T { return self.samples.count } - var min: T { return self.samples.first! } - var max: T { return self.samples.last! } - var mean: T { return Int(self.stats.mean.rounded()) } - var sd: T { return Int(self.stats.standardDeviation.rounded()) } - var median: T { return self[0.5] } -} - -public var registeredBenchmarks: [BenchmarkInfo] = [] - -enum TestAction { - case run - case listTests -} - -struct TestConfig { - /// The delimiter to use when printing output. - let delim: String - - /// Duration of the test measurement in seconds. - /// - /// Used to compute the number of iterations, if no fixed amount is specified. - /// This is useful when one wishes for a test to run for a - /// longer amount of time to perform performance analysis on the test in - /// instruments. - let sampleTime: Double - - /// Number of iterations averaged in the sample. - /// When not specified, we'll compute the number of iterations to be averaged - /// in the sample from the actual runtime and the desired `sampleTime`. - let numIters: Int? - - /// The number of samples we should take of each test. - let numSamples: Int? - - /// Quantiles to report in results. - let quantile: Int? - - /// Time unit in which to report results (nanos, micros, millis) (default: nanoseconds) - let timeUnit: TimeUnit - - /// Report quantiles with delta encoding. - let delta: Bool - - /// Is verbose output enabled? - let verbose: Bool - - // Should we log the test's memory usage? - let logMemory: Bool - - /// After we run the tests, should the harness sleep to allow for utilities - /// like leaks that require a PID to run on the test harness. - let afterRunSleep: UInt32? - - /// The list of tests to run. - let tests: [(index: String, info: BenchmarkInfo)] - - let action: TestAction - - init(_ registeredBenchmarks: [BenchmarkInfo]) { - struct PartialTestConfig { - var delim: String? - var tags, skipTags: Set? - var numSamples: UInt? - var numIters: UInt? - var quantile: UInt? - var timeUnit: String? - var delta: Bool? - var afterRunSleep: UInt32? - var sampleTime: Double? - var verbose: Bool? - var logMemory: Bool? - var action: TestAction? - var tests: [String]? - } - - // Custom value type parsers - func tags(tags: String) throws -> Set { - // We support specifying multiple tags by splitting on comma, i.e.: - // --tags=Array,Dictionary - // --skip-tags=Array,Set,unstable,skip - return Set( - try tags.split(separator: ",").map(String.init).map { - try checked({ BenchmarkCategory(rawValue: $0) }, $0) - } - ) - } - func finiteDouble(value: String) -> Double? { - return Double(value).flatMap { $0.isFinite ? $0 : nil } - } - - // Configure the command line argument parser - let p = ArgumentParser(into: PartialTestConfig()) - p.addArgument( - "--num-samples", \.numSamples, - help: "number of samples to take per benchmark;\n" + - "default: 1 or auto-scaled to measure for\n" + - "`sample-time` if num-iters is also specified\n", - parser: { UInt($0) } - ) - p.addArgument( - "--num-iters", \.numIters, - help: "number of iterations averaged in the sample;\n" + - "default: auto-scaled to measure for `sample-time`", - parser: { UInt($0) } - ) - p.addArgument( - "--quantile", \.quantile, - help: "report quantiles instead of normal dist. stats;\n" + - "use 4 to get a five-number summary with quartiles,\n" + - "10 (deciles), 20 (ventiles), 100 (percentiles), etc.", - parser: { UInt($0) } - ) - p.addArgument( - "--time-unit", \.timeUnit, - help: "time unit to be used for reported measurements;\n" + - "supported values: ns, us, ms; default: ns", - parser: { $0 } - ) - p.addArgument( - "--delta", \.delta, defaultValue: true, - help: "report quantiles with delta encoding" - ) - p.addArgument( - "--sample-time", \.sampleTime, - help: "duration of test measurement in seconds\ndefault: 1", - parser: finiteDouble - ) - p.addArgument( - "--verbose", \.verbose, defaultValue: true, - help: "increase output verbosity" - ) - p.addArgument( - "--memory", \.logMemory, defaultValue: true, - help: "log the change in maximum resident set size (MAX_RSS)" - ) - p.addArgument( - "--delim", \.delim, - help: "value delimiter used for log output; default: ,", - parser: { $0 } - ) - p.addArgument( - "--tags", \PartialTestConfig.tags, - help: "run tests matching all the specified categories", - parser: tags - ) - p.addArgument( - "--skip-tags", \PartialTestConfig.skipTags, defaultValue: [], - help: "don't run tests matching any of the specified\n" + - "categories; default: unstable,skip", - parser: tags - ) - p.addArgument( - "--sleep", \.afterRunSleep, - help: "number of seconds to sleep after benchmarking", - parser: { UInt32($0) } - ) - p.addArgument( - "--list", \.action, defaultValue: .listTests, - help: "don't run the tests, just log the list of test \n" + - "numbers, names and tags (respects specified filters)" - ) - p.addArgument(nil, \.tests) // positional arguments - let c = p.parse() - - // Configure from the command line arguments, filling in the defaults. - self.delim = c.delim ?? "," - self.sampleTime = c.sampleTime ?? 1.0 - self.numIters = c.numIters.map { Int($0) } - self.numSamples = c.numSamples.map { Int($0) } - self.quantile = c.quantile.map { Int($0) } - self.timeUnit = c.timeUnit.map { TimeUnit($0) } ?? TimeUnit.nanoseconds - self.delta = c.delta ?? false - self.verbose = c.verbose ?? false - self.logMemory = c.logMemory ?? false - self.afterRunSleep = c.afterRunSleep - self.action = c.action ?? .run - self.tests = TestConfig.filterTests( - registeredBenchmarks, - specifiedTests: Set(c.tests ?? []), - tags: c.tags ?? [], - skipTags: c.skipTags ?? [.unstable, .skip] - ) - - if self.logMemory, self.tests.count > 1 { - print( - """ - warning: The memory usage of a test, reported as the change in MAX_RSS, - is based on measuring the peak memory used by the whole process. - These results are meaningful only when running a single test, - not in the batch mode! - """) - } - - // We always prepare the configuration string and call the print to have - // the same memory usage baseline between verbose and normal mode. - let testList = self.tests.map { $0.1.name }.joined(separator: ", ") - let configuration = """ - --- CONFIG --- - NumSamples: \(numSamples ?? 0) - Verbose: \(verbose) - LogMemory: \(logMemory) - SampleTime: \(sampleTime) - NumIters: \(numIters ?? 0) - Quantile: \(quantile ?? 0) - TimeUnit: \(timeUnit) - Delimiter: \(String(reflecting: delim)) - Tests Filter: \(c.tests ?? []) - Tests to run: \(testList) - --- DATA ---\n - """ - print(self.verbose ? configuration : "", terminator: "") - } - - /// Returns the list of tests to run. - /// - /// - Parameters: - /// - registeredBenchmarks: List of all performance tests to be filtered. - /// - specifiedTests: List of explicitly specified tests to run. These can - /// be specified either by a test name or a test number. - /// - tags: Run tests tagged with all of these categories. - /// - skipTags: Don't run tests tagged with any of these categories. - /// - Returns: An array of test number and benchmark info tuples satisfying - /// specified filtering conditions. - static func filterTests( - _ registeredBenchmarks: [BenchmarkInfo], - specifiedTests: Set, - tags: Set, - skipTags: Set - ) -> [(index: String, info: BenchmarkInfo)] { - let allTests = registeredBenchmarks.sorted() - let indices = Dictionary( - uniqueKeysWithValues: - zip( - allTests.map { $0.name }, - (1...).lazy.map { String($0) } - ) - ) - - func byTags(b: BenchmarkInfo) -> Bool { - return b.tags.isSuperset(of: tags) && - b.tags.isDisjoint(with: skipTags) - } - func byNamesOrIndices(b: BenchmarkInfo) -> Bool { - return specifiedTests.contains(b.name) || - specifiedTests.contains(indices[b.name]!) - } // !! "`allTests` have been assigned an index" - return allTests - .filter(specifiedTests.isEmpty ? byTags : byNamesOrIndices) - .map { (index: indices[$0.name]!, info: $0) } - } -} - -struct Stats { - var n: Int = 0 - var S: Double = 0.0 - var mean: Double = 0.0 - var variance: Double { return self.n < 2 ? 0.0 : self.S / Double(self.n - 1) } - var standardDeviation: Double { return self.variance.squareRoot() } - - static func collect(_ s: inout Stats, _ x: Int) { - Stats.runningMeanVariance(&s, Double(x)) - } - - /// Compute running mean and variance using B. P. Welford's method. - /// - /// See Knuth TAOCP vol 2, 3rd edition, page 232, or - /// https://www.johndcook.com/blog/standard_deviation/ - static func runningMeanVariance(_ s: inout Stats, _ x: Double) { - let n = s.n + 1 - let (k, M_, S_) = (Double(n), s.mean, s.S) - let M = M_ + (x - M_) / k - let S = S_ + (x - M_) * (x - M) - (s.n, s.mean, s.S) = (n, M, S) - } -} - -#if SWIFT_RUNTIME_ENABLE_LEAK_CHECKER - -@_silgen_name("_swift_leaks_startTrackingObjects") -func startTrackingObjects(_: UnsafePointer) -> Void -@_silgen_name("_swift_leaks_stopTrackingObjects") -func stopTrackingObjects(_: UnsafePointer) -> Int - -#endif - -public final class Timer { - #if os(Linux) - public typealias TimeT = timespec - - public init() {} - - public func getTime() -> TimeT { - var ts = timespec(tv_sec: 0, tv_nsec: 0) - clock_gettime(CLOCK_REALTIME, &ts) - return ts - } - - public func getTimeAsInt() -> UInt64 { - return UInt64(getTime().tv_nsec) - } - - public func diffTimeInNanoSeconds(from start: TimeT, to end: TimeT) -> UInt64 { - let oneSecond = 1_000_000_000 // ns - var elapsed = timespec(tv_sec: 0, tv_nsec: 0) - if end.tv_nsec - start.tv_nsec < 0 { - elapsed.tv_sec = end.tv_sec - start.tv_sec - 1 - elapsed.tv_nsec = end.tv_nsec - start.tv_nsec + oneSecond - } else { - elapsed.tv_sec = end.tv_sec - start.tv_sec - elapsed.tv_nsec = end.tv_nsec - start.tv_nsec - } - return UInt64(elapsed.tv_sec) * UInt64(oneSecond) + UInt64(elapsed.tv_nsec) - } - - #else - public typealias TimeT = UInt64 - var info = mach_timebase_info_data_t(numer: 0, denom: 0) - - public init() { - mach_timebase_info(&info) - } - - public func getTime() -> TimeT { - return mach_absolute_time() - } - - public func getTimeAsInt() -> UInt64 { - return UInt64(getTime()) - } - - public func diffTimeInNanoSeconds(from start: TimeT, to end: TimeT) -> UInt64 { - let elapsed = end - start - return elapsed * UInt64(info.numer) / UInt64(info.denom) - } - #endif -} - -extension UInt64 { - public var nanoseconds: Int { return Int(self) } - public var microseconds: Int { return Int(self / 1000) } - public var milliseconds: Int { return Int(self / 1000 / 1000) } - public var seconds: Int { return Int(self / 1000 / 1000 / 1000) } -} - -enum TimeUnit: String { - case nanoseconds = "ns" - case microseconds = "μs" - case milliseconds = "ms" - case seconds = "s" - - init(_ from: String) { - switch from { - case "ns": self = .nanoseconds - case "us", "μs": self = .microseconds - case "ms": self = .milliseconds - case "s": self = .seconds - default: fatalError("Only the following time units are supported: ns, us, ms, s") - } - } - - static var `default` = TimeUnit.nanoseconds -} - -extension TimeUnit: CustomStringConvertible { - public var description: String { - return self.rawValue - } -} - -/// Performance test runner that measures benchmarks and reports the results. -final class TestRunner { - let c: TestConfig - let timer = Timer() - var start, end, lastYield: Timer.TimeT - let baseline = TestRunner.getResourceUtilization() - let schedulerQuantum = UInt64(10_000_000) // nanoseconds (== 10ms, macos) - init(_ config: TestConfig) { - self.c = config - let now = timer.getTime() - (start, end, lastYield) = (now, now, now) - } - - /// Offer to yield CPU to other processes and return current time on resume. - func yield() -> Timer.TimeT { - sched_yield() - return timer.getTime() - } - - #if os(Linux) - private static func getExecutedInstructions() -> UInt64 { - // FIXME: there is a Linux PMC API you can use to get this, but it's - // not quite so straightforward. - return 0 - } - - #else - private static func getExecutedInstructions() -> UInt64 { -// if #available(OSX 10.9, iOS 7.0, *) { -// var u = rusage_info_v4() -// let p = UnsafeMutablePointer(&u) -// p.withMemoryRebound(to: Optional.self, capacity: 1) { up in -// let _ = proc_pid_rusage(getpid(), RUSAGE_INFO_V4, up) -// } -// return u.ri_instructions -// } else { - return 0 -// } - } - #endif - - private static func getResourceUtilization() -> rusage { - #if canImport(Darwin) - let rusageSelf = RUSAGE_SELF - #else - let rusageSelf = RUSAGE_SELF.rawValue - #endif - var u = rusage(); getrusage(rusageSelf, &u); return u - } - - /// Returns maximum resident set size (MAX_RSS) delta in bytes. - /// - /// This method of estimating memory usage is valid only for executing single - /// benchmark. That's why we don't worry about reseting the `baseline` in - /// `resetMeasurements`. - /// - // FIXME: This current implementation doesn't work on Linux. It is disabled - /// permanently to avoid linker errors. Feel free to fix. - func measureMemoryUsage() -> Int? { - #if os(Linux) - return nil - #else - guard c.logMemory else { return nil } - let current = TestRunner.getResourceUtilization() - let maxRSS = current.ru_maxrss - baseline.ru_maxrss - #if canImport(Darwin) - let pageSize = _SC_PAGESIZE - #else - let pageSize = Int32(_SC_PAGESIZE) - #endif - let pages = { maxRSS / sysconf(pageSize) } - func deltaEquation(_ stat: KeyPath) -> String { - let b = baseline[keyPath: stat], c = current[keyPath: stat] - return "\(c) - \(b) = \(c - b)" - } - logVerbose( - """ - MAX_RSS \(deltaEquation(\rusage.ru_maxrss)) (\(pages()) pages) - ICS \(deltaEquation(\rusage.ru_nivcsw)) - VCS \(deltaEquation(\rusage.ru_nvcsw)) - """) - return maxRSS - #endif - } - - private func startMeasurement() { - let spent = timer.diffTimeInNanoSeconds(from: lastYield, to: end) - let nextSampleEstimate = UInt64(Double(lastSampleTime) * 1.5) - - if spent + nextSampleEstimate < schedulerQuantum { - start = timer.getTime() - } else { - logVerbose(" Yielding after ~\(spent.nanoseconds) ns") - let now = yield() - (start, lastYield) = (now, now) - } - } - - private func stopMeasurement() { - end = timer.getTime() - } - - private func resetMeasurements() { - let now = yield() - (start, end, lastYield) = (now, now, now) - } - - /// Time in nanoseconds spent running the last function - var lastSampleTime: UInt64 { - return timer.diffTimeInNanoSeconds(from: start, to: end) - } - - /// Measure the `fn` and return the average sample time per iteration (in c.timeUnit). - func measure(_ name: String, fn: (Int) -> Void, numIters: Int) -> Int { - #if SWIFT_RUNTIME_ENABLE_LEAK_CHECKER - name.withCString { p in startTrackingObjects(p) } - #endif - - startMeasurement() - fn(numIters) - stopMeasurement() - - #if SWIFT_RUNTIME_ENABLE_LEAK_CHECKER - name.withCString { p in stopTrackingObjects(p) } - #endif - - switch c.timeUnit { - case .nanoseconds: return lastSampleTime.nanoseconds / numIters - case .microseconds: return lastSampleTime.microseconds / numIters - case .milliseconds: return lastSampleTime.milliseconds / numIters - case .seconds: return lastSampleTime.seconds / numIters - } - } - - func logVerbose(_ msg: @autoclosure () -> String) { - if c.verbose { print(msg()) } - } - - /// Run the benchmark and return the measured results. - func run(_ test: BenchmarkInfo) -> BenchResults? { - // Before we do anything, check that we actually have a function to - // run. If we don't it is because the benchmark is not supported on - // the platform and we should skip it. - guard let testFn = test.runFunction else { - logVerbose("Skipping unsupported benchmark \(test.name)!") - return nil - } - logVerbose("Running \(test.name)") - - var samples: [Int] = [] - - func addSample(_ time: Int) { - logVerbose(" Sample \(samples.count),\(time)") - samples.append(time) - } - - resetMeasurements() - if let setUp = test.setUpFunction { - setUp() - stopMeasurement() - logVerbose(" SetUp \(lastSampleTime.microseconds)") - resetMeasurements() - } - - // Determine number of iterations for testFn to run for desired time. - func iterationsPerSampleTime() -> (numIters: Int, oneIter: Int) { - let oneIter = measure(test.name, fn: testFn, numIters: 1) - if oneIter > 0 { - let timePerSample = Int(c.sampleTime * 1_000_000.0) // microseconds (μs) - return (max(timePerSample / oneIter, 1), oneIter) - } else { - return (1, oneIter) - } - } - - // Determine the scale of measurements. Re-use the calibration result if - // it is just one measurement. - func calibrateMeasurements() -> Int { - let (numIters, oneIter) = iterationsPerSampleTime() - if numIters == 1 { addSample(oneIter) } - else { resetMeasurements() } // for accurate yielding reports - return numIters - } - - let numIters = min( // Cap to prevent overflow on 32-bit systems when scaled - Int.max / 10000, // by the inner loop multiplier inside the `testFn`. - c.numIters ?? calibrateMeasurements() - ) - - let numSamples = c.numSamples ?? min( - 200, // Cap the number of samples - c.numIters == nil ? 1 : calibrateMeasurements() - ) - - samples.reserveCapacity(numSamples) - logVerbose(" Collecting \(numSamples) samples.") - logVerbose(" Measuring with scale \(numIters).") - for _ in samples.count ..< numSamples { - addSample(measure(test.name, fn: testFn, numIters: numIters)) - } - - test.tearDownFunction?() - if let lf = test.legacyFactor { - logVerbose(" Applying legacy factor: \(lf)") - samples = samples.map { $0 * lf } - } - - return BenchResults(samples, maxRSS: measureMemoryUsage()) - } - - var header: String { - let withUnit = { $0 + "(\(self.c.timeUnit))" } - let withDelta = { "𝚫" + $0 } - func quantiles(q: Int) -> [String] { - // See https://en.wikipedia.org/wiki/Quantile#Specialized_quantiles - let prefix = [ - 2: "MEDIAN", 3: "T", 4: "Q", 5: "QU", 6: "S", 7: "O", 10: "D", - 12: "Dd", 16: "H", 20: "V", 33: "TT", 100: "P", 1000: "Pr", - ][q, default: "\(q)-q"] - let base20 = "0123456789ABCDEFGHIJ".map { String($0) } - let index: (Int) -> String = - { q == 2 ? "" : q <= 20 ? base20[$0] : String($0) } - let tail = (1 ..< q).map { prefix + index($0) } + ["MAX"] - return [withUnit("MIN")] + tail.map(c.delta ? withDelta : withUnit) - } - return ( - ["#", "TEST", "SAMPLES"] + - ( - c.quantile.map(quantiles) - ?? ["MIN", "MAX", "MEAN", "SD", "MEDIAN"].map(withUnit) - ) + - (c.logMemory ? ["MAX_RSS(B)"] : []) - ).joined(separator: c.delim) - } - - /// Execute benchmarks and continuously report the measurement results. - func runBenchmarks() { - var testCount = 0 - - func report(_ index: String, _ t: BenchmarkInfo, results: BenchResults?) { - func values(r: BenchResults) -> [String] { - func quantiles(q: Int) -> [Int] { - let qs = (0 ... q).map { i in r[Double(i) / Double(q)] } - return c.delta ? - qs.reduce(into: (encoded: [], last: 0)) { - $0.encoded.append($1 - $0.last); $0.last = $1 - }.encoded : qs - } - return ( - [r.sampleCount] + - ( - c.quantile.map(quantiles) - ?? [r.min, r.max, r.mean, r.sd, r.median] - ) + - [r.maxRSS].compactMap { $0 } - ).map { (c.delta && $0 == 0) ? "" : String($0) } // drop 0s in deltas - } - let benchmarkStats = ( - [index, t.name] + (results.map(values) ?? ["Unsupported"]) - ).joined(separator: c.delim) - - print(benchmarkStats) - fflush(stdout) - - if results != nil { - testCount += 1 - } - } - - print(header) - - for (index, test) in c.tests { - report(index, test, results: run(test)) - } - - print("\nTotal performance tests executed: \(testCount)") - } -} - -public func main() { - let config = TestConfig(registeredBenchmarks) - switch config.action { - case .listTests: - print("#\(config.delim)Test\(config.delim)[Tags]") - for (index, t) in config.tests { - let testDescription = [index, t.name, t.tags.sorted().description] - .joined(separator: config.delim) - print(testDescription) - } - case .run: - TestRunner(config).runBenchmarks() - if let x = config.afterRunSleep { - sleep(x) - } - } -} diff --git a/Sources/BaggageBenchmarkTools/README_SWIFT.md b/Sources/BaggageBenchmarkTools/README_SWIFT.md deleted file mode 100644 index a7be5c3..0000000 --- a/Sources/BaggageBenchmarkTools/README_SWIFT.md +++ /dev/null @@ -1,6 +0,0 @@ -## Swift Benchmark Utils - -> This benchmarking infrastructure is copied from -https://github.com/apple/swift/tree/cf53143a47278c2a465409a67376642515956777/benchmark/utils -with the intent of producing similar look and feel, as well as because we need some benchmark infra. -> When feasible we will aim to collaborate and contribute improvements back to the mainline Swift project. diff --git a/Sources/BaggageBenchmarks/BaggagePassingBenchmarks.swift b/Sources/BaggageBenchmarks/BaggagePassingBenchmarks.swift deleted file mode 100644 index 4415f8b..0000000 --- a/Sources/BaggageBenchmarks/BaggagePassingBenchmarks.swift +++ /dev/null @@ -1,190 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Baggage -import BaggageBenchmarkTools -import Dispatch -import class Foundation.NSLock - -public let BaggagePassingBenchmarks: [BenchmarkInfo] = [ - // ==== ---------------------------------------------------------------------------------------------------------------- - // MARK: "Read only" context passing around - BenchmarkInfo( - name: "BaggagePassingBenchmarks.pass_async_empty_100_000 ", - runFunction: { _ in - let context = Baggage.topLevel - pass_async(context: context, times: 100_000) - }, - tags: [], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - BenchmarkInfo( - name: "BaggagePassingBenchmarks.pass_async_smol_100_000 ", - runFunction: { _ in - var context = Baggage.topLevel - context.k1 = "one" - context.k2 = "two" - context.k3 = "three" - context.k4 = "four" - pass_async(context: context, times: 100_000) - }, - tags: [], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - BenchmarkInfo( - name: "BaggagePassingBenchmarks.pass_async_small_nonconst_100_000", - runFunction: { _ in - var context = Baggage.topLevel - context.k1 = "\(Int.random(in: 1 ... Int.max))" - context.k2 = "\(Int.random(in: 1 ... Int.max))" - context.k3 = "\(Int.random(in: 1 ... Int.max))" - context.k4 = "\(Int.random(in: 1 ... Int.max))" - pass_async(context: context, times: 100_000) - }, - tags: [], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - - // ==== ------------------------------------------------------------------------------------------------------------ - // MARK: Passing & Mutating - // Since the context is backed by a dictionary (and nothing else) we rely on its CoW semantics, those writes cause copies - // whilst the previous benchmarks which are read-only do not cause copies of the underlying storage (dictionary). - - BenchmarkInfo( - name: "BaggagePassingBenchmarks.pass_mut_async_small_100_000 ", - runFunction: { _ in - var context = Baggage.topLevel - context.k1 = "\(Int.random(in: 1 ... Int.max))" - context.k2 = "\(Int.random(in: 1 ... Int.max))" - context.k3 = "\(Int.random(in: 1 ... Int.max))" - context.k4 = "\(Int.random(in: 1 ... Int.max))" - pass_mut_async(context: context, times: 100_000) - }, - tags: [], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), -] - -private func setUp() { - // ... -} - -private func tearDown() { - // ... -} - -@inline(never) -func pass_async(context: Baggage, times remaining: Int) { - let latch = CountDownLatch(from: 1) - - func pass_async0(context: Baggage, times remaining: Int) { - if remaining == 0 { - latch.countDown() - } - DispatchQueue.global().async { - pass_async0(context: context, times: remaining - 1) - } - } - - pass_async0(context: context, times: remaining - 1) - - latch.wait() -} - -@inline(never) -func pass_mut_async(context: Baggage, times remaining: Int) { - var context = context - let latch = CountDownLatch(from: 1) - - func pass_async0(context: Baggage, times remaining: Int) { - if remaining == 0 { - latch.countDown() - } - - DispatchQueue.global().async { - // mutate the context - var context = context - context.passCounter = remaining - - pass_async0(context: context, times: remaining - 1) - } - } - - context.passCounter = remaining - pass_async0(context: context, times: remaining - 1) - - latch.wait() -} - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Baggage Keys - -private enum TestPassCounterKey: BaggageKey { - typealias Value = Int -} - -private enum TestK1: BaggageKey { - typealias Value = String -} - -private enum TestK2: BaggageKey { - typealias Value = String -} - -private enum TestK3: BaggageKey { - typealias Value = String -} - -private enum TestK4: BaggageKey { - typealias Value = String -} - -private enum TestKD1: BaggageKey { - typealias Value = [String: String] -} - -extension Baggage { - fileprivate var passCounter: TestPassCounterKey.Value { - get { return self[TestPassCounterKey.self] ?? 0 } - set { self[TestPassCounterKey.self] = newValue } - } - - fileprivate var k1: TestK1.Value? { - get { return self[TestK1.self] } - set { self[TestK1.self] = newValue } - } - - fileprivate var k2: TestK2.Value? { - get { return self[TestK2.self] } - set { self[TestK2.self] = newValue } - } - - fileprivate var k3: TestK3.Value? { - get { return self[TestK3.self] } - set { self[TestK3.self] = newValue } - } - - fileprivate var k4: TestK4.Value? { - get { return self[TestK4.self] } - set { self[TestK4.self] = newValue } - } - - fileprivate var kd1: TestKD1.Value? { - get { return self[TestKD1.self] } - set { self[TestKD1.self] = newValue } - } -} diff --git a/Sources/BaggageBenchmarks/LoggingContextBenchmarks.swift b/Sources/BaggageBenchmarks/LoggingContextBenchmarks.swift deleted file mode 100644 index 8937649..0000000 --- a/Sources/BaggageBenchmarks/LoggingContextBenchmarks.swift +++ /dev/null @@ -1,378 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Baggage -import BaggageBenchmarkTools -import Dispatch -import class Foundation.NSLock -import Logging - -private let message: Logger.Message = "Hello world how are you" - -func pad(_ label: String) -> String { - return "\(label)\(String(repeating: " ", count: max(0, 80 - label.count)))" -} - -public let LoggingContextBenchmarks: [BenchmarkInfo] = [ - // ==== ---------------------------------------------------------------------------------------------------------------- - // MARK: Baseline - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.0_log_noop_baseline_empty"), - runFunction: { iters in - let logger = Logger(label: "0_log_noop_baseline_empty", factory: { _ in SwiftLogNoOpLogHandler() }) - log_baseline(logger: logger, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.0_log_noop_baseline_smallMetadata"), - runFunction: { iters in - var logger = Logger(label: "0_log_noop_baseline_smallMetadata", factory: { _ in SwiftLogNoOpLogHandler() }) - logger[metadataKey: "k1"] = "k1-value" - logger[metadataKey: "k2"] = "k2-value" - logger[metadataKey: "k3"] = "k3-value" - log_baseline(logger: logger, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - - // ==== ---------------------------------------------------------------------------------------------------------------- - // MARK: Context / Baggage (Really do log) - - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.0_log_noop_loggerWithBaggage_small"), - runFunction: { iters in - let logger = Logger(label: "0_log_noop_loggerWithBaggage_small", factory: { _ in SwiftLogNoOpLogHandler() }) - var baggage = Baggage.topLevel - baggage[TestK1.self] = "k1-value" - baggage[TestK2.self] = "k2-value" - baggage[TestK3.self] = "k3-value" - log_loggerWithBaggage(logger: logger, baggage: baggage, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.0_log_noop_context_with_baggage_small"), - runFunction: { iters in - var context = DefaultLoggingContext.topLevel(logger: Logger(label: "0_log_noop_context_with_baggage_small", factory: { _ in SwiftLogNoOpLogHandler() })) - context.baggage[TestK1.self] = "k1-value" - context.baggage[TestK2.self] = "k2-value" - context.baggage[TestK3.self] = "k3-value" - log_throughContext(context: context, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - - // ==== ---------------------------------------------------------------------------------------------------------------- - // MARK: Context / Baggage (do actually emit the logs) - - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.1_log_real_baseline_empty"), - runFunction: { iters in - let logger = Logger(label: "1_log_real_baseline_empty", factory: StreamLogHandler.standardError) - log_baseline(logger: logger, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.1_log_real_baseline_smallMetadata"), - runFunction: { iters in - var logger = Logger(label: "1_log_real_baseline_smallMetadata", factory: StreamLogHandler.standardError) - logger[metadataKey: "k1"] = "k1-value" - logger[metadataKey: "k2"] = "k2-value" - logger[metadataKey: "k3"] = "k3-value" - log_baseline(logger: logger, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.1_log_real_loggerWithBaggage_small"), - runFunction: { iters in - let logger = Logger(label: "1_log_real_loggerWithBaggage_small", factory: StreamLogHandler.standardError) - var baggage = Baggage.topLevel - baggage[TestK1.self] = "k1-value" - baggage[TestK2.self] = "k2-value" - baggage[TestK3.self] = "k3-value" - log_loggerWithBaggage(logger: logger, baggage: baggage, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.1_log_real_context_with_baggage_small"), - runFunction: { iters in - var context = DefaultLoggingContext.topLevel(logger: Logger(label: "1_log_real_context_with_baggage_small", factory: StreamLogHandler.standardError)) - context.baggage[TestK1.self] = "k1-value" - context.baggage[TestK2.self] = "k2-value" - context.baggage[TestK3.self] = "k3-value" - log_throughContext(context: context, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - - // ==== ---------------------------------------------------------------------------------------------------------------- - // MARK: Context / Baggage (log not emitted because logLevel) - - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.2_log_real-trace_baseline_empty"), - runFunction: { iters in - let logger = Logger(label: "trace_baseline_empty", factory: StreamLogHandler.standardError) - log_baseline_trace(logger: logger, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.2_log_real-trace_baseline_smallMetadata"), - runFunction: { iters in - var logger = Logger(label: "2_log_real-trace_baseline_smallMetadata", factory: StreamLogHandler.standardError) - logger[metadataKey: "k1"] = "k1-value" - logger[metadataKey: "k2"] = "k2-value" - logger[metadataKey: "k3"] = "k3-value" - log_baseline_trace(logger: logger, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.2_log_real-trace_loggerWithBaggage_small"), - runFunction: { iters in - let logger = Logger(label: "2_log_real-trace_loggerWithBaggage_small", factory: StreamLogHandler.standardError) - var baggage = Baggage.topLevel - baggage[TestK1.self] = "k1-value" - baggage[TestK2.self] = "k2-value" - baggage[TestK3.self] = "k3-value" - log_loggerWithBaggage_trace(logger: logger, baggage: baggage, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.2_log_real-trace_context_with_baggage_small"), - runFunction: { iters in - var context = DefaultLoggingContext.topLevel(logger: Logger(label: "2_log_real-trace_context_with_baggage_small", factory: StreamLogHandler.standardError)) - context.baggage[TestK1.self] = "k1-value" - context.baggage[TestK2.self] = "k2-value" - context.baggage[TestK3.self] = "k3-value" - log_throughContext_trace(context: context, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - - // ==== ---------------------------------------------------------------------------------------------------------------- - // MARK: materialize once once - - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.3_log_real_small_context_materializeOnce"), - runFunction: { iters in - var context = DefaultLoggingContext.topLevel(logger: Logger(label: "3_log_real_context_materializeOnce", factory: StreamLogHandler.standardError)) - context.baggage[TestK1.self] = "k1-value" - context.baggage[TestK2.self] = "k2-value" - context.baggage[TestK3.self] = "k3-value" - log_materializeOnce(context: context, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), - BenchmarkInfo( - name: pad("LoggingContextBenchmarks.3_log_real-trace_small_context_materializeOnce"), - runFunction: { iters in - var context = DefaultLoggingContext.topLevel(logger: Logger(label: "3_log_real_context_materializeOnce", factory: StreamLogHandler.standardError)) - context.baggage[TestK1.self] = "k1-value" - context.baggage[TestK2.self] = "k2-value" - context.baggage[TestK3.self] = "k3-value" - log_materializeOnce_trace(context: context, iters: iters) - }, - tags: [ - .logging, - ], - setUpFunction: { setUp() }, - tearDownFunction: tearDown - ), -] - -private func setUp() { - // ... -} - -private func tearDown() { - // ... -} - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Benchmarks - -@inline(never) -func log_baseline(logger: Logger, iters remaining: Int) { - for _ in 0 ..< remaining { - logger.warning(message) - } -} - -@inline(never) -func log_baseline_trace(logger: Logger, iters remaining: Int) { - for _ in 0 ..< remaining { - logger.trace(message) - } -} - -@inline(never) -func log_loggerWithBaggage(logger: Logger, baggage: Baggage, iters remaining: Int) { - for _ in 0 ..< remaining { - logger.with(baggage).warning(message) - } -} - -@inline(never) -func log_throughContext(context: LoggingContext, iters remaining: Int) { - for _ in 0 ..< remaining { - context.logger.warning(message) - } -} - -@inline(never) -func log_loggerWithBaggage_trace(logger: Logger, baggage: Baggage, iters remaining: Int) { - for _ in 0 ..< remaining { - logger.with(baggage).trace(message) - } -} - -@inline(never) -func log_throughContext_trace(context: LoggingContext, iters remaining: Int) { - for _ in 0 ..< remaining { - context.logger.trace(message) - } -} - -@inline(never) -func log_materializeOnce_trace(context: LoggingContext, iters remaining: Int) { - var logger = context.logger - context.baggage.forEach { key, value in - logger[metadataKey: key.name] = "\(value)" - } - - for _ in 0 ..< remaining { - logger.trace(message) - } -} - -@inline(never) -func log_materializeOnce(context: LoggingContext, iters remaining: Int) { - var logger = context.logger - context.baggage.forEach { key, value in - logger[metadataKey: key.name] = "\(value)" - } - - for _ in 0 ..< remaining { - logger.warning(message) - } -} - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Baggage Keys - -private enum TestK1: BaggageKey { - typealias Value = String -} - -private enum TestK2: BaggageKey { - typealias Value = String -} - -private enum TestK3: BaggageKey { - typealias Value = String -} - -private enum TestK4: BaggageKey { - typealias Value = String -} - -private enum TestKD1: BaggageKey { - typealias Value = [String: String] -} - -extension Baggage { - fileprivate var k1: TestK1.Value? { - get { return self[TestK1.self] } - set { self[TestK1.self] = newValue } - } - - fileprivate var k2: TestK2.Value? { - get { return self[TestK2.self] } - set { self[TestK2.self] = newValue } - } - - fileprivate var k3: TestK3.Value? { - get { return self[TestK3.self] } - set { self[TestK3.self] = newValue } - } - - fileprivate var k4: TestK4.Value? { - get { return self[TestK4.self] } - set { self[TestK4.self] = newValue } - } - - fileprivate var kd1: TestKD1.Value? { - get { return self[TestKD1.self] } - set { self[TestKD1.self] = newValue } - } -} diff --git a/Sources/BaggageBenchmarks/locks.swift b/Sources/BaggageBenchmarks/locks.swift deleted file mode 100644 index c0bf51a..0000000 --- a/Sources/BaggageBenchmarks/locks.swift +++ /dev/null @@ -1,253 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) -import Darwin -#else -import Glibc -#endif - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: CountDownLatch - -internal class CountDownLatch { - private var counter: Int - private let condition: _Condition - private let lock: _Mutex - - init(from: Int) { - self.counter = from - self.condition = _Condition() - self.lock = _Mutex() - } - - /// Returns previous value before the decrement was issued. - func countDown() { - self.lock.synchronized { - self.counter -= 1 - - if self.counter == 0 { - self.condition.signalAll() - } - } - } - - var count: Int { - return self.lock.synchronized { - self.counter - } - } - - func wait() { - self.lock.synchronized { - while true { - if self.counter == 0 { - return // done - } - - self.condition.wait(lock) - } - } - } -} - -extension CountDownLatch: CustomStringConvertible { - public var description: String { - return "CountDownLatch(remaining:\(self.count)" - } -} - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Condition - -final class _Condition { - @usableFromInline - var condition = pthread_cond_t() - - public init() { - let error = pthread_cond_init(&self.condition, nil) - - switch error { - case 0: - return - default: - fatalError("Condition could not be created: \(error)") - } - } - - deinit { - pthread_cond_destroy(&condition) - } - - @inlinable - public func wait(_ mutex: _Mutex) { - let error = pthread_cond_wait(&self.condition, &mutex.mutex) - - switch error { - case 0: - return - case EPERM: - fatalError("Wait failed, mutex is not owned by this thread") - case EINVAL: - fatalError("Wait failed, condition is not valid") - default: - fatalError("Wait failed with unspecified error: \(error)") - } - } - -// @inlinable -// public func wait(_ mutex: _Mutex) -> Bool { -// let error = withUnsafePointer(to: time) { p -> Int32 in -// #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) -// return pthread_cond_timedwait_relative_np(&condition, &mutex.mutex, p) -// #else -// return pthread_cond_timedwait(&condition, &mutex.mutex, p) -// #endif -// } -// -// switch error { -// case 0: -// return true -// case ETIMEDOUT: -// return false -// case EPERM: -// fatalError("Wait failed, mutex is not owned by this thread") -// case EINVAL: -// fatalError("Wait failed, condition is not valid") -// default: -// fatalError("Wait failed with unspecified error: \(error)") -// } -// } - - @inlinable - public func signal() { - let error = pthread_cond_signal(&self.condition) - - switch error { - case 0: - return - case EINVAL: - fatalError("Signal failed, condition is not valid") - default: - fatalError("Signal failed with unspecified error: \(error)") - } - } - - @inlinable - public func signalAll() { - let error = pthread_cond_broadcast(&self.condition) - - switch error { - case 0: - return - case EINVAL: - fatalError("Signal failed, condition is not valid") - default: - fatalError("Signal failed with unspecified error: \(error)") - } - } -} - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Mutex - -final class _Mutex { - @usableFromInline - var mutex = pthread_mutex_t() - - public init() { - var attr = pthread_mutexattr_t() - pthread_mutexattr_init(&attr) - pthread_mutexattr_settype(&attr, Int32(PTHREAD_MUTEX_RECURSIVE)) - - let error = pthread_mutex_init(&self.mutex, &attr) - pthread_mutexattr_destroy(&attr) - - switch error { - case 0: - return - default: - fatalError("Could not create mutex: \(error)") - } - } - - deinit { - pthread_mutex_destroy(&mutex) - } - - @inlinable - public func lock() { - let error = pthread_mutex_lock(&self.mutex) - - switch error { - case 0: - return - case EDEADLK: - fatalError("Mutex could not be acquired because it would have caused a deadlock") - default: - fatalError("Failed with unspecified error: \(error)") - } - } - - @inlinable - public func unlock() { - let error = pthread_mutex_unlock(&self.mutex) - - switch error { - case 0: - return - case EPERM: - fatalError("Mutex could not be unlocked because it is not held by the current thread") - default: - fatalError("Unlock failed with unspecified error: \(error)") - } - } - - @inlinable - public func tryLock() -> Bool { - let error = pthread_mutex_trylock(&self.mutex) - - switch error { - case 0: - return true - case EBUSY: - return false - case EDEADLK: - fatalError("Mutex could not be acquired because it would have caused a deadlock") - default: - fatalError("Failed with unspecified error: \(error)") - } - } - - @inlinable - public func synchronized(_ f: () -> A) -> A { - self.lock() - - defer { - unlock() - } - - return f() - } - - @inlinable - public func synchronized(_ f: () throws -> A) throws -> A { - self.lock() - - defer { - unlock() - } - - return try f() - } -} diff --git a/Sources/BaggageBenchmarks/main.swift b/Sources/BaggageBenchmarks/main.swift deleted file mode 100644 index a62855b..0000000 --- a/Sources/BaggageBenchmarks/main.swift +++ /dev/null @@ -1,42 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import BaggageBenchmarkTools - -assert({ - print("===========================================================================") - print("= !! YOU ARE RUNNING BENCHMARKS IN DEBUG MODE !! =") - print("= When running on the command line, use: `swift run -c release` =") - print("===========================================================================") - return true -}()) - -@inline(__always) -private func registerBenchmark(_ bench: BenchmarkInfo) { - registeredBenchmarks.append(bench) -} - -@inline(__always) -private func registerBenchmark(_ benches: [BenchmarkInfo]) { - benches.forEach(registerBenchmark) -} - -@inline(__always) -private func registerBenchmark(_ name: String, _ function: @escaping (Int) -> Void, _ tags: [BenchmarkCategory]) { - registerBenchmark(BenchmarkInfo(name: name, runFunction: function, tags: tags)) -} - -registerBenchmark(BaggagePassingBenchmarks) -registerBenchmark(LoggingContextBenchmarks) - -main() diff --git a/Sources/InstrumentationBaggage/Baggage.swift b/Sources/InstrumentationBaggage/Baggage.swift new file mode 100644 index 0000000..06dc5dc --- /dev/null +++ b/Sources/InstrumentationBaggage/Baggage.swift @@ -0,0 +1,234 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Distributed Tracing Baggage +// open source project +// +// Copyright (c) 2020-2021 Apple Inc. and the Swift Distributed Tracing Baggage +// project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// A `Baggage` is a heterogeneous storage type with value semantics for keyed values in a type-safe fashion. +/// +/// Its values are uniquely identified via `BaggageKey`s (by type identity). These keys also dictate the type of +/// value allowed for a specific key-value pair through their associated type `Value`. +/// +/// ## Defining keys and accessing values +/// Baggage keys are defined as types, most commonly case-less enums (as no actual instances are required) +/// which conform to the `BaggageKey` protocol: +/// +/// private enum TestIDKey: BaggageKey { +/// typealias Value = String +/// } +/// +/// While defining a key, one should also immediately declare an extension on `Baggage` to allow convenient and discoverable ways to interact +/// with the baggage item. The extension should take the form of: +/// +/// extension Baggage { +/// var testID: String? { +/// get { +/// self[TestIDKey.self] +/// } set { +/// self[TestIDKey.self] = newValue +/// } +/// } +/// } +/// +/// For consistency, it is recommended to name key types with the `...Key` suffix (e.g. `SomethingKey`) and the property +/// used to access a value identifier by such key the prefix of the key (e.g. `something`). Please also observe the usual +/// Swift naming conventions, e.g. prefer `ID` to `Id` etc. +/// +/// ## Usage +/// Using a baggage container is fairly straight forward, as it boils down to using the prepared computed properties: +/// +/// var baggage = Baggage.topLevel +/// // set a new value +/// baggage.testID = "abc" +/// // retrieve a stored value +/// let testID = baggage.testID ?? "default" +/// // remove a stored value +/// baggage.testIDKey = nil +/// +/// Note that normally a baggage should not be "created" ad-hoc by user code, but rather it should be passed to it from +/// a runtime. A `Baggage` may already be available to you through Baggage.$current when using structured concurrency. +/// Otherwise, for example when working in an HTTP server framework, it is most likely that the baggage is already passed +/// directly or indirectly (e.g. in a `FrameworkContext`). +/// +/// ### Accessing all values +/// +/// The only way to access "all" values in a baggage is by using the `forEach` function. +/// `Baggage` does not expose more functions on purpose to prevent abuse and treating it as too much of an +/// arbitrary value smuggling container, but only make it convenient for tracing and instrumentation systems which need +/// to access either specific or all items carried inside a baggage. +public struct Baggage { + private var _storage = [AnyBaggageKey: Any]() + + /// Internal on purpose, please use `Baggage.TODO` or `Baggage.topLevel` to create an "empty" baggage, + /// which carries more meaning to other developers why an empty baggage was used. + init() {} +} + +// MARK: - Creating Baggage + +extension Baggage { + /// Creates a new empty "top level" baggage, generally used as an "initial" baggage to immediately be populated with + /// some values by a framework or runtime. Another use case is for tasks starting in the "background" (e.g. on a timer), + /// which don't have a "request context" per se that they can pick up, and as such they have to create a "top level" + /// baggage for their work. + /// + /// ## Usage in frameworks and libraries + /// This function is really only intended to be used by frameworks and libraries, at the "top-level" where a request's, + /// message's or task's processing is initiated. For example, a framework handling requests, should create an empty + /// baggage when handling a request only to immediately populate it with useful trace information extracted from e.g. + /// request headers. + /// + /// ## Usage in applications + /// Application code should never have to create an empty baggage during the processing lifetime of any request, + /// and only should create baggages if some processing is performed in the background - thus the naming of this property. + /// + /// Usually, a framework such as an HTTP server or similar "request handler" would already provide users + /// with a context to be passed along through subsequent calls, either implicitly through the task-local `Baggage.$current` + /// or explicitly as part of some kind of "FrameworkContext". + /// + /// If unsure where to obtain a baggage from, prefer using `.TODO("Not sure where I should get a context from here?")` + /// in order to inform other developers that the lack of baggage passing was not done on purpose, but rather because either + /// not being sure where to obtain a baggage from, or other framework limitations -- e.g. the outer framework not being + /// baggage aware just yet. + public static var topLevel: Baggage { + Baggage() + } +} + +extension Baggage { + /// A baggage intended as a placeholder until a real value can be passed through a function call. + /// + /// It should ONLY be used while prototyping or when the passing of the proper baggage is not yet possible, + /// e.g. because an external library did not pass it correctly and has to be fixed before the proper baggage + /// can be obtained where the TO-DO is currently used. + /// + /// ## Crashing on TO-DO context creation + /// You may set the `BAGGAGE_CRASH_TODOS` variable while compiling a project in order to make calls to this function crash + /// with a fatal error, indicating where a to-do baggage was used. This comes in handy when wanting to ensure that + /// a project never ends up using code which initially was written as "was lazy, did not pass baggage", yet the + /// project requires baggage passing to be done correctly throughout the application. Similar checks can be performed + /// at compile time easily using linters (not yet implemented), since it is always valid enough to detect a to-do context + /// being passed as illegal and warn or error when spotted. + /// + /// ## Example + /// + /// let baggage = Baggage.TODO("The framework XYZ should be modified to pass us a baggage here, and we'd pass it along")) + /// + /// - Parameters: + /// - reason: Informational reason for developers, why a placeholder context was used instead of a proper one, + /// - Returns: Empty "to-do" baggage which should be eventually replaced with a carried through one, or `topLevel`. + public static func TODO( + _ reason: StaticString? = "", + function: String = #function, + file: String = #file, + line: UInt = #line + ) -> Baggage { + var baggage = Baggage() + #if BAGGAGE_CRASH_TODOS + fatalError("BAGGAGE_CRASH_TODOS: at \(file):\(line) (function \(function)), reason: \(reason)") + #else + baggage[TODOKey.self] = .init(file: file, line: line) + return baggage + #endif + } + + private enum TODOKey: BaggageKey { + typealias Value = TODOLocation + static var nameOverride: String? { + return "todo" + } + } +} + +/// Carried automatically by a "to do" baggage. +/// It can be used to track where a baggage originated and which "to do" baggage must be fixed into a real one to avoid this. +public struct TODOLocation { + /// Source file location where the to-do `Baggage` was created + public let file: String + /// Source line location where the to-do `Baggage` was created + public let line: UInt +} + +// MARK: - Interacting with Baggage + +extension Baggage { + /// Provides type-safe access to the baggage's values. + /// This API should ONLY be used inside of accessor implementations. + /// + /// End users should use "accessors" the key's author MUST define rather than using this subscript, following this pattern: + /// + /// internal enum TestID: Baggage.Key { + /// typealias Value = TestID + /// } + /// + /// extension Baggage { + /// public internal(set) var testID: TestID? { + /// get { + /// self[TestIDKey.self] + /// } + /// set { + /// self[TestIDKey.self] = newValue + /// } + /// } + /// } + /// + /// This is in order to enforce a consistent style across projects and also allow for fine grained control over + /// who may set and who may get such property. Just access control to the Key type itself lacks such fidelity. + /// + /// Note that specific baggage and context types MAY (and usually do), offer also a way to set baggage values, + /// however in the most general case it is not required, as some frameworks may only be able to offer reading. + public subscript(_ key: Key.Type) -> Key.Value? { + get { + guard let value = self._storage[AnyBaggageKey(key)] else { return nil } + // safe to force-cast as this subscript is the only way to set a value. + return (value as! Key.Value) + } + set { + self._storage[AnyBaggageKey(key)] = newValue + } + } +} + +extension Baggage { + /// The number of items in the baggage. + public var count: Int { + self._storage.count + } + + /// A Boolean value that indicates whether the baggage is empty. + public var isEmpty: Bool { + self._storage.isEmpty + } + + /// Iterate through all items in this `Baggage` by invoking the given closure for each item. + /// + /// The order of those invocations is NOT guaranteed and should not be relied on. + /// + /// - Parameter body: The closure to be invoked for each item stored in this `Baggage`, + /// passing the type-erased key and the associated value. + public func forEach(_ body: (AnyBaggageKey, Any) throws -> Void) rethrows { + try self._storage.forEach { key, value in + try body(key, value) + } + } +} + +// MARK: - Propagating Baggage + +#if swift(>=5.5) +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +extension Baggage { + /// A `Baggage` automatically propagated through task-local storage. This API enables binding a top-level `Baggage` and passing it + /// implicitly to any child tasks when using structured concurrency. + @TaskLocal public static var current: Baggage? +} +#endif diff --git a/Sources/InstrumentationBaggage/BaggageKey.swift b/Sources/InstrumentationBaggage/BaggageKey.swift new file mode 100644 index 0000000..118dc8d --- /dev/null +++ b/Sources/InstrumentationBaggage/BaggageKey.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Distributed Tracing Baggage +// open source project +// +// Copyright (c) 2020-2021 Apple Inc. and the Swift Distributed Tracing Baggage +// project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Baggage keys provide type-safe access to `Baggage`s by declaring the type of value they "key" at compile-time. +/// To give your `BaggageKey` an explicit name you may override the `nameOverride` property. +/// +/// In general, `BaggageKey`s should be `internal` or `private` to the part of a system using it. +/// +/// All access to baggage items should be performed through an accessor computed property defined as shown below: +/// +/// /// The Key type should be internal (or private). +/// enum TestIDKey: BaggageKey { +/// typealias Value = String +/// static var nameOverride: String? { "test-id" } +/// } +/// +/// extension Baggage { +/// /// This is some useful property documentation. +/// public internal(set) var testID: String? { +/// get { +/// self[TestIDKey.self] +/// } +/// set { +/// self[TestIDKey.self] = newValue +/// } +/// } +/// } +/// +/// This pattern allows library authors fine-grained control over which values may be set, and which only get by end-users. +public protocol BaggageKey { + /// The type of value uniquely identified by this key. + associatedtype Value + + /// The human-readable name of this key. + /// This name will be used instead of the type name when a value is printed. + /// + /// It MAY also be picked up by an instrument (from Swift Tracing) which serializes baggage items and e.g. used as + /// header name for carried metadata. Though generally speaking header names are NOT required to use the nameOverride, + /// and MAY use their well known names for header names etc, as it depends on the specific transport and instrument used. + /// + /// For example, a baggage key representing the W3C "trace-state" header may want to return "trace-state" here, + /// in order to achieve a consistent look and feel of this baggage item throughout logging and tracing systems. + /// + /// Defaults to `nil`. + static var nameOverride: String? { get } +} + +extension BaggageKey { + public static var nameOverride: String? { nil } +} + +/// A type-erased `BaggageKey` used when iterating through the `Baggage` using its `forEach` method. +public struct AnyBaggageKey { + /// The key's type represented erased to an `Any.Type`. + public let keyType: Any.Type + + private let _nameOverride: String? + + /// A human-readable String representation of the underlying key. + /// If no explicit name has been set on the wrapped key the type name is used. + public var name: String { + self._nameOverride ?? String(describing: self.keyType.self) + } + + init(_ keyType: Key.Type) { + self.keyType = keyType + self._nameOverride = keyType.nameOverride + } +} + +extension AnyBaggageKey: Hashable { + public static func == (lhs: AnyBaggageKey, rhs: AnyBaggageKey) -> Bool { + return ObjectIdentifier(lhs.keyType) == ObjectIdentifier(rhs.keyType) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self.keyType)) + } +} diff --git a/Tests/BaggageTests/BaggageContextTests+XCTest.swift b/Tests/BaggageTests/BaggageContextTests+XCTest.swift deleted file mode 100644 index fd8d7f6..0000000 --- a/Tests/BaggageTests/BaggageContextTests+XCTest.swift +++ /dev/null @@ -1,35 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -// -// BaggageContextTests+XCTest.swift -// -import XCTest -/// -/// NOTE: This file was generated by generate_linux_tests.rb -/// -/// Do NOT edit this file directly as it will be regenerated automatically when needed. -/// - -extension LoggingContextTests { - - @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") - static var allTests : [(String, (LoggingContextTests) -> () throws -> Void)] { - return [ - ("test_ExampleFrameworkContext_dumpBaggage", test_ExampleFrameworkContext_dumpBaggage), - ("test_ExampleFrameworkContext_log_withBaggage", test_ExampleFrameworkContext_log_withBaggage), - ("test_DefaultLoggingContext_log_withBaggage", test_DefaultLoggingContext_log_withBaggage), - ("test_ExampleFrameworkContext_log_prefersLoggingContextOverExistingLoggerMetadata", test_ExampleFrameworkContext_log_prefersLoggingContextOverExistingLoggerMetadata), - ] - } -} - diff --git a/Tests/BaggageTests/BaggageContextTests.swift b/Tests/BaggageTests/BaggageContextTests.swift deleted file mode 100644 index 0cabc9a..0000000 --- a/Tests/BaggageTests/BaggageContextTests.swift +++ /dev/null @@ -1,212 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Baggage -import Logging -import XCTest - -final class LoggingContextTests: XCTestCase { - func test_ExampleFrameworkContext_dumpBaggage() throws { - var baggage = Baggage.topLevel - let logger = Logger(label: "TheLogger") - - baggage.testID = 42 - let context = ExampleFrameworkContext(context: baggage, logger: logger) - - func frameworkFunctionDumpsBaggage(param: String, context: LoggingContext) -> String { - var s = "" - context.baggage.forEach { key, item in - s += "\(key.name): \(item)\n" - } - return s - } - - let result = frameworkFunctionDumpsBaggage(param: "x", context: context) - XCTAssertEqual( - result, - """ - TestIDKey: 42 - - """ - ) - } - - func test_ExampleFrameworkContext_log_withBaggage() throws { - let baggage = Baggage.topLevel - let logging = TestLogging() - let logger = Logger(label: "TheLogger", factory: { label in logging.make(label: label) }) - - var context = ExampleFrameworkContext(context: baggage, logger: logger) - - context.baggage.secondTestID = "value" - context.baggage.testID = 42 - context.logger.info("Hello") - - context.baggage.testID = nil - context.logger.warning("World") - - context.baggage.secondTestID = nil - context.logger[metadataKey: "metadata"] = "on-logger" - context.logger.warning("!") - - // These implicitly exercise logger.updateMetadata - - logging.history.assertExist(level: .info, message: "Hello", metadata: [ - "TestIDKey": .stringConvertible(42), - "secondIDExplicitlyNamed": "value", - ]) - logging.history.assertExist(level: .warning, message: "World", metadata: [ - "secondIDExplicitlyNamed": "value", - ]) - logging.history.assertExist(level: .warning, message: "!", metadata: [ - "metadata": "on-logger", - ]) - } - - func test_DefaultLoggingContext_log_withBaggage() throws { - let logging = TestLogging() - let logger = Logger(label: "TheLogger", factory: { label in logging.make(label: label) }) - - var context = DefaultLoggingContext.topLevel(logger: logger) - - context.baggage.secondTestID = "value" - context.baggage.testID = 42 - context.logger.info("Hello") - - context.baggage.testID = nil - context.logger.warning("World") - - context.baggage.secondTestID = nil - context.logger[metadataKey: "metadata"] = "on-logger" - context.logger.warning("!") - - // These implicitly exercise logger.updateMetadata - - logging.history.assertExist(level: .info, message: "Hello", metadata: [ - "TestIDKey": .stringConvertible(42), - "secondIDExplicitlyNamed": "value", - ]) - logging.history.assertExist(level: .warning, message: "World", metadata: [ - "secondIDExplicitlyNamed": "value", - ]) - logging.history.assertExist(level: .warning, message: "!", metadata: [ - "metadata": "on-logger", - ]) - } - - func test_ExampleFrameworkContext_log_prefersLoggingContextOverExistingLoggerMetadata() { - let baggage = Baggage.topLevel - let logging = TestLogging() - var logger = Logger(label: "TheLogger", factory: { label in logging.make(label: label) }) - logger[metadataKey: "secondIDExplicitlyNamed"] = "set on logger" - - var context = ExampleFrameworkContext(context: baggage, logger: logger) - - context.baggage.secondTestID = "set on baggage" - - context.logger.info("Hello") - - logging.history.assertExist(level: .info, message: "Hello", metadata: [ - "secondIDExplicitlyNamed": "set on baggage", - ]) - } -} - -struct ExampleFrameworkContext: LoggingContext { - var baggage: Baggage { - willSet { - self._logger.updateMetadata(previous: self.baggage, latest: newValue) - } - } - - var _logger: Logger - var logger: Logger { - get { - return self._logger - } - set { - self._logger = newValue - self._logger.updateMetadata(previous: self.baggage, latest: self.baggage) - } - } - - init(context baggage: Baggage, logger: Logger) { - self.baggage = baggage - self._logger = logger - self._logger.updateMetadata(previous: .topLevel, latest: baggage) - } -} - -struct CoolFrameworkContext: LoggingContext { - var baggage: Baggage { - willSet { - self.logger.updateMetadata(previous: self.baggage, latest: newValue) - } - } - - var logger: Logger { - didSet { - self.logger.updateMetadata(previous: self.baggage, latest: self.baggage) - } - } - - // framework context defines other values as well - let frameworkField: String - - // including the popular eventLoop - let eventLoop: FakeEventLoop - - init() { - self.baggage = .topLevel - self.logger = Logger(label: "some-framework-logger") - self.eventLoop = FakeEventLoop() - self.frameworkField = "" - self.logger.updateMetadata(previous: .topLevel, latest: self.baggage) - } - - func forEachBaggageItem(_ body: (AnyBaggageKey, Any) throws -> Void) rethrows { - return try self.baggage.forEach(body) - } -} - -struct FakeEventLoop {} - -private extension Baggage { - var testID: Int? { - get { - return self[TestIDKey.self] - } - set { - self[TestIDKey.self] = newValue - } - } - - var secondTestID: String? { - get { - return self[SecondTestIDKey.self] - } - set { - self[SecondTestIDKey.self] = newValue - } - } -} - -private enum TestIDKey: Baggage.Key { - typealias Value = Int -} - -private enum SecondTestIDKey: Baggage.Key { - typealias Value = String - - static let nameOverride: String? = "secondIDExplicitlyNamed" -} diff --git a/Tests/BaggageTests/FrameworkContextTests+XCTest.swift b/Tests/BaggageTests/FrameworkContextTests+XCTest.swift deleted file mode 100644 index 13b7eb8..0000000 --- a/Tests/BaggageTests/FrameworkContextTests+XCTest.swift +++ /dev/null @@ -1,33 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -// -// FrameworkContextTests+XCTest.swift -// -import XCTest -/// -/// NOTE: This file was generated by generate_linux_tests.rb -/// -/// Do NOT edit this file directly as it will be regenerated automatically when needed. -/// - -extension FrameworkLoggingContextTests { - - @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") - static var allTests : [(String, (FrameworkLoggingContextTests) -> () throws -> Void)] { - return [ - ("testLoggingContextSubscript", testLoggingContextSubscript), - ("testLoggingContextForEach", testLoggingContextForEach), - ] - } -} - diff --git a/Tests/BaggageTests/FrameworkContextTests.swift b/Tests/BaggageTests/FrameworkContextTests.swift deleted file mode 100644 index 6f6f93f..0000000 --- a/Tests/BaggageTests/FrameworkContextTests.swift +++ /dev/null @@ -1,88 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Baggage -@testable import CoreBaggage -import Logging -import XCTest - -final class FrameworkLoggingContextTests: XCTestCase { - func testLoggingContextSubscript() { - var context = TestFrameworkContext() - - // mutate baggage context directly - context.baggage[OtherKey.self] = "test" - XCTAssertEqual(context.baggage.otherKey, "test") - } - - func testLoggingContextForEach() { - var contents = [AnyBaggageKey: Any]() - var context = TestFrameworkContext() - - context.baggage.testKey = 42 - context.baggage.otherKey = "test" - - context.baggage.forEach { key, value in - contents[key] = value - } - - XCTAssertNotNil(contents[AnyBaggageKey(TestKey.self)]) - XCTAssertEqual(contents[AnyBaggageKey(TestKey.self)] as? Int, 42) - XCTAssertNotNil(contents[AnyBaggageKey(OtherKey.self)]) - XCTAssertEqual(contents[AnyBaggageKey(OtherKey.self)] as? String, "test") - } -} - -private struct TestFrameworkContext: LoggingContext { - var baggage = Baggage.topLevel - - private var _logger = Logger(label: "test") - var logger: Logger { - get { - return self._logger.with(self.baggage) - } - set { - self._logger = newValue - } - } -} - -private enum TestKey: Baggage.Key { - typealias Value = Int -} - -extension Baggage { - var testKey: Int? { - get { - return self[TestKey.self] - } - set { - self[TestKey.self] = newValue - } - } -} - -private enum OtherKey: Baggage.Key { - typealias Value = String -} - -extension Baggage { - var otherKey: String? { - get { - return self[OtherKey.self] - } - set { - self[OtherKey.self] = newValue - } - } -} diff --git a/Tests/BaggageTests/TestLogger.swift b/Tests/BaggageTests/TestLogger.swift deleted file mode 100644 index 13ea15f..0000000 --- a/Tests/BaggageTests/TestLogger.swift +++ /dev/null @@ -1,333 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Distributed Tracing Baggage open source project -// -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Logging API open source project -// -// Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift Logging API project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation -@testable import Logging -import XCTest - -/// Copy of swift-log's TestLogging -internal struct TestLogging { - private let _config = Config() // shared among loggers - private let recorder = Recorder() // shared among loggers - - func make(label: String) -> LogHandler { - return TestLogHandler(label: label, config: self.config, recorder: self.recorder) - } - - var config: Config { return self._config } - var history: History { return self.recorder } -} - -internal struct TestLogHandler: LogHandler { - private let logLevelLock = NSLock() - private let metadataLock = NSLock() - private let recorder: Recorder - private let config: Config - private var logger: Logger // the actual logger - - let label: String - init(label: String, config: Config, recorder: Recorder) { - self.label = label - self.config = config - self.recorder = recorder - self.logger = Logger(label: "test", StreamLogHandler.standardOutput(label: label)) - self.logger.logLevel = .debug - } - - func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) { - let metadata = (self._metadataSet ? self.metadata : MDC.global.metadata).merging(metadata ?? [:], uniquingKeysWith: { _, new in new }) - self.logger.log(level: level, message, metadata: metadata, source: source, file: file, function: function, line: line) - self.recorder.record(level: level, metadata: metadata, message: message, source: source) - } - - private var _logLevel: Logger.Level? - var logLevel: Logger.Level { - get { - // get from config unless set - return self.logLevelLock.withLock { self._logLevel } ?? self.config.get(key: self.label) - } - set { - self.logLevelLock.withLock { self._logLevel = newValue } - } - } - - private var _metadataSet = false - private var _metadata = Logger.Metadata() { - didSet { - self._metadataSet = true - } - } - - public var metadata: Logger.Metadata { - get { - return self.metadataLock.withLock { self._metadata } - } - set { - self.metadataLock.withLock { self._metadata = newValue } - } - } - - // TODO: would be nice to delegate to local copy of logger but StdoutLogger is a reference type. why? - subscript(metadataKey metadataKey: Logger.Metadata.Key) -> Logger.Metadata.Value? { - get { - return self.metadataLock.withLock { self._metadata[metadataKey] } - } - set { - self.metadataLock.withLock { - self._metadata[metadataKey] = newValue - } - } - } -} - -internal class Config { - private static let ALL = "*" - - private let lock = NSLock() - private var storage = [String: Logger.Level]() - - func get(key: String) -> Logger.Level { - return self.get(key) ?? self.get(Config.ALL) ?? Logger.Level.debug - } - - func get(_ key: String) -> Logger.Level? { - guard let value = (self.lock.withLock { self.storage[key] }) else { - return nil - } - return value - } - - func set(key: String = Config.ALL, value: Logger.Level) { - self.lock.withLock { self.storage[key] = value } - } - - func clear() { - self.lock.withLock { self.storage.removeAll() } - } -} - -internal class Recorder: History { - private let lock = NSLock() - private var _entries = [LogEntry]() - - func record(level: Logger.Level, metadata: Logger.Metadata?, message: Logger.Message, source: String) { - self.lock.withLock { - self._entries.append(LogEntry(level: level, metadata: metadata, message: message.description, source: source)) - } - } - - var entries: [LogEntry] { - return self.lock.withLock { return self._entries } - } -} - -internal protocol History { - var entries: [LogEntry] { get } -} - -internal extension History { - func atLevel(level: Logger.Level) -> [LogEntry] { - return self.entries.filter { entry in - level == entry.level - } - } - - var trace: [LogEntry] { - return self.atLevel(level: .debug) - } - - var debug: [LogEntry] { - return self.atLevel(level: .debug) - } - - var info: [LogEntry] { - return self.atLevel(level: .info) - } - - var warning: [LogEntry] { - return self.atLevel(level: .warning) - } - - var error: [LogEntry] { - return self.atLevel(level: .error) - } -} - -internal struct LogEntry { - let level: Logger.Level - let metadata: Logger.Metadata? - let message: String - let source: String -} - -extension History { - func assertExist(level: Logger.Level, - message: String, - metadata: Logger.Metadata? = nil, - source: String? = nil, - file: StaticString = #file, - line: UInt = #line) { - let source = source ?? Logger.currentModule(filePath: "\(file)") - let entry = self.find(level: level, message: message, metadata: metadata, source: source) - XCTAssertNotNil( - entry, - """ - entry not found: \(level), \(source), \(String(describing: metadata)), \(message) - All entries: - \(self.entries.map { "\($0)" }.joined(separator: "\n")) - """, - file: file, - line: line - ) - } - - func assertNotExist(level: Logger.Level, - message: String, - metadata: Logger.Metadata? = nil, - source: String? = nil, - file: StaticString = #file, - line: UInt = #line) { - let source = source ?? Logger.currentModule(filePath: "\(file)") - let entry = self.find(level: level, message: message, metadata: metadata, source: source) - XCTAssertNil( - entry, - "entry was found: \(level), \(source), \(String(describing: metadata)), \(message)", - file: file, - line: line - ) - } - - func find(level: Logger.Level, message: String, metadata: Logger.Metadata? = nil, source: String) -> LogEntry? { - return self.entries.first { entry in - entry.level == level && - entry.message == message && - entry.metadata ?? [:] == metadata ?? [:] && - entry.source == source - } - } -} - -public class MDC { - private let lock = NSLock() - private var storage = [Int: Logger.Metadata]() - - public static var global = MDC() - - private init() {} - - public subscript(metadataKey: String) -> Logger.Metadata.Value? { - get { - return self.lock.withLock { - return self.storage[self.threadId]?[metadataKey] - } - } - set { - self.lock.withLock { - if self.storage[self.threadId] == nil { - self.storage[self.threadId] = Logger.Metadata() - } - self.storage[self.threadId]![metadataKey] = newValue - } - } - } - - public var metadata: Logger.Metadata { - return self.lock.withLock { - return self.storage[self.threadId] ?? [:] - } - } - - public func clear() { - self.lock.withLock { - _ = self.storage.removeValue(forKey: self.threadId) - } - } - - public func with(metadata: Logger.Metadata, _ body: () throws -> Void) rethrows { - metadata.forEach { self[$0] = $1 } - defer { - metadata.keys.forEach { self[$0] = nil } - } - try body() - } - - public func with(metadata: Logger.Metadata, _ body: () throws -> T) rethrows -> T { - metadata.forEach { self[$0] = $1 } - defer { - metadata.keys.forEach { self[$0] = nil } - } - return try body() - } - - // for testing - internal func flush() { - self.lock.withLock { - self.storage.removeAll() - } - } - - private var threadId: Int { - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) - return Int(pthread_mach_thread_np(pthread_self())) - #else - return Int(pthread_self()) - #endif - } -} - -internal extension NSLock { - func withLock(_ body: () -> T) -> T { - self.lock() - defer { - self.unlock() - } - return body() - } -} - -internal struct TestLibrary { - private let logger = Logger(label: "TestLibrary") - private let queue = DispatchQueue(label: "TestLibrary") - - public init() {} - - public func doSomething() { - self.logger.info("TestLibrary::doSomething") - } - - public func doSomethingAsync(completion: @escaping () -> Void) { - // libraries that use global loggers and async, need to make sure they propagate the - // logging metadata when creating a new thread - let metadata = MDC.global.metadata - self.queue.asyncAfter(deadline: .now() + 0.1) { - MDC.global.with(metadata: metadata) { - self.logger.info("TestLibrary::doSomethingAsync") - completion() - } - } - } -} diff --git a/Tests/InstrumentationBaggageTests/BaggageTests+XCTest.swift b/Tests/InstrumentationBaggageTests/BaggageTests+XCTest.swift new file mode 100644 index 0000000..5d41e4d --- /dev/null +++ b/Tests/InstrumentationBaggageTests/BaggageTests+XCTest.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Distributed Tracing Baggage +// open source project +// +// Copyright (c) 2020-2021 Apple Inc. and the Swift Distributed Tracing Baggage +// project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// +// BaggageTests+XCTest.swift +// +import XCTest +/// +/// NOTE: This file was generated by generate_linux_tests.rb +/// +/// Do NOT edit this file directly as it will be regenerated automatically when needed. +/// + +extension BaggageTests { + + @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") + static var allTests : [(String, (BaggageTests) -> () throws -> Void)] { + return [ + ("test_topLevelBaggageIsEmpty", test_topLevelBaggageIsEmpty), + ("test_readAndWriteThroughSubscript", test_readAndWriteThroughSubscript), + ("test_forEachIteratesOverAllBaggageItems", test_forEachIteratesOverAllBaggageItems), + ("test_TODO_doesNotCrashWithoutExplicitCompilerFlag", test_TODO_doesNotCrashWithoutExplicitCompilerFlag), + ("test_automaticPropagationThroughTaskLocal", test_automaticPropagationThroughTaskLocal), + ] + } +} + diff --git a/Tests/InstrumentationBaggageTests/BaggageTests.swift b/Tests/InstrumentationBaggageTests/BaggageTests.swift new file mode 100644 index 0000000..ef8ce31 --- /dev/null +++ b/Tests/InstrumentationBaggageTests/BaggageTests.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Distributed Tracing Baggage +// open source project +// +// Copyright (c) 2020-2021 Apple Inc. and the Swift Distributed Tracing Baggage +// project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import InstrumentationBaggage +import XCTest + +final class BaggageTests: XCTestCase { + func test_topLevelBaggageIsEmpty() { + let baggage = Baggage.topLevel + + XCTAssertTrue(baggage.isEmpty) + XCTAssertEqual(baggage.count, 0) + } + + func test_readAndWriteThroughSubscript() throws { + var baggage = Baggage.topLevel + XCTAssertNil(baggage[FirstTestKey.self]) + XCTAssertNil(baggage[SecondTestKey.self]) + + baggage[FirstTestKey.self] = 42 + baggage[SecondTestKey.self] = 42.0 + + XCTAssertFalse(baggage.isEmpty) + XCTAssertEqual(baggage.count, 2) + XCTAssertEqual(baggage[FirstTestKey.self], 42) + XCTAssertEqual(baggage[SecondTestKey.self], 42.0) + } + + func test_forEachIteratesOverAllBaggageItems() { + var baggage = Baggage.topLevel + + baggage[FirstTestKey.self] = 42 + baggage[SecondTestKey.self] = 42.0 + baggage[ThirdTestKey.self] = "test" + + var baggageItems = [AnyBaggageKey: Any]() + baggage.forEach { key, value in + baggageItems[key] = value + } + XCTAssertEqual(baggageItems.count, 3) + XCTAssertTrue(baggageItems.contains(where: { $0.key.name == "FirstTestKey" })) + XCTAssertTrue(baggageItems.contains(where: { $0.value as? Int == 42 })) + XCTAssertTrue(baggageItems.contains(where: { $0.key.name == "SecondTestKey" })) + XCTAssertTrue(baggageItems.contains(where: { $0.value as? Double == 42.0 })) + XCTAssertTrue(baggageItems.contains(where: { $0.key.name == "explicit" })) + XCTAssertTrue(baggageItems.contains(where: { $0.value as? String == "test" })) + } + + func test_TODO_doesNotCrashWithoutExplicitCompilerFlag() { + _ = Baggage.TODO(#function) + } + + func test_automaticPropagationThroughTaskLocal() throws { + #if swift(>=5.5) + guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { + throw XCTSkip("Task locals are not supported on this platform.") + } + + XCTAssertNil(Baggage.current) + + var baggage = Baggage.topLevel + baggage[FirstTestKey.self] = 42 + + var propagatedBaggage: Baggage? + func exampleFunction() { + propagatedBaggage = Baggage.current + } + + Baggage.$current.withValue(baggage, operation: exampleFunction) + + XCTAssertEqual(propagatedBaggage?.count, 1) + XCTAssertEqual(propagatedBaggage?[FirstTestKey.self], 42) + #endif + } + + private enum FirstTestKey: BaggageKey { + typealias Value = Int + } + + private enum SecondTestKey: BaggageKey { + typealias Value = Double + } + + private enum ThirdTestKey: BaggageKey { + typealias Value = String + + static let nameOverride: String? = "explicit" + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 1485b7b..a593eb3 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,8 +1,10 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the Swift Distributed Tracing Baggage open source project +// This source file is part of the Swift Distributed Tracing Baggage +// open source project // -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors +// Copyright (c) 2020-2021 Apple Inc. and the Swift Distributed Tracing Baggage +// project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -21,7 +23,7 @@ import XCTest /// #if os(Linux) || os(FreeBSD) - @testable import BaggageTests + @testable import InstrumentationBaggageTests // This protocol is necessary to we can call the 'run' method (on an existential of this protocol) // without the compiler noticing that we're calling a deprecated function. @@ -32,8 +34,7 @@ class LinuxMainRunnerImpl: LinuxMainRunner { @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") func run() { XCTMain([ - testCase(FrameworkLoggingContextTests.allTests), - testCase(LoggingContextTests.allTests), + testCase(BaggageTests.allTests), ]) } } diff --git a/docker/Dockerfile b/docker/Dockerfile index 3f2a2e7..2c26e42 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG swift_version=5.0 +ARG swift_version=5.2 ARG ubuntu_version=bionic ARG base_image=swift:$swift_version-$ubuntu_version FROM $base_image diff --git a/docker/docker-compose.1804.50.yaml b/docker/docker-compose.1804.50.yaml deleted file mode 100644 index db93fa2..0000000 --- a/docker/docker-compose.1804.50.yaml +++ /dev/null @@ -1,16 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: swift-distributed-tracing-baggage:18.04-5.0 - build: - args: - ubuntu_version: "bionic" - swift_version: "5.0" - - test: - image: swift-distributed-tracing-baggage:18.04-5.0 - - shell: - image: swift-distributed-tracing-baggage:18.04-5.0 diff --git a/docker/docker-compose.1804.51.yaml b/docker/docker-compose.1804.51.yaml deleted file mode 100644 index ccd1206..0000000 --- a/docker/docker-compose.1804.51.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: swift-distributed-tracing-baggage:18.04-5.1 - build: - args: - ubuntu_version: "bionic" - swift_version: "5.1" - - test: - image: swift-distributed-tracing-baggage:18.04-5.1 - environment: [] - #- SANITIZER_ARG=--sanitize=thread - - shell: - image: swift-distributed-tracing-baggage:18.04-5.1 diff --git a/docker/docker-compose.1804.52.yaml b/docker/docker-compose.1804.52.yaml index edb7a4d..a2b9eb2 100644 --- a/docker/docker-compose.1804.52.yaml +++ b/docker/docker-compose.1804.52.yaml @@ -11,8 +11,6 @@ services: test: image: swift-distributed-tracing-baggage:18.04-5.2 - environment: [] - #- SANITIZER_ARG=--sanitize=thread shell: image: swift-distributed-tracing-baggage:18.04-5.2 diff --git a/docker/docker-compose.1804.53.yaml b/docker/docker-compose.1804.53.yaml index 4fcc0d7..0cbed42 100644 --- a/docker/docker-compose.1804.53.yaml +++ b/docker/docker-compose.1804.53.yaml @@ -6,12 +6,11 @@ services: image: swift-distributed-tracing-baggage:18.04-5.3 build: args: - base_image: "swiftlang/swift:nightly-5.3-bionic" + ubuntu_version: "bionic" + swift_version: "5.3" test: image: swift-distributed-tracing-baggage:18.04-5.3 - environment: [] - #- SANITIZER_ARG=--sanitize=thread shell: image: swift-distributed-tracing-baggage:18.04-5.3 diff --git a/docker/docker-compose.1804.54.yaml b/docker/docker-compose.1804.54.yaml new file mode 100644 index 0000000..bbb9922 --- /dev/null +++ b/docker/docker-compose.1804.54.yaml @@ -0,0 +1,16 @@ +version: "3" + +services: + + runtime-setup: + image: swift-distributed-tracing-baggage:18.04-5.4 + build: + args: + ubuntu_version: "bionic" + swift_version: "5.4" + + test: + image: swift-distributed-tracing-baggage:18.04-5.4 + + shell: + image: swift-distributed-tracing-baggage:18.04-5.4 diff --git a/docker/docker-compose.1804.55.yaml b/docker/docker-compose.1804.55.yaml new file mode 100644 index 0000000..a718d20 --- /dev/null +++ b/docker/docker-compose.1804.55.yaml @@ -0,0 +1,15 @@ +version: "3" + +services: + + runtime-setup: + image: swift-distributed-tracing-baggage:18.04-5.5 + build: + args: + base_image: "swiftlang/swift:nightly-5.5-bionic" + + test: + image: swift-distributed-tracing-baggage:18.04-5.5 + + shell: + image: swift-distributed-tracing-baggage:18.04-5.5 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 43c95a2..e5cdc50 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -34,15 +34,6 @@ services: test: <<: *common - security_opt: # FIXME: temp while private - - label=type:container_runtime_t - volumes: # FIXME: temp while private - - ..:/code:z - - ~/.ssh:/root/.ssh - - $SSH_AUTH_SOCK:/tmp/ssh-agent:z - environment: # FIXME: temp while private - - SSH_AUTH_SOCK=/tmp/ssh-agent - - GIT_SSH_COMMAND=ssh -vv -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no command: /bin/bash -xcl "swift test -Xswiftc -warnings-as-errors $${SANITIZER_ARG-}" # util diff --git a/scripts/generate_docs.sh b/scripts/generate_docs.sh index 56ade5b..f25267e 100755 --- a/scripts/generate_docs.sh +++ b/scripts/generate_docs.sh @@ -1,9 +1,11 @@ #!/bin/bash ##===----------------------------------------------------------------------===## ## -## This source file is part of the Swift Distributed Tracing Baggage open source project +## This source file is part of the Swift Distributed Tracing Baggage +## open source project ## -## Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors +## Copyright (c) 2020-2021 Apple Inc. and the Swift Distributed Tracing Baggage +## project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information @@ -17,8 +19,8 @@ set -e my_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" declare -r root_path="$my_path/.." version=$(git describe --abbrev=0 --tags || echo "0.0.0") -declare -r modules=(Baggage) -main_module="Baggage" +declare -r modules=(InstrumentationBaggage) +main_module="InstrumentationBaggage" declare -r project_xcodeproj_name="swift-distributed-tracing-baggage" declare -r project_gh_name="swift-distributed-tracing-baggage" @@ -69,7 +71,6 @@ jazzy_args=(--clean --xcodebuild-arguments -scheme,$project_xcodeproj_name-Package) cat > "$module_switcher" <<"EOF" # Swift Distributed Tracing Baggage Docs - This project contains modules: EOF diff --git a/scripts/generate_linux_tests.rb b/scripts/generate_linux_tests.rb index b8b6f10..f1ce7e8 100755 --- a/scripts/generate_linux_tests.rb +++ b/scripts/generate_linux_tests.rb @@ -34,9 +34,11 @@ def header(fileName) string = <<-eos //===----------------------------------------------------------------------===// // -// This source file is part of the Swift Distributed Tracing Baggage open source project +// This source file is part of the Swift Distributed Tracing Baggage +// open source project // -// Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors +// Copyright (c) 2020-2021 Apple Inc. and the Swift Distributed Tracing Baggage +// project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/scripts/soundness.sh b/scripts/soundness.sh index f615b19..63a7c49 100755 --- a/scripts/soundness.sh +++ b/scripts/soundness.sh @@ -1,9 +1,11 @@ #!/bin/bash ##===----------------------------------------------------------------------===## ## -## This source file is part of the Swift Distributed Tracing Baggage open source project +## This source file is part of the Swift Distributed Tracing Baggage +## open source project ## -## Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors +## Copyright (c) 2020-2021 Apple Inc. and the Swift Distributed Tracing Baggage +## project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information @@ -15,19 +17,6 @@ set -eu here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -printf "=> Checking linux tests... " -FIRST_OUT="$(git status --porcelain)" -ruby "$here/../scripts/generate_linux_tests.rb" > /dev/null -SECOND_OUT="$(git status --porcelain)" -if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then - printf "\033[0;31mmissing changes!\033[0m\n" - git --no-pager diff - exit 1 -else - printf "\033[0;32mokay.\033[0m\n" -fi - bash $here/validate_license_headers.sh bash $here/validate_language.sh bash $here/validate_format.sh -bash $here/validate_naming.sh diff --git a/scripts/validate_format.sh b/scripts/validate_format.sh index 20d7f73..9c28050 100755 --- a/scripts/validate_format.sh +++ b/scripts/validate_format.sh @@ -1,9 +1,11 @@ #!/bin/bash ##===----------------------------------------------------------------------===## ## -## This source file is part of the Swift Distributed Tracing Baggage open source project +## This source file is part of the Swift Distributed Tracing Baggage +## open source project ## -## Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors +## Copyright (c) 2020-2021 Apple Inc. and the Swift Distributed Tracing Baggage +## project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information diff --git a/scripts/validate_language.sh b/scripts/validate_language.sh index 75d7e84..22741f4 100755 --- a/scripts/validate_language.sh +++ b/scripts/validate_language.sh @@ -1,9 +1,11 @@ #!/bin/bash ##===----------------------------------------------------------------------===## ## -## This source file is part of the Swift Distributed Tracing Baggage open source project +## This source file is part of the Swift Distributed Tracing Baggage +## open source project ## -## Copyright (c) 2021 Apple Inc. and the Swift Distributed Tracing Baggage project authors +## Copyright (c) 2020-2021 Apple Inc. and the Swift Distributed Tracing Baggage +## project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information diff --git a/scripts/validate_license_headers.sh b/scripts/validate_license_headers.sh index 4bece55..8aa4ae4 100755 --- a/scripts/validate_license_headers.sh +++ b/scripts/validate_license_headers.sh @@ -1,9 +1,11 @@ #!/bin/bash ##===----------------------------------------------------------------------===## ## -## This source file is part of the Swift Distributed Tracing Baggage open source project +## This source file is part of the Swift Distributed Tracing Baggage +## open source project ## -## Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors +## Copyright (c) 2020-2021 Apple Inc. and the Swift Distributed Tracing Baggage +## project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information @@ -31,11 +33,11 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" function replace_acceptable_years() { # this needs to replace all acceptable forms with 'YEARS' - sed -e 's/2019-2020/YEARS/' -e 's/2019-2021/YEARS/' -e 's/2020/YEARS/' -e 's/2021/YEARS/' + sed -e 's/2020-2021/YEARS/' -e 's/2020/YEARS/' -e 's/2021/YEARS/' } printf "=> Checking license headers\n" -tmp=$(mktemp /tmp/.swift-baggage-context-soundness_XXXXXX) +tmp=$(mktemp /tmp/.swift-distributed-tracing-baggage-soundness_XXXXXX) for language in swift-or-c bash dtrace; do printf " * $language... " @@ -45,14 +47,16 @@ for language in swift-or-c bash dtrace; do matching_files=( -name '*' ) case "$language" in swift-or-c) - exceptions=( -name c_nio_http_parser.c -o -name c_nio_http_parser.h -o -name cpp_magic.h -o -name Package.swift -o -name CNIOSHA1.h -o -name c_nio_sha1.c -o -name ifaddrs-android.c -o -name ifaddrs-android.h) + exceptions=( -name Package.swift) matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' ) cat > "$tmp" <<"EOF" //===----------------------------------------------------------------------===// // -// This source file is part of the Swift Distributed Tracing Baggage open source project +// This source file is part of the Swift Distributed Tracing Baggage +// open source project // -// Copyright (c) YEARS Apple Inc. and the Swift Distributed Tracing Baggage project authors +// Copyright (c) YEARS Apple Inc. and the Swift Distributed Tracing Baggage +// project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -68,9 +72,11 @@ EOF #!/bin/bash ##===----------------------------------------------------------------------===## ## -## This source file is part of the Swift Distributed Tracing Baggage open source project +## This source file is part of the Swift Distributed Tracing Baggage +## open source project ## -## Copyright (c) YEARS Apple Inc. and the Swift Distributed Tracing Baggage project authors +## Copyright (c) YEARS Apple Inc. and the Swift Distributed Tracing Baggage +## project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information @@ -86,9 +92,11 @@ EOF #!/usr/sbin/dtrace -q -s /*===----------------------------------------------------------------------===* * - * This source file is part of the Swift Distributed Tracing Baggage open source project + * This source file is part of the Swift Distributed Tracing Baggage + * open source project * - * Copyright (c) YEARS Apple Inc. and the Swift Distributed Tracing Baggage project authors + * Copyright (c) YEARS Apple Inc. and the Swift Distributed Tracing Baggage + * project authors * Licensed under Apache License v2.0 * * See LICENSE.txt for license information diff --git a/scripts/validate_naming.sh b/scripts/validate_naming.sh deleted file mode 100755 index 19c4c3d..0000000 --- a/scripts/validate_naming.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the Swift Distributed Tracing Baggage open source project -## -## Copyright (c) 2020 Apple Inc. and the Swift Distributed Tracing Baggage project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftNIO open source project -## -## Copyright (c) 2017-2019 Apple Inc. and the SwiftNIO project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftNIO project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -printf "=> Checking for unacceptable language... " -# This greps for unacceptable terminology. The square bracket[s] are so that -# "git grep" doesn't find the lines that greps :). -unacceptable_terms=( - -e blacklis[t] - -e whitelis[t] - -e slav[e] -) -if git grep --color=never -i "${unacceptable_terms[@]}" > /dev/null; then - printf "\033[0;31mUnacceptable language found.\033[0m\n" - git grep -i "${unacceptable_terms[@]}" - exit 1 -fi -printf "\033[0;32mokay.\033[0m\n"