| // This source file is part of the Swift.org open source project |
| // |
| // Copyright (c) 2016 Apple Inc. and the Swift project authors |
| // Licensed under Apache License v2.0 with Runtime Library Exception |
| // |
| // See http://swift.org/LICENSE.txt for license information |
| // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| // |
| // |
| // PerformanceMeter.swift |
| // Measures the performance of a block of code and reports the results. |
| // |
| |
| /// Describes a type that is capable of measuring some aspect of code performance |
| /// over time. |
| internal protocol PerformanceMetric { |
| /// Called once per iteration immediately before the tested code is executed. |
| /// The metric should do whatever work is required to begin a new measurement. |
| func startMeasuring() |
| |
| /// Called once per iteration immediately after the tested code is executed. |
| /// The metric should do whatever work is required to finalize measurement. |
| func stopMeasuring() |
| |
| /// Called once, after all measurements have been taken, to provide feedback |
| /// about the collected measurements. |
| /// - Returns: Measurement results to present to the user. |
| func calculateResults() -> String |
| |
| /// Called once, after all measurements have been taken, to determine whether |
| /// the measurements should be treated as a test failure or not. |
| /// - Returns: A diagnostic message if the results indicate failure, else nil. |
| func failureMessage() -> String? |
| } |
| |
| /// Protocol used by `PerformanceMeter` to report measurement results |
| internal protocol PerformanceMeterDelegate { |
| /// Reports a string representation of the gathered performance metrics |
| /// - Parameter results: The raw measured values, and some derived data such |
| /// as average, and standard deviation |
| /// - Parameter file: The source file name where the measurement was invoked |
| /// - Parameter line: The source line number where the measurement was invoked |
| func recordMeasurements(results: String, file: StaticString, line: Int) |
| |
| /// Reports a test failure from the analysis of performance measurements. |
| /// This can currently be caused by an unexpectedly large standard deviation |
| /// calculated over the data. |
| /// - Parameter description: An explanation of the failure |
| /// - Parameter file: The source file name where the measurement was invoked |
| /// - Parameter line: The source line number where the measurement was invoked |
| func recordFailure(description: String, file: StaticString, line: Int) |
| |
| /// Reports a misuse of the `PerformanceMeter` API, such as calling ` |
| /// startMeasuring` multiple times. |
| /// - Parameter description: An explanation of the misuse |
| /// - Parameter file: The source file name where the misuse occurred |
| /// - Parameter line: The source line number where the misuse occurred |
| func recordAPIViolation(description: String, file: StaticString, line: Int) |
| } |
| |
| /// - Bug: This class is intended to be `internal` but is public to work around |
| /// a toolchain bug on Linux. See `XCTestCase._performanceMeter` for more info. |
| public final class PerformanceMeter { |
| enum Error: Swift.Error, CustomStringConvertible { |
| case noMetrics |
| case unknownMetric(metricName: String) |
| case startMeasuringAlreadyCalled |
| case stopMeasuringAlreadyCalled |
| case startMeasuringNotCalled |
| case stopBeforeStarting |
| |
| var description: String { |
| switch self { |
| case .noMetrics: return "At least one metric must be provided to measure." |
| case .unknownMetric(let name): return "Unknown metric: \(name)" |
| case .startMeasuringAlreadyCalled: return "Already called startMeasuring() once this iteration." |
| case .stopMeasuringAlreadyCalled: return "Already called stopMeasuring() once this iteration." |
| case .startMeasuringNotCalled: return "startMeasuring() must be called during the block." |
| case .stopBeforeStarting: return "Cannot stop measuring before starting measuring." |
| } |
| } |
| } |
| |
| internal var didFinishMeasuring: Bool { |
| return state == .measurementFinished || state == .measurementAborted |
| } |
| |
| private enum State { |
| case iterationUnstarted |
| case iterationStarted |
| case iterationFinished |
| case measurementFinished |
| case measurementAborted |
| } |
| private var state: State = .iterationUnstarted |
| |
| private let metrics: [PerformanceMetric] |
| private let delegate: PerformanceMeterDelegate |
| private let invocationFile: StaticString |
| private let invocationLine: Int |
| |
| private init(metrics: [PerformanceMetric], delegate: PerformanceMeterDelegate, file: StaticString, line: Int) { |
| self.metrics = metrics |
| self.delegate = delegate |
| self.invocationFile = file |
| self.invocationLine = line |
| } |
| |
| static func measureMetrics(_ metricNames: [String], delegate: PerformanceMeterDelegate, file: StaticString = #file, line: Int = #line, for block: (PerformanceMeter) -> Void) { |
| do { |
| let metrics = try self.metrics(forNames: metricNames) |
| let meter = PerformanceMeter(metrics: metrics, delegate: delegate, file: file, line: line) |
| meter.measure(block) |
| } catch let e { |
| delegate.recordAPIViolation(description: String(describing: e), file: file, line: line) |
| } |
| } |
| |
| func startMeasuring(file: StaticString = #file, line: Int = #line) { |
| guard state == .iterationUnstarted else { |
| return recordAPIViolation(.startMeasuringAlreadyCalled, file: file, line: line) |
| } |
| state = .iterationStarted |
| metrics.forEach { $0.startMeasuring() } |
| } |
| |
| func stopMeasuring(file: StaticString = #file, line: Int = #line) { |
| guard state != .iterationUnstarted else { |
| return recordAPIViolation(.stopBeforeStarting, file: file, line: line) |
| } |
| |
| guard state != .iterationFinished else { |
| return recordAPIViolation(.stopMeasuringAlreadyCalled, file: file, line: line) |
| } |
| |
| state = .iterationFinished |
| metrics.forEach { $0.stopMeasuring() } |
| } |
| |
| func abortMeasuring() { |
| state = .measurementAborted |
| } |
| |
| |
| private static func metrics(forNames names: [String]) throws -> [PerformanceMetric] { |
| guard !names.isEmpty else { throw Error.noMetrics } |
| |
| let metricsMapping = [WallClockTimeMetric.name : WallClockTimeMetric.self] |
| |
| return try names.map({ |
| guard let metricType = metricsMapping[$0] else { throw Error.unknownMetric(metricName: $0) } |
| return metricType.init() |
| }) |
| } |
| |
| private var numberOfIterations: Int { |
| return 10 |
| } |
| |
| private func measure(_ block: (PerformanceMeter) -> Void) { |
| for _ in (0..<numberOfIterations) { |
| state = .iterationUnstarted |
| |
| block(self) |
| stopMeasuringIfNeeded() |
| |
| if state == .measurementAborted { return } |
| |
| if state == .iterationUnstarted { |
| recordAPIViolation(.startMeasuringNotCalled, file: invocationFile, line: invocationLine) |
| return |
| } |
| } |
| state = .measurementFinished |
| |
| recordResults() |
| recordFailures() |
| } |
| |
| private func stopMeasuringIfNeeded() { |
| if state == .iterationStarted { |
| stopMeasuring(file: invocationFile, line: invocationLine) |
| } |
| } |
| |
| private func recordResults() { |
| for metric in metrics { |
| delegate.recordMeasurements(results: metric.calculateResults(), file: invocationFile, line: invocationLine) |
| } |
| } |
| |
| private func recordFailures() { |
| metrics.flatMap({ $0.failureMessage() }).forEach { message in |
| delegate.recordFailure(description: message, file: invocationFile, line: invocationLine) |
| } |
| } |
| |
| private func recordAPIViolation(_ error: Error, file: StaticString, line: Int) { |
| state = .measurementAborted |
| delegate.recordAPIViolation(description: String(describing: error), file: file, line: line) |
| } |
| } |