Harness.swift 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. // Performance/Harness.swift - Performance harness definition
  2. //
  3. // Copyright (c) 2014 - 2019 Apple Inc. and the project authors
  4. // Licensed under Apache License v2.0 with Runtime Library Exception
  5. //
  6. // See LICENSE.txt for license information:
  7. // https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
  8. //
  9. // -----------------------------------------------------------------------------
  10. ///
  11. /// Defines the class that runs the performance tests.
  12. ///
  13. // -----------------------------------------------------------------------------
  14. import Foundation
  15. private func padded(_ input: String, to width: Int) -> String {
  16. input + String(repeating: " ", count: max(0, width - input.count))
  17. }
  18. /// It is expected that the generator will provide these in an extension.
  19. protocol GeneratedHarnessMembers {
  20. /// The number of times to loop the body of the run() method.
  21. /// Increase this to get better precision.
  22. var runCount: Int { get }
  23. /// The main body of the performance harness.
  24. func run()
  25. }
  26. /// Harness used for performance tests.
  27. ///
  28. /// The generator script will generate an extension to this class that adds a
  29. /// run() method, which the main.swift file calls.
  30. class Harness: GeneratedHarnessMembers {
  31. /// The number of times to execute the block passed to measure().
  32. var measurementCount = 10
  33. /// The number of times to call append() for repeated fields.
  34. let repeatedCount: Int32 = 10
  35. /// Ordered list of task names
  36. var taskNames = [String]()
  37. /// The times taken by subtasks during each measured attempt.
  38. var subtaskTimings = [String: [TimeInterval]]()
  39. /// Times for the subtasks in the current attempt.
  40. var currentSubtasks = [String: TimeInterval]()
  41. /// The file to which results should be written.
  42. let resultsFile: FileHandle?
  43. /// Creates a new harness that writes its statistics to the given file
  44. /// (as well as to stdout).
  45. init(resultsFile: FileHandle?) {
  46. self.resultsFile = resultsFile
  47. }
  48. /// Measures the time it takes to execute the given block. The block is
  49. /// executed five times and the mean/standard deviation are computed.
  50. func measure(block: () throws -> Void) {
  51. var timings = [TimeInterval]()
  52. subtaskTimings.removeAll()
  53. print("Running each check \(runCount) times, times in µs")
  54. var headingsDisplayed = false
  55. do {
  56. // Do each measurement multiple times and collect the means and standard
  57. // deviation to account for noise.
  58. for attempt in 1...measurementCount {
  59. currentSubtasks.removeAll()
  60. taskNames.removeAll()
  61. let start = Date()
  62. for _ in 0..<runCount {
  63. taskNames.removeAll()
  64. try block()
  65. }
  66. let end = Date()
  67. let diff = end.timeIntervalSince(start) * 1000
  68. timings.append(diff)
  69. if !headingsDisplayed {
  70. let names = taskNames
  71. print(" ", terminator: "")
  72. for (i, name) in names.enumerated() {
  73. if i % 2 == 0 {
  74. print(padded(name, to: 18), terminator: "")
  75. }
  76. }
  77. print()
  78. print(" ", terminator: "")
  79. print(padded("", to: 9), terminator: "")
  80. for (i, name) in names.enumerated() {
  81. if i % 2 == 1 {
  82. print(padded(name, to: 18), terminator: "")
  83. }
  84. }
  85. print()
  86. headingsDisplayed = true
  87. }
  88. print(String(format: "%3d", attempt), terminator: "")
  89. for name in taskNames {
  90. let time = currentSubtasks[name] ?? 0
  91. print(String(format: "%9.3f", time), terminator: "")
  92. subtaskTimings[name] = (subtaskTimings[name] ?? []) + [time]
  93. }
  94. print()
  95. }
  96. } catch let e {
  97. fatalError("Generated harness threw an error: \(e)")
  98. }
  99. for (name, times) in subtaskTimings {
  100. writeToLog("\"\(name)\": \(times),\n")
  101. }
  102. let (mean, stddev) = statistics(timings)
  103. let stats =
  104. String(format: "Relative stddev = %.1f%%\n", (stddev / mean) * 100.0)
  105. print(stats)
  106. }
  107. /// Measure an individual subtask whose timing will be printed separately
  108. /// from the main results.
  109. func measureSubtask<Result>(
  110. _ name: String,
  111. block: () throws -> Result
  112. ) rethrows -> Result {
  113. try autoreleasepool { () -> Result in
  114. taskNames.append(name)
  115. let start = Date()
  116. let result = try block()
  117. let end = Date()
  118. let diff = end.timeIntervalSince(start) / Double(runCount) * 1000000.0
  119. currentSubtasks[name] = (currentSubtasks[name] ?? 0) + diff
  120. return result
  121. }
  122. }
  123. /// Compute the mean and standard deviation of the given time intervals.
  124. private func statistics(_ timings: [TimeInterval]) -> (mean: TimeInterval, stddev: TimeInterval) {
  125. var sum: TimeInterval = 0
  126. var sqsum: TimeInterval = 0
  127. for timing in timings {
  128. sum += timing
  129. sqsum += timing * timing
  130. }
  131. let n = TimeInterval(timings.count)
  132. let mean = sum / n
  133. let variance = sqsum / n - mean * mean
  134. return (mean: mean, stddev: sqrt(variance))
  135. }
  136. /// Writes a string to the data results file that will be parsed by the
  137. /// calling script to produce visualizations.
  138. private func writeToLog(_ string: String) {
  139. if let resultsFile = resultsFile {
  140. let utf8 = Data(string.utf8)
  141. resultsFile.write(utf8)
  142. }
  143. }
  144. }