| //===--- 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 |
| |
| import TestsUtils |
| |
| 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(samples.count) * quantile).rounded(.up)) - 1) |
| return samples[index] |
| } |
| |
| var sampleCount: T { return samples.count } |
| var min: T { return samples.first! } |
| var max: T { return samples.last! } |
| var mean: T { return Int(stats.mean.rounded()) } |
| var sd: T { return Int(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? |
| |
| /// 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<BenchmarkCategory>? |
| var numSamples: UInt? |
| var numIters: UInt? |
| var quantile: UInt? |
| 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<BenchmarkCategory> { |
| // 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("--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. |
| delim = c.delim ?? "," |
| sampleTime = c.sampleTime ?? 1.0 |
| numIters = c.numIters.map { Int($0) } |
| numSamples = c.numSamples.map { Int($0) } |
| quantile = c.quantile.map { Int($0) } |
| delta = c.delta ?? false |
| verbose = c.verbose ?? false |
| logMemory = c.logMemory ?? false |
| afterRunSleep = c.afterRunSleep |
| action = c.action ?? .run |
| tests = TestConfig.filterTests(registeredBenchmarks, |
| specifiedTests: Set(c.tests ?? []), |
| tags: c.tags ?? [], |
| skipTags: c.skipTags ?? [.unstable, .skip]) |
| |
| if logMemory && 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! |
| """) |
| } |
| |
| if verbose { |
| let testList = tests.map({ $0.1.name }).joined(separator: ", ") |
| print(""" |
| --- CONFIG --- |
| NumSamples: \(numSamples ?? 0) |
| Verbose: \(verbose) |
| LogMemory: \(logMemory) |
| SampleTime: \(sampleTime) |
| NumIters: \(numIters ?? 0) |
| Quantile: \(quantile ?? 0) |
| Delimiter: \(String(reflecting: delim)) |
| Tests Filter: \(c.tests ?? []) |
| Tests to run: \(testList) |
| |
| --- DATA ---\n |
| """) |
| } |
| } |
| |
| /// 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<String>, |
| tags: Set<BenchmarkCategory>, |
| skipTags: Set<BenchmarkCategory> |
| ) -> [(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 n < 2 ? 0.0 : S / Double(n - 1) } |
| var standardDeviation: Double { return 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<CChar>) -> () |
| @_silgen_name("_swift_leaks_stopTrackingObjects") |
| func stopTrackingObjects(_: UnsafePointer<CChar>) -> Int |
| |
| #endif |
| |
| final class Timer { |
| #if os(Linux) |
| typealias TimeT = timespec |
| |
| func getTime() -> TimeT { |
| var ts = timespec(tv_sec: 0, tv_nsec: 0) |
| clock_gettime(CLOCK_REALTIME, &ts) |
| return ts |
| } |
| |
| 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 |
| typealias TimeT = UInt64 |
| var info = mach_timebase_info_data_t(numer: 0, denom: 0) |
| |
| init() { |
| mach_timebase_info(&info) |
| } |
| |
| func getTime() -> TimeT { |
| return mach_absolute_time() |
| } |
| |
| func diffTimeInNanoSeconds(from start: TimeT, to end: TimeT) -> UInt64 { |
| let elapsed = end - start |
| return elapsed * UInt64(info.numer) / UInt64(info.denom) |
| } |
| #endif |
| } |
| |
| extension UInt64 { |
| var microseconds: Int { return Int(self / 1000) } |
| } |
| |
| /// 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<rusage_info_t>.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<rusage, Int>) -> 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.microseconds) μs") |
| 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 (μs). |
| 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 |
| |
| return lastSampleTime.microseconds / 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 / 10_000, // by the inner loop multiplier inside the `testFn`. |
| c.numIters ?? calibrateMeasurements()) |
| |
| let numSamples = c.numSamples ?? min(2000, // 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 + "(μs)"} |
| 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) |
| } |
| } |
| } |