mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
466 lines
15 KiB
Swift
466 lines
15 KiB
Swift
//===--- 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<BenchmarkCategory>?
|
|
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<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; 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<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) }
|
|
}
|
|
}
|
|
|
|
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<CChar>) -> ()
|
|
@_silgen_name("_swift_leaks_stopTrackingObjects")
|
|
func stopTrackingObjects(_: UnsafePointer<CChar>) -> 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<rusage, Int>) -> 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..<c.numSamples {
|
|
let nsPerSecond = 1_000_000_000.0 // nanoseconds
|
|
let time_per_sample = UInt64(c.sampleTime * nsPerSecond)
|
|
|
|
var scale : UInt
|
|
var elapsed_time : UInt64 = 0
|
|
if c.fixedNumIters == 0 {
|
|
elapsed_time = sampler.run(test.name, fn: testFn, num_iters: 1)
|
|
|
|
if elapsed_time > 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))
|
|
}
|
|
}
|
|
}
|