//===--- 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 #endif import TestsUtils struct BenchResults { let sampleCount, min, max, mean, sd, median, maxRSS: UInt64 } 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 /// If we are asked to have a fixed number of iterations, the number of fixed /// iterations. The default value of 0 means: automatically compute the /// number of iterations to measure the test for a specified sample time. let fixedNumIters: UInt /// The number of samples we should take of each test. let numSamples: Int /// 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: Int? /// 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, afterRunSleep: Int? var fixedNumIters: UInt? 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; default: 1", parser: { Int($0) }) p.addArgument("--num-iters", \.fixedNumIters, help: "number of iterations averaged in the sample;\n" + "default: auto-scaled to measure for `sample-time`", parser: { UInt($0) }) 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: { Int($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 fixedNumIters = c.fixedNumIters ?? 0 numSamples = c.numSamples ?? 1 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) Verbose: \(verbose) LogMemory: \(logMemory) SampleTime: \(sampleTime) FixedIters: \(fixedNumIters) 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, 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) } } } func internalMeanSD(_ inputs: [UInt64]) -> (UInt64, UInt64) { // If we are empty, return 0, 0. if inputs.isEmpty { return (0, 0) } // If we have one element, return elt, 0. if inputs.count == 1 { return (inputs[0], 0) } // Ok, we have 2 elements. var sum1: UInt64 = 0 var sum2: UInt64 = 0 for i in inputs { sum1 += i } let mean: UInt64 = sum1 / UInt64(inputs.count) for i in inputs { sum2 = sum2 &+ UInt64((Int64(i) &- Int64(mean))&*(Int64(i) &- Int64(mean))) } return (mean, UInt64(sqrt(Double(sum2)/(Double(inputs.count) - 1)))) } func internalMedian(_ inputs: [UInt64]) -> UInt64 { return inputs.sorted()[inputs.count / 2] } #if SWIFT_RUNTIME_ENABLE_LEAK_CHECKER @_silgen_name("_swift_leaks_startTrackingObjects") func startTrackingObjects(_: UnsafePointer) -> () @_silgen_name("_swift_leaks_stopTrackingObjects") func stopTrackingObjects(_: UnsafePointer) -> Int #endif #if os(Linux) class Timer { typealias TimeT = timespec func getTime() -> TimeT { var ticks = timespec(tv_sec: 0, tv_nsec: 0) clock_gettime(CLOCK_REALTIME, &ticks) return ticks } func diffTimeInNanoSeconds(from start_ticks: TimeT, to end_ticks: TimeT) -> UInt64 { var elapsed_ticks = timespec(tv_sec: 0, tv_nsec: 0) if end_ticks.tv_nsec - start_ticks.tv_nsec < 0 { elapsed_ticks.tv_sec = end_ticks.tv_sec - start_ticks.tv_sec - 1 elapsed_ticks.tv_nsec = end_ticks.tv_nsec - start_ticks.tv_nsec + 1000000000 } else { elapsed_ticks.tv_sec = end_ticks.tv_sec - start_ticks.tv_sec elapsed_ticks.tv_nsec = end_ticks.tv_nsec - start_ticks.tv_nsec } return UInt64(elapsed_ticks.tv_sec) * UInt64(1000000000) + UInt64(elapsed_ticks.tv_nsec) } } #else class Timer { 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_ticks: TimeT, to end_ticks: TimeT) -> UInt64 { let elapsed_ticks = end_ticks - start_ticks return elapsed_ticks * UInt64(info.numer) / UInt64(info.denom) } } #endif class SampleRunner { let timer = Timer() let baseline = SampleRunner.getResourceUtilization() let c: TestConfig init(_ config: TestConfig) { self.c = config } private static func getResourceUtilization() -> rusage { var u = rusage(); getrusage(RUSAGE_SELF, &u); return u } /// Returns maximum resident set size (MAX_RSS) delta in bytes func measureMemoryUsage() -> Int { var current = SampleRunner.getResourceUtilization() let maxRSS = current.ru_maxrss - baseline.ru_maxrss if c.verbose { let pages = maxRSS / sysconf(_SC_PAGESIZE) func deltaEquation(_ stat: KeyPath) -> String { let b = baseline[keyPath: stat], c = current[keyPath: stat] return "\(c) - \(b) = \(c - b)" } print(""" MAX_RSS \(deltaEquation(\rusage.ru_maxrss)) (\(pages) pages) ICS \(deltaEquation(\rusage.ru_nivcsw)) VCS \(deltaEquation(\rusage.ru_nvcsw)) """) } return maxRSS } func run(_ name: String, fn: (Int) -> Void, num_iters: UInt) -> UInt64 { // Start the timer. #if SWIFT_RUNTIME_ENABLE_LEAK_CHECKER name.withCString { p in startTrackingObjects(p) } #endif let start_ticks = timer.getTime() fn(Int(num_iters)) // Stop the timer. let end_ticks = timer.getTime() #if SWIFT_RUNTIME_ENABLE_LEAK_CHECKER name.withCString { p in stopTrackingObjects(p) } #endif // Compute the spent time and the scaling factor. return timer.diffTimeInNanoSeconds(from: start_ticks, to: end_ticks) } } /// Invoke the benchmark entry point and return the run time in milliseconds. func runBench(_ test: BenchmarkInfo, _ c: TestConfig) -> BenchResults? { var samples = [UInt64](repeating: 0, count: c.numSamples) // 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 { if c.verbose { print("Skipping unsupported benchmark \(test.name)!") } return nil } if c.verbose { print("Running \(test.name) for \(c.numSamples) samples.") } let sampler = SampleRunner(c) test.setUpFunction?() for s in 0.. 0 { scale = UInt(time_per_sample / elapsed_time) } else { if c.verbose { print(" Warning: elapsed time is 0. This can be safely ignored if the body is empty.") } scale = 1 } } else { // Compute the scaling factor if a fixed c.fixedNumIters is not specified. scale = c.fixedNumIters if scale == 1 { elapsed_time = sampler.run(test.name, fn: testFn, num_iters: 1) } } // Make integer overflow less likely on platforms where Int is 32 bits wide. // FIXME: Switch BenchmarkInfo to use Int64 for the iteration scale, or fix // benchmarks to not let scaling get off the charts. scale = min(scale, UInt(Int.max) / 10_000) // Rerun the test with the computed scale factor. if scale > 1 { if c.verbose { print(" Measuring with scale \(scale).") } elapsed_time = sampler.run(test.name, fn: testFn, num_iters: scale) } else { scale = 1 } // save result in microseconds or k-ticks samples[s] = elapsed_time / UInt64(scale) / 1000 if c.verbose { print(" Sample \(s),\(samples[s])") } } test.tearDownFunction?() let (mean, sd) = internalMeanSD(samples) // Return our benchmark results. return BenchResults(sampleCount: UInt64(samples.count), min: samples.min()!, max: samples.max()!, mean: mean, sd: sd, median: internalMedian(samples), maxRSS: UInt64(sampler.measureMemoryUsage())) } /// Execute benchmarks and continuously report the measurement results. func runBenchmarks(_ c: TestConfig) { let withUnit = {$0 + "(us)"} let header = ( ["#", "TEST", "SAMPLES"] + ["MIN", "MAX", "MEAN", "SD", "MEDIAN"].map(withUnit) + (c.logMemory ? ["MAX_RSS(B)"] : []) ).joined(separator: c.delim) print(header) var testCount = 0 func report(_ index: String, _ t: BenchmarkInfo, results: BenchResults?) { func values(r: BenchResults) -> [String] { return ([r.sampleCount, r.min, r.max, r.mean, r.sd, r.median] + (c.logMemory ? [r.maxRSS] : [])).map { String($0) } } let benchmarkStats = ( [index, t.name] + (results.map(values) ?? ["Unsupported"]) ).joined(separator: c.delim) print(benchmarkStats) fflush(stdout) if (results != nil) { testCount += 1 } } for (index, test) in c.tests { report(index, test, results:runBench(test, c)) } print("") print("Totals\(c.delim)\(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 = [String(index), t.name, t.tags.sorted().description] .joined(separator: config.delim) print(testDescription) } case .run: runBenchmarks(config) if let x = config.afterRunSleep { sleep(UInt32(x)) } } }