├── XCMetrics ├── Assets.xcassets │ ├── Contents.json │ ├── cat.imageset │ │ ├── cat.png │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── String+Init.swift ├── Log.swift ├── ImageProcessor.swift ├── AsyncTask.swift ├── SignpostName.swift ├── NavigationControllerDelegate.swift ├── ImageLocalStorage.swift ├── ImageProcessingViewController.swift ├── Lifecycle │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── SceneDelegate.swift ├── Info.plist ├── ScrollTableViewController.swift ├── AtomicArray.swift ├── Base.lproj │ └── Main.storyboard └── PermutationAlgorithms.swift ├── XCMetrics.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ └── XCMetrics.xcscheme └── project.pbxproj ├── XCMetricsTests ├── Signpost.swift ├── XCSignpostHookedTestCase.swift ├── MXMInstrumentHookedTestCase.swift ├── Measurements.swift ├── Info.plist ├── MXMInstrumentHook.swift ├── XCPerformanceMetricsTests.swift ├── MonotonicClockMetric.swift ├── XCAsyncPerformanceTests.swift ├── XCSignpostMetricsTests.swift ├── XCMetricsTests.swift ├── XCTOSSignpostMetric+Hook.swift └── XCTPerformanceMetric+All.swift ├── XCMetricsUITests ├── XCAppLaunchHookedTestCase.swift ├── Info.plist ├── XCTApplicationLaunchMetric+Hook.swift ├── XCAppLaunchMetricsUITests.swift ├── XCSignpostTransitionMetricsUITests.swift ├── XCSignpostScrollMetricsUITests.swift └── XCMetricsUITests.swift ├── LICENSE.md ├── XCMetricsTestPlan └── XCMetrics.xctestplan ├── .gitignore └── README.md /XCMetrics/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XCMetrics/Assets.xcassets/cat.imageset/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoaurabhK/XCMetrics/HEAD/XCMetrics/Assets.xcassets/cat.imageset/cat.png -------------------------------------------------------------------------------- /XCMetrics.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /XCMetrics/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /XCMetricsTests/Signpost.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Signpost.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 25/08/20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Signpost: Hashable { 11 | let subsystem: String 12 | let category: String 13 | let name: String 14 | } 15 | -------------------------------------------------------------------------------- /XCMetrics.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /XCMetrics/String+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Init.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | init(_ staticString: StaticString) { 12 | self = staticString.withUTF8Buffer { 13 | String(decoding: $0, as: UTF8.self) 14 | } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /XCMetrics/Assets.xcassets/cat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cat.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XCMetrics.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "InterposeKit", 6 | "repositoryURL": "https://github.com/steipete/InterposeKit", 7 | "state": { 8 | "branch": "master", 9 | "revision": "1f53cdd25a939018dd668bb38722226f4fa141b2", 10 | "version": null 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /XCMetricsTests/XCSignpostHookedTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCSignpostHookTestCase.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 25/08/20. 6 | // 7 | 8 | import XCTest 9 | import InterposeKit 10 | 11 | class XCSignpostHookedTestCase: MXMInstrumentHookedTestCase { 12 | private static var signpostHook: Interpose? 13 | 14 | class override func setUp() { 15 | super.setUp() 16 | signpostHook = XCTOSSignpostMetric.signpostHook 17 | } 18 | 19 | class override func tearDown() { 20 | _ = try? signpostHook?.revert() 21 | Measurements.clear() 22 | super.tearDown() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /XCMetricsTests/MXMInstrumentHookedTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MXMInstrumentHookedTestCase.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 29/08/20. 6 | // 7 | 8 | import XCTest 9 | import InterposeKit 10 | 11 | class MXMInstrumentHookedTestCase: XCTestCase { 12 | private static var serialInstrumentalsQueue: Interpose? 13 | 14 | class override func setUp() { 15 | super.setUp() 16 | serialInstrumentalsQueue = MXMInstrumentHook.serialInstrumentalsQueue 17 | } 18 | 19 | class override func tearDown() { 20 | _ = try? serialInstrumentalsQueue?.revert() 21 | super.tearDown() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /XCMetricsTests/Measurements.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Measurements.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 25/08/20. 6 | // 7 | 8 | import XCTest 9 | 10 | struct Measurements { 11 | private static var storage = [Signpost: [XCTPerformanceMeasurement]]() 12 | static func value(for signpost: Signpost) -> [XCTPerformanceMeasurement] { 13 | return storage[signpost] ?? [] 14 | } 15 | 16 | static func update(measurements: [XCTPerformanceMeasurement], for signpost: Signpost) { 17 | storage[signpost] = measurements 18 | } 19 | 20 | static func clear() { 21 | storage.removeAll() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /XCMetricsUITests/XCAppLaunchHookedTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCAppLaunchHookedTestCase.swift 3 | // XCMetricsUITests 4 | // 5 | // Created by Soaurabh Kakkar on 25/08/20. 6 | // 7 | 8 | import XCTest 9 | import InterposeKit 10 | 11 | class XCAppLaunchHookedTestCase: MXMInstrumentHookedTestCase { 12 | private static var appLaunchHook: Interpose? 13 | 14 | class override func setUp() { 15 | super.setUp() 16 | appLaunchHook = XCTApplicationLaunchMetric.appLaunchHook 17 | } 18 | 19 | class override func tearDown() { 20 | _ = try? appLaunchHook?.revert() 21 | Measurements.clear() 22 | super.tearDown() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /XCMetrics/Log.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Log.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Log { 11 | let subsystem: String 12 | let category: String 13 | } 14 | 15 | let permutationLog = Log(subsystem: "com.sk.XCMetrics", category: "PermutationOperations") 16 | let asyncTaskLog = Log(subsystem: "com.sk.XCMetrics", category: "asyncTasks") 17 | let processImageLog = Log(subsystem: "com.sk.XCMetrics", category: "processImages") 18 | let scrollLog = Log(subsystem: "com.sk.XCMetrics", category: "scrollOperations") 19 | let navTransitionLog = Log(subsystem: "com.sk.XCMetrics", category: "navigationTransitions") 20 | 21 | -------------------------------------------------------------------------------- /XCMetrics/ImageProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageProcessor.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import CoreImage 9 | import CoreImage.CIFilterBuiltins 10 | import UIKit 11 | 12 | final class ImageProcessor { 13 | class func processImage(_ inputImage: UIImage) -> UIImage { 14 | let context = CIContext(options: nil) 15 | let filter = CIFilter.sepiaTone() 16 | filter.inputImage = CIImage(image: inputImage) 17 | filter.intensity = 0.7 18 | guard let ciOutputImage = filter.outputImage else { return inputImage } 19 | guard let cgOutputImage = context.createCGImage(ciOutputImage, from: ciOutputImage.extent) else { return inputImage } 20 | return UIImage(cgImage: cgOutputImage) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /XCMetricsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /XCMetricsUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /XCMetrics/AsyncTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncTask.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import Foundation 9 | import os.signpost 10 | 11 | private let asyncTaskOSLog = OSLog(subsystem: asyncTaskLog.subsystem, category: asyncTaskLog.category) 12 | private let signpostID = OSSignpostID(log: asyncTaskOSLog) 13 | 14 | func asyncTask(withDuration duration: Double, _ completion: @escaping () -> Void) { 15 | os_signpost(.begin, log: asyncTaskOSLog, name: SignpostName.asyncTask, signpostID: signpostID, "task started: %{public}.2f duration", duration) 16 | DispatchQueue.main.asyncAfter(deadline: .now() + duration) { 17 | os_signpost(.end, log: asyncTaskOSLog, name: SignpostName.asyncTask, signpostID: signpostID, "task ended: %{public}.2f duration", duration) 18 | completion() 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /XCMetrics/SignpostName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignpostName.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SignpostName { 11 | static let permutationsOverMutationsFunctionalOptimized4: StaticString = "permutationsOverMutationsFunctionalOptimized4" 12 | static let permutationsOverMutations: StaticString = "permutationsOverMutations" 13 | static let permutationsConcurrent: StaticString = "permutationsConcurrent" 14 | static let asyncTask: StaticString = "asyncTask" 15 | static let processImage: StaticString = "processImage" 16 | static let scrollDecelerationSignpost: StaticString = "scrollDecelerationSignpost" 17 | static let scrollDraggingSignpost: StaticString = "scrollDraggingSignpost" 18 | static let scrollViewNavTransition: StaticString = "scrollViewNavigationTransition" 19 | } 20 | 21 | -------------------------------------------------------------------------------- /XCMetricsTests/MXMInstrumentHook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InstrumentHook.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 29/08/20. 6 | // 7 | 8 | import Foundation 9 | import InterposeKit 10 | 11 | final class MXMInstrumentHook: NSObject { 12 | static var serialInstrumentalsQueue: Interpose? { 13 | try? Interpose(NSClassFromString("MXMInstrument")!) { 14 | try $0.prepareHook( 15 | Selector(("initWithInstrumentals:")), 16 | methodSignature: (@convention(c) (NSObject, Selector, AnyObject) -> NSObject).self, 17 | hookSignature: (@convention(block) (NSObject, AnyObject) -> NSObject).self) { 18 | store in { `self`, inputInstrumentals in 19 | let hookedInstrument = store.original(`self`, store.selector, inputInstrumentals) 20 | hookedInstrument.setValue(DispatchQueue(label: "com.apple.metricmeasurement.instrument.instrumentals.hooked"), forKey: "_instrumentalsQueue") 21 | return hookedInstrument 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Soaurabh Kakkar 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /XCMetrics/NavigationControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationControllerDelegate.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import UIKit 9 | import os.signpost 10 | 11 | @objc extension UIViewController { 12 | func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { 13 | } 14 | 15 | func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { 16 | } 17 | } 18 | 19 | final class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate { 20 | func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { 21 | viewController.navigationController(navigationController, willShow: viewController, animated: animated) 22 | } 23 | 24 | func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { 25 | viewController.navigationController(navigationController, didShow: viewController, animated: animated) 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /XCMetricsTestPlan/XCMetrics.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "D88F327E-911E-452F-9FC1-41F42D85DC9A", 5 | "name" : "XCMetrics", 6 | "options" : { 7 | "uiTestingScreenshotsLifetime" : "deleteOnSuccess" 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "codeCoverage" : false, 13 | "mainThreadCheckerEnabled" : false, 14 | "targetForVariableExpansion" : { 15 | "containerPath" : "container:XCMetrics.xcodeproj", 16 | "identifier" : "B5E7E85A24EE67BA00024239", 17 | "name" : "XCMetrics" 18 | }, 19 | "testExecutionOrdering" : "random", 20 | "uiTestingScreenshotsLifetime" : "keepNever", 21 | "userAttachmentLifetime" : "keepNever" 22 | }, 23 | "testTargets" : [ 24 | { 25 | "target" : { 26 | "containerPath" : "container:XCMetrics.xcodeproj", 27 | "identifier" : "B5E7E87224EE67BB00024239", 28 | "name" : "XCMetricsTests" 29 | } 30 | }, 31 | { 32 | "target" : { 33 | "containerPath" : "container:XCMetrics.xcodeproj", 34 | "identifier" : "B5E7E87D24EE67BC00024239", 35 | "name" : "XCMetricsUITests" 36 | } 37 | } 38 | ], 39 | "version" : 1 40 | } 41 | -------------------------------------------------------------------------------- /XCMetricsUITests/XCTApplicationLaunchMetric+Hook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTApplicationLaunchMetric+Hooked.swift 3 | // XCMetricsUITests 4 | // 5 | // Created by Soaurabh Kakkar on 25/08/20. 6 | // 7 | 8 | import XCTest 9 | import InterposeKit 10 | 11 | extension XCTApplicationLaunchMetric { 12 | static var appLaunchHook: Interpose? { 13 | try? Interpose(XCTApplicationLaunchMetric.self) { 14 | try $0.prepareHook( 15 | #selector(XCTApplicationLaunchMetric.reportMeasurements(from:to:)), 16 | methodSignature: (@convention(c) (XCTApplicationLaunchMetric, Selector, XCTPerformanceMeasurementTimestamp, XCTPerformanceMeasurementTimestamp) -> [XCTPerformanceMeasurement]).self, 17 | hookSignature: (@convention(block) (XCTApplicationLaunchMetric, XCTPerformanceMeasurementTimestamp, XCTPerformanceMeasurementTimestamp) -> [XCTPerformanceMeasurement]).self) { 18 | store in { `self`, startTime, endTime in 19 | let measurements = store.original(`self`, store.selector, startTime, endTime) 20 | return hookedValue(for: signpost(from: `self`), with: measurements) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /XCMetrics/ImageLocalStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageLocalStorage.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ImageLocalStorage { 11 | private let folderURL: URL 12 | 13 | init(identifier: String) throws { 14 | let url = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) 15 | folderURL = url.appendingPathComponent(identifier) 16 | try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil) 17 | } 18 | 19 | func remove(_ filename: String) { 20 | let url = folderURL.appendingPathComponent(filename) 21 | try? FileManager.default.removeItem(at: url) 22 | } 23 | 24 | func store(_ image: UIImage, filename: String) { 25 | let url = folderURL.appendingPathComponent(filename) 26 | let data = image.jpegData(compressionQuality: 1) 27 | try? data?.write(to: url, options: .atomicWrite) 28 | } 29 | 30 | func load(withFilename filename: String) -> UIImage? { 31 | let url = folderURL.appendingPathComponent(filename) 32 | return UIImage(contentsOfFile: url.path) 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /XCMetrics/ImageProcessingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageProcessingViewController.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import UIKit 9 | import os.signpost 10 | 11 | private let processImageOSLog = OSLog(subsystem: processImageLog.subsystem, category: processImageLog.category) 12 | 13 | final class ImageProcessingViewController: UIViewController { 14 | private let originalImage = UIImage(named: "cat")! 15 | private let imageStorage: ImageLocalStorage? = { 16 | return try? ImageLocalStorage(identifier: "Background") 17 | }() 18 | 19 | @IBOutlet weak var originalImageView: UIImageView! 20 | @IBOutlet weak var processedImageView: UIImageView! 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | originalImageView.image = originalImage 25 | } 26 | 27 | @IBAction func processImage(_ sender: UIButton) { 28 | os_signpost(.begin, log: processImageOSLog, name: SignpostName.processImage) 29 | 30 | imageStorage?.store(originalImage, filename: "original") 31 | 32 | let processedImage = ImageProcessor.processImage(originalImage) 33 | imageStorage?.store(processedImage, filename: "processed") 34 | processedImageView.image = processedImage 35 | 36 | os_signpost(.end, log: processImageOSLog, name: SignpostName.processImage) 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /XCMetricsUITests/XCAppLaunchMetricsUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCAppLaunchMetricsUITests.swift 3 | // XCMetricsUITests 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import XCTest 9 | 10 | final class XCAppLaunchMetricsUITests: XCAppLaunchHookedTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // In UI tests it is usually best to stop immediately when a failure occurs. 14 | continueAfterFailure = false 15 | } 16 | 17 | func testLaunchPerformance() throws { 18 | // This measures how long it takes to launch your application. 19 | let app = XCUIApplication() 20 | 21 | measure(metrics: [XCTApplicationLaunchMetric()]) { 22 | app.launch() 23 | } 24 | } 25 | 26 | // waitUntilResponsive specifies the end of the application launch interval to be when the application has displayed the first frame and is responsive. 27 | // NOTE: https://stackoverflow.com/questions/59645536/available-attribute-does-not-work-with-xctest-classes-or-methods 28 | func testLaunchPerfUntilResponsive() throws { 29 | // This measures how long it takes to launch your application. 30 | guard #available(iOS 14, *) else { return } 31 | let app = XCUIApplication() 32 | 33 | measure(metrics: [XCTApplicationLaunchMetric(waitUntilResponsive: true)]) { 34 | app.launch() 35 | app.activate() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /XCMetrics/Lifecycle/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | final class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /XCMetricsTests/XCPerformanceMetricsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCPerformanceMetricsTests.swift 3 | // XCMetricsTests 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import XCTest 9 | @testable import XCMetrics 10 | 11 | ///Permuation Defination: The number of ways to choose a sample of r elements from a set of n distinct objects where order does matter and replacements are not allowed. When n = r this reduces to n!, a simple factorial of n. 12 | /// nPr = fact(n) / fact(n - r) 13 | final class XCPerformanceMetricsTests: XCTestCase { 14 | let source: [Int] = [1, 2, 3, 4, 5, 6, 7] // 7! = 5040 unique arrays. 15 | 16 | // Self.defaultPerformanceMetrics will be measured 17 | func testPerformanceFunctional() { 18 | self.measure { 19 | _ = source.permutationsOverMutationsFunctionalOptimized4() 20 | } 21 | } 22 | 23 | func testPerformanceNonFunctional() { 24 | self.measureMetrics(Self.defaultPerformanceMetrics, automaticallyStartMeasuring: true) { 25 | _ = source.permutationsOverMutations() 26 | } 27 | } 28 | 29 | func testPerformanceConcurrent() { 30 | self.measureMetrics(Self.defaultPerformanceMetrics, automaticallyStartMeasuring: false) { 31 | startMeasuring() 32 | _ = source.permutationsConcurrent(concurrentThreads: 4) 33 | stopMeasuring() 34 | } 35 | } 36 | 37 | // By default, it returns [XCTPerformanceMetric.wallClockTime] 38 | // XCTPerformanceMetric.wallClockTime is to measure the number of seconds the block of code takes to execute. 39 | override class var defaultPerformanceMetrics: [XCTPerformanceMetric] { 40 | return XCTPerformanceMetric.all 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /XCMetrics/Lifecycle/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /XCMetricsUITests/XCSignpostTransitionMetricsUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCSignpostTransitionMetricsUITests.swift 3 | // XCMetricsUITests 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import XCTest 9 | 10 | final class XCSignpostTransitionMetricsUITests: XCSignpostHookedTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // In UI tests it is usually best to stop immediately when a failure occurs. 14 | continueAfterFailure = false 15 | } 16 | 17 | func testNavigationTransitionPerformance() { 18 | let app = XCUIApplication() 19 | app.launch() 20 | let scrollPerfCell = app.staticTexts["Scroll Performance"] 21 | 22 | let signpostMetric = self.signpostMetric(for: SignpostName.scrollViewNavTransition) 23 | 24 | let metrics: [XCTMetric] 25 | if #available(iOS 14, *) { 26 | metrics = [signpostMetric, XCTOSSignpostMetric.navigationTransitionMetric] 27 | } else { 28 | metrics = [signpostMetric] 29 | } 30 | 31 | measure(metrics: metrics) { 32 | let backButton = app.navigationBars.buttons["Back"] 33 | if backButton.exists { backButton.tap() } 34 | startMeasuring() 35 | scrollPerfCell.tap() 36 | } 37 | } 38 | 39 | override class var defaultMeasureOptions: XCTMeasureOptions { 40 | let measureOptions = XCTMeasureOptions() 41 | measureOptions.invocationOptions = [.manuallyStart] 42 | return measureOptions 43 | } 44 | 45 | private func signpostMetric(for name: StaticString) -> XCTOSSignpostMetric { 46 | return XCTOSSignpostMetric(subsystem: navTransitionLog.subsystem, category: navTransitionLog.category, name: String(name)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /XCMetrics/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /XCMetricsTests/MonotonicClockMetric.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MonotonicClockMetric.swift 3 | // XCMetricsTests 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import XCTest 9 | 10 | final class MonotonicClockMetric: NSObject, XCTMetric { 11 | var startTimes = AtomicArray() 12 | var endTimes = AtomicArray() 13 | 14 | // This will get called on main queue/thread. 15 | func reportMeasurements(from startTime: XCTPerformanceMeasurementTimestamp, to endTime: XCTPerformanceMeasurementTimestamp) throws -> [XCTPerformanceMeasurement] { 16 | 17 | let startTimeNano = startTime.absoluteTimeNanoSeconds 18 | let endTimeNano = endTime.absoluteTimeNanoSeconds 19 | 20 | // NOTE: We can change the order of if and else-if to run our monotonic-time implementation. 21 | let timeDiffNS: UInt64 22 | if endTimeNano > startTimeNano { 23 | timeDiffNS = endTimeNano - startTimeNano 24 | } else if let startTimeNS = startTimes.first, let endTimeNS = endTimes.first, endTimeNS > startTimeNS { 25 | timeDiffNS = endTimeNS - startTimeNS 26 | } else { 27 | timeDiffNS = .min // or throw exception here. 28 | } 29 | startTimes.removeFirst() 30 | endTimes.removeFirst() 31 | 32 | let measurement = Measurement(value: Double(timeDiffNS), unit: Unit(symbol: "ns")) 33 | return [XCTPerformanceMeasurement(identifier: "com.sk.XCTPerformanceMetric_MonotonicClockTime", displayName: "Monotonic Clock Time", value: measurement)] 34 | } 35 | 36 | // This will get called on background queue i.e. any random thread for each iteration. 37 | func willBeginMeasuring() { 38 | startTimes.append(DispatchTime.now().uptimeNanoseconds) 39 | } 40 | 41 | // This will get called on background queue i.e. any random thread for each iteration. 42 | func didStopMeasuring() { 43 | endTimes.append(DispatchTime.now().uptimeNanoseconds) 44 | } 45 | 46 | func copy(with zone: NSZone? = nil) -> Any { 47 | return self 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /XCMetrics/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /XCMetrics/Lifecycle/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /XCMetricsTests/XCAsyncPerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCAsyncPerformanceTests.swift 3 | // XCMetricsTests 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import XCTest 9 | @testable import XCMetrics 10 | 11 | final class XCAsyncPerformanceTests: XCSignpostHookedTestCase { 12 | 13 | func testPerformanceAsyncTask() { 14 | let signpostMetric = self.signpostMetric(for: SignpostName.asyncTask) 15 | 16 | self.measure(metrics: [signpostMetric, XCTClockMetric()], options: Self.defaultMeasureOptions) { 17 | let exp = expectation(description: "asyncTask") 18 | self.startMeasuring() 19 | asyncTask(withDuration: 0.1) { 20 | if #available(iOS 14, *) { 21 | self.stopMeasuring() 22 | } 23 | exp.fulfill() 24 | } 25 | wait(for: [exp], timeout: 0.5) 26 | } 27 | } 28 | 29 | func testPerformanceAsyncTaskWallClock() { 30 | self.measureMetrics([XCTPerformanceMetric.wallClockTime], automaticallyStartMeasuring: false) { 31 | let exp = expectation(description: "asyncTask") 32 | self.startMeasuring() 33 | asyncTask(withDuration: 0.1) { 34 | self.stopMeasuring() 35 | exp.fulfill() 36 | } 37 | wait(for: [exp], timeout: 0.5) 38 | } 39 | } 40 | 41 | // XCTMeasureOptions `default` option, automatically starts/stops measuring and has iterationCount of 5. 42 | // We have overriden defaultMeasureOptions to have iterationCount of 10. Note that the block is actually invoked `iterationCount` + 1 times, and the first iteration is discarded. This is done to reduce the chance that the first iteration will be an outlier. 43 | override class var defaultMeasureOptions: XCTMeasureOptions { 44 | let measureOptions = XCTMeasureOptions() 45 | measureOptions.iterationCount = 10 46 | if #available(iOS 14, *) { 47 | measureOptions.invocationOptions = [.manuallyStart, .manuallyStop] 48 | } else { 49 | // NOTE: .manuallyStop doesn't work on iOS 13 with some metrics 50 | measureOptions.invocationOptions = [.manuallyStart] 51 | } 52 | return measureOptions 53 | } 54 | 55 | private func signpostMetric(for name: StaticString) -> XCTOSSignpostMetric { 56 | return XCTOSSignpostMetric(subsystem: asyncTaskLog.subsystem, category: asyncTaskLog.category, name: String(name)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /XCMetricsUITests/XCSignpostScrollMetricsUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCSignpostScrollMetricsUITests.swift 3 | // XCMetricsUITests 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import XCTest 9 | 10 | final class XCSignpostScrollMetricsUITests: XCSignpostHookedTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // In UI tests it is usually best to stop immediately when a failure occurs. 14 | continueAfterFailure = false 15 | } 16 | 17 | func testScrollDecelerationPerformance() { 18 | let app = XCUIApplication() 19 | app.launch() 20 | 21 | app.staticTexts["Scroll Performance"].tap() 22 | let emojiTableView = app.tables.firstMatch 23 | 24 | let signpostMetric = self.signpostMetric(for: SignpostName.scrollDecelerationSignpost) 25 | 26 | let metrics: [XCTMetric] 27 | if #available(iOS 14, *) { 28 | metrics = [signpostMetric, XCTOSSignpostMetric.scrollDecelerationMetric] 29 | } else { 30 | metrics = [signpostMetric] 31 | } 32 | 33 | measure(metrics: metrics) { 34 | emojiTableView.swipeDown(velocity: .fast) 35 | startMeasuring() 36 | emojiTableView.swipeUp(velocity: .fast) 37 | } 38 | } 39 | 40 | func testScrollDraggingPerformance() { 41 | let app = XCUIApplication() 42 | app.launch() 43 | 44 | app.staticTexts["Scroll Performance"].tap() 45 | let emojiTableView = app.tables.firstMatch 46 | 47 | let signpostMetric = self.signpostMetric(for: SignpostName.scrollDraggingSignpost) 48 | 49 | let metrics: [XCTMetric] 50 | if #available(iOS 14, *) { 51 | metrics = [signpostMetric, XCTOSSignpostMetric.scrollDraggingMetric] 52 | } else { 53 | metrics = [signpostMetric] 54 | } 55 | 56 | measure(metrics: metrics) { 57 | emojiTableView.swipeDown(velocity: .fast) 58 | startMeasuring() 59 | emojiTableView.swipeUp(velocity: .fast) 60 | } 61 | } 62 | 63 | override class var defaultMeasureOptions: XCTMeasureOptions { 64 | let measureOptions = XCTMeasureOptions() 65 | measureOptions.invocationOptions = [.manuallyStart] 66 | return measureOptions 67 | } 68 | 69 | private func signpostMetric(for name: StaticString) -> XCTOSSignpostMetric { 70 | return XCTOSSignpostMetric(subsystem: scrollLog.subsystem, category: scrollLog.category, name: String(name)) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /XCMetricsTests/XCSignpostMetricsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCSignpostMetricsTests.swift 3 | // XCMetricsTests 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import XCTest 9 | @testable import XCMetrics 10 | 11 | ///Permuation Defination: The number of ways to choose a sample of r elements from a set of n distinct objects where order does matter and replacements are not allowed. When n = r this reduces to n!, a simple factorial of n. 12 | /// nPr = fact(n) / fact(n - r) 13 | final class XCSignpostMetricsTests: XCSignpostHookedTestCase { 14 | let source: [Int] = [1, 2, 3, 4, 5, 6, 7] // 7! = 5040 unique arrays. 15 | 16 | // Self.defaultMeasureOptions will be used 17 | func testPerformanceFunctional() { 18 | let signpostMetric = self.signpostMetric(for: SignpostName.permutationsOverMutationsFunctionalOptimized4) 19 | 20 | self.measure(metrics: [signpostMetric, XCTClockMetric()]) { 21 | _ = source.permutationsOverMutationsFunctionalOptimized4() 22 | } 23 | } 24 | 25 | // Self.defaultMeasureOptions will be used 26 | func testPerformanceNonFunctional() { 27 | let signpostMetric = self.signpostMetric(for: SignpostName.permutationsOverMutations) 28 | 29 | self.measure(metrics: [signpostMetric, XCTClockMetric()]) { 30 | _ = source.permutationsOverMutations() 31 | } 32 | } 33 | 34 | // Self.defaultMeasureOptions will be used 35 | func testPerformanceConcurrent() { 36 | let signpostMetric = self.signpostMetric(for: SignpostName.permutationsConcurrent) 37 | 38 | self.measure(metrics: [signpostMetric, XCTClockMetric()]) { 39 | _ = source.permutationsConcurrent(concurrentThreads: 4) 40 | } 41 | } 42 | 43 | // XCTMeasureOptions `default` option, automatically starts/stops measuring and has iterationCount of 5. 44 | // We have overriden defaultMeasureOptions to have iterationCount of 10. Note that the block is actually invoked `iterationCount` + 1 times, and the first iteration is discarded. This is done to reduce the chance that the first iteration will be an outlier. 45 | override class var defaultMeasureOptions: XCTMeasureOptions { 46 | let measureOptions = XCTMeasureOptions() 47 | measureOptions.iterationCount = 10 48 | // automatic start/stop measuring with rawValue zero. 49 | measureOptions.invocationOptions = [XCTMeasureOptions.InvocationOptions(rawValue: 0)] 50 | return measureOptions 51 | } 52 | 53 | private func signpostMetric(for name: StaticString) -> XCTOSSignpostMetric { 54 | return XCTOSSignpostMetric(subsystem: permutationLog.subsystem, category: permutationLog.category, name: String(name)) 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /XCMetricsTests/XCMetricsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCMetricsTests.swift 3 | // XCMetricsTests 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import XCTest 9 | @testable import XCMetrics 10 | 11 | ///Permuation Defination: The number of ways to choose a sample of r elements from a set of n distinct objects where order does matter and replacements are not allowed. When n = r this reduces to n!, a simple factorial of n. 12 | /// nPr = fact(n) / fact(n - r) 13 | final class XCMetricsTests: XCTestCase { 14 | let source: [Int] = [1, 2, 3, 4, 5, 6, 7] // 7! = 5040 unique arrays. 15 | 16 | // Self.defaultMeasureOptions will be used 17 | func testPerformanceFunctional() { 18 | self.measure(metrics: Self.defaultMetrics) { 19 | _ = source.permutationsOverMutationsFunctionalOptimized4() 20 | } 21 | } 22 | 23 | // Self.defaultMetrics will be measured 24 | func testPerformanceNonFunctional() { 25 | self.measure(options: Self.defaultMeasureOptions) { 26 | _ = source.permutationsOverMutations() 27 | } 28 | } 29 | 30 | func testPerformanceConcurrent() { 31 | let measureOptions = XCTMeasureOptions() 32 | if #available(iOS 14, *) { 33 | measureOptions.invocationOptions = [.manuallyStart, .manuallyStop] 34 | } else { 35 | // NOTE: .manuallyStop doesn't work on iOS 13 with some metrics 36 | measureOptions.invocationOptions = [.manuallyStart] 37 | } 38 | measureOptions.iterationCount = 10 39 | 40 | self.measure(metrics: Self.defaultMetrics, options: measureOptions) { 41 | startMeasuring() 42 | _ = source.permutationsConcurrent(concurrentThreads: 4) 43 | if #available(iOS 14, *) { 44 | stopMeasuring() 45 | } 46 | } 47 | } 48 | 49 | // By default, it returns [XCTClockMetric()] 50 | // XCTClockMetric() is to measure the number of seconds the block of code takes to execute. 51 | override class var defaultMetrics: [XCTMetric] { 52 | return [ 53 | XCTClockMetric(), 54 | XCTStorageMetric(), 55 | XCTMemoryMetric(), 56 | XCTCPUMetric(), 57 | MonotonicClockMetric() 58 | ] 59 | } 60 | 61 | // XCTMeasureOptions `default` option, automatically starts/stops measuring and has iterationCount of 5. 62 | // We have overriden defaultMeasureOptions to have iterationCount of 10. Note that the block is actually invoked `iterationCount` + 1 times, and the first iteration is discarded. This is done to reduce the chance that the first iteration will be an outlier. 63 | override class var defaultMeasureOptions: XCTMeasureOptions { 64 | let measureOptions = XCTMeasureOptions() 65 | measureOptions.iterationCount = 10 66 | // automatic start/stop measuring with rawValue zero. 67 | measureOptions.invocationOptions = [XCTMeasureOptions.InvocationOptions(rawValue: 0)] 68 | return measureOptions 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ## User settings 34 | xcuserdata/ 35 | 36 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 37 | *.xcscmblueprint 38 | *.xccheckout 39 | 40 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 41 | build/ 42 | DerivedData/ 43 | *.moved-aside 44 | *.pbxuser 45 | !default.pbxuser 46 | *.mode1v3 47 | !default.mode1v3 48 | *.mode2v3 49 | !default.mode2v3 50 | *.perspectivev3 51 | !default.perspectivev3 52 | 53 | ## Gcc Patch 54 | /*.gcno 55 | 56 | ## Obj-C/Swift specific 57 | *.hmap 58 | 59 | ## App packaging 60 | *.ipa 61 | *.dSYM.zip 62 | *.dSYM 63 | 64 | ## Playgrounds 65 | timeline.xctimeline 66 | playground.xcworkspace 67 | 68 | # Swift Package Manager 69 | # 70 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 71 | # Packages/ 72 | # Package.pins 73 | # Package.resolved 74 | # *.xcodeproj 75 | # 76 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 77 | # hence it is not needed unless you have added a package configuration file to your project 78 | # .swiftpm 79 | 80 | .build/ 81 | 82 | # CocoaPods 83 | # 84 | # We recommend against adding the Pods directory to your .gitignore. However 85 | # you should judge for yourself, the pros and cons are mentioned at: 86 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 87 | # 88 | # Pods/ 89 | # 90 | # Add this line if you want to avoid checking in source code from the Xcode workspace 91 | # *.xcworkspace 92 | 93 | # Carthage 94 | # 95 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 96 | # Carthage/Checkouts 97 | 98 | Carthage/Build/ 99 | 100 | # Accio dependency management 101 | Dependencies/ 102 | .accio/ 103 | 104 | # fastlane 105 | # 106 | # It is recommended to not store the screenshots in the git repo. 107 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 108 | # For more information about the recommended setup visit: 109 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 110 | 111 | fastlane/report.xml 112 | fastlane/Preview.html 113 | fastlane/screenshots/**/*.png 114 | fastlane/test_output 115 | 116 | # Code Injection 117 | # 118 | # After new code Injection tools there's a generated folder /iOSInjectionProject 119 | # https://github.com/johnno1962/injectionforxcode 120 | 121 | iOSInjectionProject/ 122 | -------------------------------------------------------------------------------- /XCMetrics/ScrollTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollTableViewController.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import UIKit 9 | import os.signpost 10 | 11 | final class ScrollTableViewController: UITableViewController { 12 | private lazy var scrollOSLog = OSLog(subsystem: scrollLog.subsystem, category: scrollLog.category) 13 | private lazy var navTransitionOSLog = OSLog(subsystem: navTransitionLog.subsystem, category: navTransitionLog.category) 14 | 15 | private lazy var dataSource = ((1...10).flatMap{ _ -> [String] in emojiItems }) 16 | let emojiItems = [ 17 | "😎 Cool", 18 | "♥️ Heart", 19 | "☹️ Sad", 20 | "😊 Smile", 21 | "🤔 Thinking", 22 | "😍 Love", 23 | "🤗 Hug", 24 | "🥳 Party", 25 | "😕 Confused", 26 | "👍 PlusOne", 27 | "👎 MinusOne", 28 | "🥱 Yawn", 29 | "🤝 HandShake", 30 | "🙏 Pray", 31 | "✌️ Victory", 32 | ] 33 | 34 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 35 | return 150 36 | } 37 | 38 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 39 | let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) 40 | cell.textLabel?.text = dataSource[indexPath.row] 41 | return cell 42 | } 43 | } 44 | 45 | // MARK: Signpost Events 46 | extension ScrollTableViewController { 47 | override func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { 48 | if #available(iOS 14.0, *) { 49 | os_signpost(.animationBegin, log: scrollOSLog, name: SignpostName.scrollDecelerationSignpost) 50 | } else { 51 | os_signpost(.begin, log: scrollOSLog, name: SignpostName.scrollDecelerationSignpost) 52 | } 53 | } 54 | 55 | override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 56 | os_signpost(.end, log: scrollOSLog, name: SignpostName.scrollDecelerationSignpost) 57 | } 58 | 59 | override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 60 | if #available(iOS 14.0, *) { 61 | os_signpost(.animationBegin, log: scrollOSLog, name: SignpostName.scrollDraggingSignpost) 62 | } else { 63 | os_signpost(.begin, log: scrollOSLog, name: SignpostName.scrollDraggingSignpost) 64 | } 65 | } 66 | 67 | override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 68 | os_signpost(.end, log: scrollOSLog, name: SignpostName.scrollDraggingSignpost) 69 | } 70 | 71 | override func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { 72 | if #available(iOS 14.0, *) { 73 | os_signpost(.animationBegin, log: navTransitionOSLog, name: SignpostName.scrollViewNavTransition) 74 | } else { 75 | os_signpost(.begin, log: navTransitionOSLog, name: SignpostName.scrollViewNavTransition) 76 | } 77 | } 78 | 79 | override func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { 80 | os_signpost(.end, log: navTransitionOSLog, name: SignpostName.scrollViewNavTransition) 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /XCMetricsTests/XCTOSSignpostMetric+Hook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTOSSignpostMetric+Hooks.swift 3 | // XCMetricsTests 4 | // 5 | // Created by Soaurabh Kakkar on 21/08/20. 6 | // 7 | 8 | import XCTest 9 | import InterposeKit 10 | 11 | func signpost(from metric: XCTMetric) -> Signpost { 12 | guard metric is XCTApplicationLaunchMetric || metric is XCTOSSignpostMetric, let metricObject = metric as? NSObject else { 13 | fatalError("Expected AppLaunch or Signpost Metric!") 14 | } 15 | let underlyingMetricSelector = NSSelectorFromString("_underlyingMetric") 16 | let underlyingMetricIMP = metricObject.method(for: underlyingMetricSelector) 17 | let underlyingMetricMethod = unsafeBitCast(underlyingMetricIMP, to: (@convention(c) (NSObject, Selector) -> NSObject).self) 18 | let signpostUnderlyingMetric = underlyingMetricMethod(metricObject, underlyingMetricSelector) 19 | 20 | let subsystemSelector = NSSelectorFromString("subsystem") 21 | let subsystemIMP = signpostUnderlyingMetric.method(for: subsystemSelector) 22 | let subsystemMethod = unsafeBitCast(subsystemIMP, to: (@convention(c) (NSObject, Selector) -> String).self) 23 | let subsystem = subsystemMethod(signpostUnderlyingMetric, subsystemSelector) 24 | 25 | let categorySelector = NSSelectorFromString("category") 26 | let categoryIMP = signpostUnderlyingMetric.method(for: categorySelector) 27 | let categoryMethod = unsafeBitCast(categoryIMP, to: (@convention(c) (NSObject, Selector) -> String).self) 28 | let category = categoryMethod(signpostUnderlyingMetric, categorySelector) 29 | 30 | let nameSelector = NSSelectorFromString("name") 31 | let nameIMP = signpostUnderlyingMetric.method(for: nameSelector) 32 | let nameMethod = unsafeBitCast(nameIMP, to: (@convention(c) (NSObject, Selector) -> String).self) 33 | let name = nameMethod(signpostUnderlyingMetric, nameSelector) 34 | 35 | return Signpost(subsystem: subsystem, category: category, name: name) 36 | } 37 | 38 | func hookedValue(for signpost: Signpost, with measurements: [XCTPerformanceMeasurement]) -> [XCTPerformanceMeasurement] { 39 | if measurements.count >= Measurements.value(for: signpost).count { 40 | Measurements.update(measurements: measurements, for: signpost) 41 | } 42 | if #available(iOS 14, *) { 43 | return Measurements.value(for: signpost) 44 | } else { 45 | guard let lastMeasurement = Measurements.value(for: signpost).last else { 46 | return [] 47 | } 48 | return [lastMeasurement] 49 | } 50 | } 51 | 52 | extension XCTOSSignpostMetric { 53 | static var signpostHook: Interpose? { 54 | try? Interpose(XCTOSSignpostMetric.self) { 55 | try $0.prepareHook( 56 | #selector(XCTOSSignpostMetric.reportMeasurements(from:to:)), 57 | methodSignature: (@convention(c) (XCTOSSignpostMetric, Selector, XCTPerformanceMeasurementTimestamp, XCTPerformanceMeasurementTimestamp) -> [XCTPerformanceMeasurement]).self, 58 | hookSignature: (@convention(block) (XCTOSSignpostMetric, XCTPerformanceMeasurementTimestamp, XCTPerformanceMeasurementTimestamp) -> [XCTPerformanceMeasurement]).self) { 59 | store in { `self`, startTime, endTime in 60 | let measurements = store.original(`self`, store.selector, startTime, endTime) 61 | return hookedValue(for: signpost(from: `self`), with: measurements) 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /XCMetricsUITests/XCMetricsUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCMetricsUITests.swift 3 | // XCMetricsUITests 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import XCTest 9 | 10 | final class XCMetricsUITests: XCSignpostHookedTestCase { 11 | 12 | typealias MetricAlloc = @convention(c) (XCTMetric.Type, Selector) -> NSObject 13 | typealias MetricInitWithProcessName = @convention(c) (NSObject, Selector, String) -> XCTMetric 14 | 15 | override func setUpWithError() throws { 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | } 19 | 20 | func testImageProcessingPerformance() { 21 | let app = XCUIApplication() 22 | 23 | app.launch() 24 | app.staticTexts["Image Processing"].tap() 25 | let processButton = app.buttons["Process"] 26 | 27 | let measureOptions = XCTMeasureOptions() 28 | measureOptions.iterationCount = 10 29 | 30 | let signpostMetric = self.signpostMetric(for: SignpostName.processImage) 31 | 32 | // NOTE: XCTClockMetric doesn't make much sense in UITest(s) because it will target the time of execution in current process, not the process targetted by XCUIApplication proxy. 33 | // NOTE: XCTCPUMetric(application: app) and XCTMemoryMetric(application: app) won’t work on iOS 14. However, It will work when checked on iOS 13.6, I still doubt their accuracy, so we should discard any outlier cpu/memory values. 34 | let memoryMetric: XCTMetric 35 | let cpuMetric: XCTMetric 36 | if #available(iOS 14, *) { 37 | memoryMetric = self.initWithProcessName(for: XCTMemoryMetric.self, processName: "XCMetrics") 38 | cpuMetric = self.initWithProcessName(for: XCTCPUMetric.self, processName: "XCMetrics") 39 | } else { 40 | memoryMetric = XCTMemoryMetric(application: app) 41 | cpuMetric = XCTCPUMetric(application: app) 42 | } 43 | 44 | measure(metrics: [memoryMetric, cpuMetric, signpostMetric, XCTStorageMetric(application: app)], options: measureOptions) { 45 | processButton.tap() 46 | } 47 | } 48 | 49 | //initWithProcessName: is a private API, used to ensure support for iOS 14. 50 | private func initWithProcessName(for type: XCTMetric.Type, processName: String) -> XCTMetric { 51 | guard type is XCTMemoryMetric.Type || 52 | type is XCTCPUMetric.Type || 53 | type is XCTStorageMetric.Type else { 54 | fatalError("CPU, Storage and Memory metric implements initWithProcessName:") 55 | } 56 | let allocSelector = NSSelectorFromString("alloc") 57 | let allocIMP = method_getImplementation(class_getClassMethod(type.self, allocSelector)!) 58 | let allocMethod = unsafeBitCast(allocIMP, to: MetricAlloc.self) 59 | let result = allocMethod(type.self, allocSelector) 60 | 61 | let initSelector = NSSelectorFromString("initWithProcessName:") 62 | let methodIMP = result.method(for: initSelector) 63 | let initMethod = unsafeBitCast(methodIMP, to: MetricInitWithProcessName.self) 64 | return initMethod(result, initSelector, processName) 65 | } 66 | 67 | private func signpostMetric(for name: StaticString) -> XCTOSSignpostMetric { 68 | return XCTOSSignpostMetric(subsystem: processImageLog.subsystem, category: processImageLog.category, name: String(name)) 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /XCMetrics.xcodeproj/xcshareddata/xcschemes/XCMetrics.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 31 | 32 | 33 | 34 | 35 | 40 | 41 | 44 | 45 | 46 | 47 | 59 | 61 | 67 | 68 | 69 | 70 | 76 | 78 | 84 | 85 | 86 | 87 | 89 | 90 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /XCMetricsTests/XCTPerformanceMetric+All.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTPerformanceMetric+All.swift 3 | // XCMetricsTests 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | // wallClockTime: is the time that a clock on the wall (or a stopwatch in hand) would measure as having elapsed between the start of the process and 'now'. 9 | 10 | // User CPU Time(userTime): Amount of time the processor worked on the specific program or the amount of time spent in user code. 11 | 12 | // System CPU Time(systemTime): Amount of time the processor worked on operating system's functions connected to that specific program or the amount of time spent in kernel code(servicing system calls). 13 | 14 | // runTime: Sum of userTime & systemTime 15 | 16 | // Note: wall-clock time is not the number of seconds that the process has spent on the CPU. it is the elapsed time, including time spent waiting for its turn on the CPU (while other processes get to run). 17 | 18 | // SideNote: 19 | // 1. On a single core machine wall-clock time will always be greater than the cpu-time. 20 | // 2. iOS 14 always reports systemTime as zero irrespective of system/kernal calls. 21 | 22 | // VM Allocations: System reserves a VM memory for every app/process and as a developer you can influence this memory usage by creating less images, reducing the number of malloc calls etc. 23 | // For instance, when you allocate a large CGImage, a tiny object is allocated on the heap, but the many megabytes of image data is allocated in a separate VM region. In an image-heavy application, your heap may be relatively small but your VM regions may be rather large. In that case, you'd need to take a closer look at the size and number of images your application has loaded at any given moment. 24 | 25 | // Heap Allocations: Consist of the memory allocations your application makes i.e. all your Swift and Objective-C objects. 26 | 27 | // Persistent Memory: objects that currently exist in memory. 28 | // Transient/Temporary Memory: Objects that have existed but have since been deallocated. 29 | // Persistent objects are using up memory, transient objects have had their memory released. 30 | 31 | import XCTest 32 | 33 | public extension XCTPerformanceMetric { 34 | static let userTime = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_UserTime") 35 | static let runTime = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_RunTime") 36 | static let systemTime = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_SystemTime") 37 | static let transientVMKB = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_TransientVMAllocationsKilobytes") 38 | static let temporaryHeapKB = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_TemporaryHeapAllocationsKilobytes") 39 | static let highWatermarkVM = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_HighWaterMarkForVMAllocations") 40 | static let totalHeapKB = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_TotalHeapAllocationsKilobytes") 41 | static let persistentVM = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_PersistentVMAllocations") 42 | static let persistentHeap = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_PersistentHeapAllocations") 43 | static let transientHeapKB = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_TransientHeapAllocationsKilobytes") 44 | static let persistentHeapNodes = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_PersistentHeapAllocationsNodes") 45 | static let highWatermarkHeap = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_HighWaterMarkForHeapAllocations") 46 | static let transientHeapNodes = XCTPerformanceMetric(rawValue: "com.apple.XCTPerformanceMetric_TransientHeapAllocationsNodes") 47 | 48 | static let all: [XCTPerformanceMetric] = [.wallClockTime, 49 | .userTime, 50 | .runTime, 51 | .systemTime, 52 | .transientVMKB, 53 | .temporaryHeapKB, 54 | .highWatermarkVM, 55 | .totalHeapKB, 56 | .persistentVM, 57 | .persistentHeap, 58 | .transientHeapKB, 59 | .persistentHeapNodes, 60 | .highWatermarkHeap, 61 | .transientHeapNodes] 62 | } 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | XCMetrics: Measure performance metrics using XCTest 2 | ====================================== 3 | 4 | ```XCMetrics``` is a reference implementation to accurately measure performance metrics of your code using Unit and UI tests. It provides detailed examples covering both old( i.e. XCTPerformanceMetric) & the new(i.e. XCTMetric) performance-testing system. It also fixes bugs/peculiarities in the XCTest performance APIs with runtime hooks(i.e. Monkey Patching), making it more stable & consistent. 5 | 6 | ## Purpose 7 | Performance testing using XCTest framework(i.e. XCTMetric confirming types) have some rough edges i.e. bugs/peculiarities. This project aims to fix all those bugs in the performance APIs using Swizzling, making a consistent experience on both iOS 13 & 14. Here's the list of open issues and radars: 8 | 1. [Default Launch Performance Test Crashes Occasionally](http://openradar.appspot.com/radar?id=4976981179367424) 9 | 2. [Getting error measuring signpost metrics in XCUITests](https://developer.apple.com/forums/thread/130576) 10 | 3. [XCTests crash randomly in parallel testing](https://developer.apple.com/forums/thread/125536) 11 | 4. [XCTOSSignpostMetric doesn’t detect signposts logged by static library](https://developer.apple.com/forums/thread/127458) 12 | 5. [Lost connection to test manager service](https://openradar.appspot.com/24224991) 13 | 6. [Lastly, Lack of performance tests documentation](https://developer.apple.com/forums/thread/132060) 14 | 15 | ## Identifying Performance Bottlenecks & Regressions 16 | Performance tests helps in identifying the impact of your code in terms of various metrics like CPU, Clock, Memory, Storage, Signpost, AppLaunch and more. Capturing performance metrics in your CI pipeline helps in identifying performance bottlenecks and regressions produced by your code over-time. If your performance metrics are going off, then Instruments can help in analysing the root cause. 17 | 18 | ## What's Inside 19 | Detailed examples of performance testing covering the usage of following metrics:
20 | 1. ```XCTCPUMetric``` to record information about CPU activity during a performance test. 21 | 2. ```XCTClockMetric``` to record the time that elapses during a performance test. 22 | 3. ```XCTMemoryMetric``` to record the physical memory that a performance test uses. 23 | 4. ```XCTOSSignpostMetric``` to record the time that a performance test spends executing a signposted region of code. 24 | 5. ```XCTStorageMetric``` to record the amount of data that a performance test logically writes to storage. 25 | 6. ```XCTApplicationLaunchMetric``` to record the application launch duration for a performance test. 26 | 7. ```MonotonicClockMetric``` to record the time(in nanoseconds) that elapses during a performance test, implemented as a custom ```XCTMetric```. 27 | 8. ```scrollDecelerationMetric``` to record scroll deceleration animations. 28 | 9. ```scrollDraggingMetric``` to record scroll-dragging animations. 29 | 10. ```navigationTransitionMetric``` to record the duration of navigation transitions between views. 30 | 11. ```wallClockTime``` to record the time in seconds to execute a block of code. 31 | 12. And, all private metrics of ```XCTPerformanceMetric``` system. 32 | 33 | ## Tools & Support 34 | 1. Xcode 12.0 Beta 6, Build version 12A8189n. 35 | 2. Swift 5.3 and later. 36 | 3. iOS 13 and later. 37 | 38 | ## Dependencies 39 | ```XCMetrics``` uses [InterposeKit](https://github.com/steipete/InterposeKit) for adding runtime hooks(i.e. swizzling). 40 | 41 | ## Contributing 42 | XCMetrics welcomes contributions in the form of GitHub issues and pull-requests:
43 | 1. For PRs, please add the purpose and summary of your changes in the PR description.
44 | 2. For issues, please add the steps to reproduce and tools/OS version.
45 | 3. Make sure you test your contributions.
46 | 4. [Ideas and suggestions](https://twitter.com/soaurabh) are very welcome! 47 | 48 | By submitting a pull request, you represent that you have the right to license your contribution to Soaurabh Kakkar and the community, and agree by submitting the patch that your contributions are licensed under the XCMetrics project license. 49 | 50 | ## License 51 | XCMetrics is licensed under the [MIT License](LICENSE.md) 52 | 53 | ## References 54 | https://developer.apple.com/documentation/os/logging/recording_performance_data
55 | https://developer.apple.com/documentation/xctest/performance_tests
56 | https://developer.apple.com/documentation/xctest/xctestcase
57 | https://indiestack.com/2018/02/xcodes-secret-performance-tests
58 | https://developer.apple.com/videos/play/wwdc2018/405
59 | https://developer.apple.com/videos/play/wwdc2019/417
60 | https://developer.apple.com/videos/play/wwdc2020/10077 61 | # 62 | Building an end to end performance tests CI pipeline? Give [XCTool](https://github.com/SoaurabhK/XCTool) a try! 63 | -------------------------------------------------------------------------------- /XCMetrics/AtomicArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicArray.swift 3 | // XCMetricsTests 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | // Reference: https://stackoverflow.com/a/54565351 8 | 9 | import Foundation 10 | 11 | struct AtomicArray: RangeReplaceableCollection { 12 | 13 | typealias Element = T 14 | typealias Index = Int 15 | typealias SubSequence = AtomicArray 16 | typealias Indices = Range 17 | fileprivate var array: Array 18 | var startIndex: Int { return array.startIndex } 19 | var endIndex: Int { return array.endIndex } 20 | var indices: Range { return array.indices } 21 | 22 | func index(after i: Int) -> Int { return array.index(after: i) } 23 | 24 | private var semaphore = DispatchSemaphore(value: 1) 25 | fileprivate func _wait() { semaphore.wait() } 26 | fileprivate func _signal() { semaphore.signal() } 27 | } 28 | 29 | // MARK: - Instance Methods 30 | 31 | extension AtomicArray { 32 | 33 | init(_ elements: S) where S : Sequence, AtomicArray.Element == S.Element { 34 | array = Array(elements) 35 | } 36 | 37 | init() { self.init([]) } 38 | 39 | init(repeating repeatedValue: AtomicArray.Element, count: Int) { 40 | let array = Array(repeating: repeatedValue, count: count) 41 | self.init(array) 42 | } 43 | } 44 | 45 | // MARK: - Instance Methods 46 | 47 | extension AtomicArray { 48 | 49 | public mutating func append(_ newElement: AtomicArray.Element) { 50 | _wait(); defer { _signal() } 51 | array.append(newElement) 52 | } 53 | 54 | public mutating func append(contentsOf newElements: S) where S : Sequence, AtomicArray.Element == S.Element { 55 | _wait(); defer { _signal() } 56 | array.append(contentsOf: newElements) 57 | } 58 | 59 | func filter(_ isIncluded: (AtomicArray.Element) throws -> Bool) rethrows -> AtomicArray { 60 | _wait(); defer { _signal() } 61 | let subArray = try array.filter(isIncluded) 62 | return AtomicArray(subArray) 63 | } 64 | 65 | public mutating func insert(_ newElement: AtomicArray.Element, at i: AtomicArray.Index) { 66 | _wait(); defer { _signal() } 67 | array.insert(newElement, at: i) 68 | } 69 | 70 | mutating func insert(contentsOf newElements: S, at i: AtomicArray.Index) where S : Collection, AtomicArray.Element == S.Element { 71 | _wait(); defer { _signal() } 72 | array.insert(contentsOf: newElements, at: i) 73 | } 74 | 75 | mutating func popLast() -> AtomicArray.Element? { 76 | _wait(); defer { _signal() } 77 | return array.popLast() 78 | } 79 | 80 | @discardableResult mutating func remove(at i: AtomicArray.Index) -> AtomicArray.Element { 81 | _wait(); defer { _signal() } 82 | return array.remove(at: i) 83 | } 84 | 85 | mutating func removeAll() { 86 | _wait(); defer { _signal() } 87 | array.removeAll() 88 | } 89 | 90 | mutating func removeAll(keepingCapacity keepCapacity: Bool) { 91 | _wait(); defer { _signal() } 92 | array.removeAll() 93 | } 94 | 95 | mutating func removeAll(where shouldBeRemoved: (AtomicArray.Element) throws -> Bool) rethrows { 96 | _wait(); defer { _signal() } 97 | try array.removeAll(where: shouldBeRemoved) 98 | } 99 | 100 | @discardableResult mutating func removeFirst() -> AtomicArray.Element { 101 | _wait(); defer { _signal() } 102 | return array.removeFirst() 103 | } 104 | 105 | mutating func removeFirst(_ k: Int) { 106 | _wait(); defer { _signal() } 107 | array.removeFirst(k) 108 | } 109 | 110 | @discardableResult mutating func removeLast() -> AtomicArray.Element { 111 | _wait(); defer { _signal() } 112 | return array.removeLast() 113 | } 114 | 115 | mutating func removeLast(_ k: Int) { 116 | _wait(); defer { _signal() } 117 | array.removeLast(k) 118 | } 119 | 120 | @inlinable public func forEach(_ body: (Element) throws -> Void) rethrows { 121 | _wait(); defer { _signal() } 122 | try array.forEach(body) 123 | } 124 | 125 | mutating func removeFirstIfExist(where shouldBeRemoved: (AtomicArray.Element) throws -> Bool) { 126 | _wait(); defer { _signal() } 127 | guard let index = try? array.firstIndex(where: shouldBeRemoved) else { return } 128 | array.remove(at: index) 129 | } 130 | 131 | mutating func removeSubrange(_ bounds: Range) { 132 | _wait(); defer { _signal() } 133 | array.removeSubrange(bounds) 134 | } 135 | 136 | mutating func replaceSubrange(_ subrange: R, with newElements: C) where C : Collection, R : RangeExpression, T == C.Element, AtomicArray.Index == R.Bound { 137 | _wait(); defer { _signal() } 138 | array.replaceSubrange(subrange, with: newElements) 139 | } 140 | 141 | mutating func reserveCapacity(_ n: Int) { 142 | _wait(); defer { _signal() } 143 | array.reserveCapacity(n) 144 | } 145 | 146 | public var count: Int { 147 | _wait(); defer { _signal() } 148 | return array.count 149 | } 150 | 151 | public var isEmpty: Bool { 152 | _wait(); defer { _signal() } 153 | return array.isEmpty 154 | } 155 | 156 | public var first: AtomicArray.Element? { 157 | _wait(); defer { _signal() } 158 | return array.first 159 | } 160 | } 161 | 162 | // MARK: - Get/Set 163 | 164 | extension AtomicArray { 165 | 166 | // Single action 167 | 168 | func get() -> [T] { 169 | _wait(); defer { _signal() } 170 | return array 171 | } 172 | 173 | mutating func set(array: [T]) { 174 | _wait(); defer { _signal() } 175 | self.array = array 176 | } 177 | 178 | // Multy actions 179 | 180 | mutating func get(closure: ([T])->()) { 181 | _wait(); defer { _signal() } 182 | closure(array) 183 | } 184 | 185 | mutating func set(closure: ([T]) -> ([T])) { 186 | _wait(); defer { _signal() } 187 | array = closure(array) 188 | } 189 | } 190 | 191 | // MARK: - Subscripts 192 | 193 | extension AtomicArray { 194 | 195 | subscript(bounds: Range) -> AtomicArray.SubSequence { 196 | get { 197 | _wait(); defer { _signal() } 198 | return AtomicArray(array[bounds]) 199 | } 200 | } 201 | 202 | subscript(bounds: AtomicArray.Index) -> AtomicArray.Element { 203 | get { 204 | _wait(); defer { _signal() } 205 | return array[bounds] 206 | } 207 | set(value) { 208 | _wait(); defer { _signal() } 209 | array[bounds] = value 210 | } 211 | } 212 | } 213 | 214 | // MARK: - Operator Functions 215 | 216 | extension AtomicArray { 217 | 218 | static func + (lhs: Other, rhs: AtomicArray) -> AtomicArray where Other : Sequence, AtomicArray.Element == Other.Element { 219 | return AtomicArray(lhs + rhs.get()) 220 | } 221 | 222 | static func + (lhs: AtomicArray, rhs: Other) -> AtomicArray where Other : Sequence, AtomicArray.Element == Other.Element { 223 | return AtomicArray(lhs.get() + rhs) 224 | } 225 | 226 | static func + (lhs: AtomicArray, rhs: Other) -> AtomicArray where Other : RangeReplaceableCollection, AtomicArray.Element == Other.Element { 227 | return AtomicArray(lhs.get() + rhs) 228 | } 229 | 230 | static func + (lhs: AtomicArray, rhs: AtomicArray) -> AtomicArray { 231 | return AtomicArray(lhs.get() + rhs.get()) 232 | } 233 | 234 | static func += (lhs: inout AtomicArray, rhs: Other) where Other : Sequence, AtomicArray.Element == Other.Element { 235 | lhs._wait(); defer { lhs._signal() } 236 | lhs.array += rhs 237 | } 238 | } 239 | 240 | // MARK: - CustomStringConvertible 241 | 242 | extension AtomicArray: CustomStringConvertible { 243 | var description: String { 244 | _wait(); defer { _signal() } 245 | return "\(array)" 246 | } 247 | } 248 | 249 | // MARK: - Equatable 250 | 251 | extension AtomicArray where Element : Equatable { 252 | 253 | func split(separator: Element, maxSplits: Int, omittingEmptySubsequences: Bool) -> [ArraySlice] { 254 | _wait(); defer { _signal() } 255 | return array.split(separator: separator, maxSplits: maxSplits, omittingEmptySubsequences: omittingEmptySubsequences) 256 | } 257 | 258 | func firstIndex(of element: Element) -> Int? { 259 | _wait(); defer { _signal() } 260 | return array.firstIndex(of: element) 261 | } 262 | 263 | func lastIndex(of element: Element) -> Int? { 264 | _wait(); defer { _signal() } 265 | return array.lastIndex(of: element) 266 | } 267 | 268 | func starts(with possiblePrefix: PossiblePrefix) -> Bool where PossiblePrefix : Sequence, Element == PossiblePrefix.Element { 269 | _wait(); defer { _signal() } 270 | return array.starts(with: possiblePrefix) 271 | } 272 | 273 | func elementsEqual(_ other: OtherSequence) -> Bool where OtherSequence : Sequence, Element == OtherSequence.Element { 274 | _wait(); defer { _signal() } 275 | return array.elementsEqual(other) 276 | } 277 | 278 | func contains(_ element: Element) -> Bool { 279 | _wait(); defer { _signal() } 280 | return array.contains(element) 281 | } 282 | 283 | static func != (lhs: AtomicArray, rhs: AtomicArray) -> Bool { 284 | lhs._wait(); defer { lhs._signal() } 285 | rhs._wait(); defer { rhs._signal() } 286 | return lhs.array != rhs.array 287 | } 288 | 289 | static func == (lhs: AtomicArray, rhs: AtomicArray) -> Bool { 290 | lhs._wait(); defer { lhs._signal() } 291 | rhs._wait(); defer { rhs._signal() } 292 | return lhs.array == rhs.array 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /XCMetrics/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /XCMetrics/PermutationAlgorithms.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermutationAlgorithms.swift 3 | // XCMetrics 4 | // 5 | // Created by Soaurabh Kakkar on 20/08/20. 6 | // 7 | 8 | import Foundation 9 | import os.signpost 10 | 11 | // MARK: Core 12 | private let permutationOSLog = OSLog(subsystem: permutationLog.subsystem, category: permutationLog.category) 13 | 14 | extension Int { 15 | var factorial: Int { 16 | guard self > 0 else { return 0 } 17 | return (1...self).reduce(1, *) 18 | } 19 | } 20 | 21 | extension Array { 22 | func split(by: Int) -> [[Element]] { 23 | return stride(from: 0, to: count, by: by).map { 24 | guard count > $0 + by else { 25 | guard count > $0 else { 26 | return [] 27 | } 28 | return Array(self[$0.. Bool) rethrows -> Iterator.Element? { 38 | 39 | for element in self { 40 | if try predicate(element) { 41 | return element 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | func index(where predicate: (Iterator.Element) throws -> Bool) rethrows -> Index? { 48 | var i = startIndex 49 | while i <= endIndex { 50 | if try predicate(self[i]) { 51 | return i 52 | } 53 | i = index(after: i) 54 | } 55 | return nil 56 | } 57 | } 58 | 59 | extension Array { 60 | 61 | /// Reverses the elements of the collection within a given range of indices. 62 | /// 63 | /// - Parameter indices: A range of valid indices of the collection, 64 | /// the elements of which will be reversed. 65 | /// 66 | mutating func reverse(indices: Range) { 67 | 68 | if indices.isEmpty { return } 69 | 70 | var low = indices.lowerBound 71 | var high = index(before: indices.upperBound) 72 | 73 | while low < high { 74 | swapAt(low, high) 75 | formIndex(after: &low) 76 | formIndex(before: &high) 77 | } 78 | } 79 | } 80 | 81 | extension Array { 82 | mutating func filterSingle(_ isIncluded: (Element) -> Bool) { 83 | for i in (0.. (element: Element, rest: [Element]) { 92 | if at == 0 { return (first!, Array(self[1.. [(element: Element, rest: [Element])] { 98 | return (0.. [[Element]] { 106 | guard count != 0 else { return [] } 107 | guard count != 1 else { return [self] } 108 | var result: [[Element]] = [] 109 | forEach { (element) in 110 | result.append(contentsOf: filter { $0 != element }.permutations().map { [element] + $0 } ) 111 | } 112 | return result 113 | } 114 | 115 | func permutationsOptimized() -> [[Element]] { 116 | guard count != 0 else { return [] } 117 | guard count != 1 else { return [self] } 118 | return permutationsOptimizedRecursion(prepending: []) 119 | } 120 | 121 | func permutationsOptimized2() -> [[Element]] { 122 | guard count != 0 else { return [] } 123 | guard count != 1 else { return [self] } 124 | return permutationsOptimizedRecursion2(prepending: []) 125 | } 126 | 127 | func permutationsOptimized3() -> [[Element]] { 128 | guard count != 0 else { return [] } 129 | guard count != 1 else { return [self] } 130 | return permutationsOptimizedRecursion3(prepending: []) 131 | } 132 | 133 | func permutationsOptimized4() -> [[Element]] { 134 | guard count != 0 else { return [] } 135 | guard count != 1 else { return [self] } 136 | return permutationsOptimizedRecursion4(prepending: []) 137 | } 138 | 139 | private func permutationsOptimizedRecursion(prepending: [Element]) -> [[Element]] { 140 | guard count != 1 else { return [prepending + self] } 141 | var result: [[Element]] = [] 142 | forEach { (element) in 143 | result.append(contentsOf: filter { $0 != element }.permutationsOptimizedRecursion(prepending: prepending + [element]) ) 144 | } 145 | return result 146 | } 147 | 148 | private func permutationsOptimizedRecursion2(prepending: [Element]) -> [[Element]] { 149 | guard count != 1 else { return [prepending + self] } 150 | var result: [[Element]] = [] 151 | forEach { (element) in 152 | let idx = index { $0 == element }! 153 | var copy = self 154 | copy.removeSubrange((idx...idx)) 155 | result.append(contentsOf: copy.permutationsOptimizedRecursion2(prepending: prepending + [element]) ) 156 | } 157 | return result 158 | } 159 | 160 | private func permutationsOptimizedRecursion3(prepending: [Element]) -> [[Element]] { 161 | guard count != 1 else { return [prepending + self] } 162 | var result: [[Element]] = [] 163 | forEach { (element) in 164 | var copy = self 165 | copy.filterSingle { $0 != element } 166 | result.append(contentsOf: copy.permutationsOptimizedRecursion3(prepending: prepending + [element]) ) 167 | } 168 | return result 169 | } 170 | 171 | private func permutationsOptimizedRecursion4(prepending: [Element]) -> [[Element]] { 172 | guard count != 1 else { return [prepending + self] } 173 | return generateSlices().map { 174 | $0.rest.permutationsOptimizedRecursion4(prepending: prepending + [$0.element]).flatMap { $0 } 175 | } 176 | } 177 | 178 | 179 | func nonRecursivePermutations() -> [[Element]] { 180 | guard count != 0 else { return [] } 181 | guard count != 1 else { return [self] } 182 | let flattenSequenceLength = count * count.factorial 183 | let flattenSequence = (0.. [[Element]] { 188 | var mutatingSelf = self 189 | var result: [[Element]] = [] 190 | var c: [Int] = .init(repeating: 0, count: count) 191 | var o: [Int] = .init(repeating: 1, count: count) 192 | for _ in 0 ... count.factorial - 1 { 193 | result.append(mutatingSelf) 194 | var s = 0 195 | for j in (0 ... count - 1).reversed() { 196 | let q = c[j] + o[j] 197 | if q > -1 { 198 | if q != j + 1 { 199 | mutatingSelf.swapAt(j - c[j] + s, j - q + s) 200 | c[j] = q 201 | break 202 | } 203 | s = s + 1 204 | } 205 | o[j] = -o[j] 206 | } 207 | } 208 | return result 209 | } 210 | } 211 | 212 | extension Array where Element: Comparable { 213 | 214 | /// Replaces the array by the next permutation of its elements in lexicographic 215 | /// order. 216 | /// 217 | /// It uses the "Algorithm L (Lexicographic permutation generation)" from 218 | /// Donald E. Knuth, "GENERATING ALL PERMUTATIONS" 219 | /// http://www-cs-faculty.stanford.edu/~uno/fasc2b.ps.gz 220 | /// 221 | /// - Returns: `true` if there was a next permutation, and `false` otherwise 222 | /// (i.e. if the array elements were in descending order). 223 | 224 | mutating func permute() -> Bool { 225 | 226 | // Nothing to do for empty or single-element arrays: 227 | if count <= 1 { 228 | return false 229 | } 230 | 231 | // L2: Find last j such that self[j] < self[j+1]. Terminate if no such j 232 | // exists. 233 | var j = count - 2 234 | while j >= 0 && self[j] >= self[j+1] { 235 | j -= 1 236 | } 237 | if j == -1 { 238 | return false 239 | } 240 | 241 | // L3: Find last l such that self[j] < self[l], then exchange elements j and l: 242 | var l = count - 1 243 | while self[j] >= self[l] { 244 | l -= 1 245 | } 246 | self.swapAt(j, l) 247 | 248 | // L4: Reverse elements j+1 ... count-1: 249 | var lo = j + 1 250 | var hi = count - 1 251 | while lo < hi { 252 | self.swapAt(lo, hi) 253 | lo += 1 254 | hi -= 1 255 | } 256 | return true 257 | } 258 | 259 | mutating func permuteFunctional() -> Bool { 260 | 261 | // Nothing to do for empty or single-element arrays: 262 | if count <= 1 { 263 | return false 264 | } 265 | 266 | // L2: Find last j such that self[j] < self[j+1]. Terminate if no such j 267 | // exists. 268 | guard let j = indices.reversed().dropFirst() 269 | .first(where: { self[$0] < self[$0 + 1] }) 270 | else { return false } 271 | 272 | // L3: Find last l such that self[j] < self[l], then exchange elements j and l: 273 | let l = indices.reversed() 274 | .first(where: { self[j] < self[$0] })! 275 | swapAt(j, l) 276 | 277 | // L4: Reverse elements j+1 ... count-1: 278 | replaceSubrange(j+1.. Bool { 283 | 284 | // Nothing to do for empty or single-element arrays: 285 | if count <= 1 { 286 | return false 287 | } 288 | 289 | // L2: Find last j such that self[j] <= self[j+1]. Terminate if no such j 290 | // exists. 291 | guard let j = indices.reversed().dropFirst() 292 | .first(where: { self[$0] <= self[$0 + 1] }) 293 | else { return false } 294 | 295 | // L3: Find last l such that self[j] <= self[l], then exchange elements j and l: 296 | let l = indices.reversed() 297 | .first(where: { self[j] <= self[$0] })! 298 | swapAt(j, l) 299 | 300 | // L4: Reverse elements j+1 ... count-1: 301 | self[j+1.. Bool { 306 | 307 | // Nothing to do for empty or single-element arrays: 308 | if count <= 1 { 309 | return false 310 | } 311 | 312 | // L2: Find last j such that self[j] <= self[j+1]. Terminate if no such j 313 | // exists. 314 | guard let j = indices.reversed().dropFirst() 315 | .first(where: { self[$0] <= self[$0 + 1] }) 316 | else { return false } 317 | 318 | // L3: Find last l such that self[j] <= self[l], then exchange elements j and l: 319 | let l = indices.reversed() 320 | .first(where: { self[j] <= self[$0] })! 321 | swapAt(j, l) 322 | 323 | // L4: Reverse elements j+1 ... count-1: 324 | self.reverse(indices: j+1.. Bool { 329 | 330 | // Nothing to do for empty or single-element arrays: 331 | if count <= 1 { 332 | return false 333 | } 334 | 335 | // L2: Find last j such that self[j] <= self[j+1]. Terminate if no such j 336 | // exists. 337 | guard let j = indices.reversed().dropFirst() 338 | .first(whereMagic: { self[$0] <= self[$0 + 1] }) 339 | else { return false } 340 | 341 | // L3: Find last l such that self[j] <= self[l], then exchange elements j and l: 342 | let l = indices.reversed() 343 | .first(whereMagic: { self[j] <= self[$0] })! 344 | swapAt(j, l) 345 | 346 | // L4: Reverse elements j+1 ... count-1: 347 | self.reverse(indices: j+1.. Bool { 352 | 353 | // L2: Find last j such that self[j] <= self[j+1]. Terminate if no such j 354 | // exists. 355 | guard let j = indices.reversed().dropFirst() 356 | .first(whereMagic: { self[$0] <= self[$0 + 1] }) 357 | else { return false } 358 | 359 | // L3: Find last l such that self[j] <= self[l], then exchange elements j and l: 360 | let l = indices.reversed() 361 | .first(whereMagic: { self[j] <= self[$0] })! 362 | swapAt(j, l) 363 | 364 | // L4: Reverse elements j+1 ... count-1: 365 | self.reverse(indices: Range(uncheckedBounds: (lower: j+1, upper: count))) 366 | return true 367 | } 368 | 369 | func permutationsOverMutations() -> [[Element]] { 370 | os_signpost(.begin, log: permutationOSLog, name: SignpostName.permutationsOverMutations) 371 | var copyOfSelf = self 372 | let result = Array<[Element]>(repeating: copyOfSelf, count: count.factorial) 373 | let output = result.map { _ -> [Element] in defer { let _ = copyOfSelf.permute() }; return copyOfSelf } 374 | os_signpost(.end, log: permutationOSLog, name: SignpostName.permutationsOverMutations) 375 | return output 376 | } 377 | 378 | func permutationsOverMutationsWithAppendAlloc() -> [[Element]] { 379 | var copyOfSelf = self 380 | var result: [[Element]] = [copyOfSelf] 381 | while copyOfSelf.permute() { result.append(copyOfSelf) } 382 | return result 383 | } 384 | 385 | func permutationsOverMutationsFunctional() -> [[Element]] { 386 | var copyOfSelf = self 387 | let result = Array<[Element]>(repeating: copyOfSelf, count: count.factorial) 388 | return result.map { _ in defer { let _ = copyOfSelf.permuteFunctional() }; return copyOfSelf } 389 | } 390 | 391 | func permutationsOverMutationsFunctionalWithAppendAlloc() -> [[Element]] { 392 | var copyOfSelf = self 393 | var result: [[Element]] = [copyOfSelf] 394 | while copyOfSelf.permuteFunctional() { result.append(copyOfSelf) } 395 | return result 396 | } 397 | 398 | func permutationsOverMutationsFunctionalOptimized() -> [[Element]] { 399 | var copyOfSelf = self 400 | let result = Array<[Element]>(repeating: copyOfSelf, count: count.factorial) 401 | return result.map { _ in defer { let _ = copyOfSelf.permuteFunctionalOptimized() }; return copyOfSelf } 402 | } 403 | 404 | func permutationsOverMutationsFunctionalOptimizedWithAppendAlloc() -> [[Element]] { 405 | var copyOfSelf = self 406 | var result: [[Element]] = [copyOfSelf] 407 | while copyOfSelf.permuteFunctionalOptimized() { result.append(copyOfSelf) } 408 | return result 409 | } 410 | 411 | func permutationsOverMutationsFunctionalOptimized2() -> [[Element]] { 412 | var copyOfSelf = self 413 | let result = Array<[Element]>(repeating: copyOfSelf, count: count.factorial) 414 | return result.map { _ in defer { let _ = copyOfSelf.permuteFunctionalOptimized2() }; return copyOfSelf } 415 | } 416 | 417 | func permutationsOverMutationsFunctionalOptimizedWithAppendAlloc2() -> [[Element]] { 418 | var copyOfSelf = self 419 | var result: [[Element]] = [copyOfSelf] 420 | while copyOfSelf.permuteFunctionalOptimized2() { result.append(copyOfSelf) } 421 | return result 422 | } 423 | 424 | func permutationsOverMutationsFunctionalOptimized3() -> [[Element]] { 425 | var copyOfSelf = self 426 | let result = Array<[Element]>(repeating: copyOfSelf, count: count.factorial) 427 | return result.map { _ in defer { let _ = copyOfSelf.permuteFunctionalOptimized3() }; return copyOfSelf } 428 | } 429 | 430 | func permutationsOverMutationsFunctionalOptimizedWithAppendAlloc3() -> [[Element]] { 431 | var copyOfSelf = self 432 | var result: [[Element]] = [copyOfSelf] 433 | while copyOfSelf.permuteFunctionalOptimized3() { result.append(copyOfSelf) } 434 | return result 435 | } 436 | 437 | func permutationsOverMutationsFunctionalOptimized4() -> [[Element]] { 438 | os_signpost(.begin, log: permutationOSLog, name: SignpostName.permutationsOverMutationsFunctionalOptimized4) 439 | var copyOfSelf = self 440 | let result = Array<[Element]>(repeating: copyOfSelf, count: count.factorial) 441 | let output = result.map { _ -> [Element] in defer { let _ = copyOfSelf.permuteFunctionalOptimized4() }; return copyOfSelf } 442 | os_signpost(.end, log: permutationOSLog, name: SignpostName.permutationsOverMutationsFunctionalOptimized4) 443 | return output 444 | } 445 | 446 | func permutationsOverMutationsFunctionalOptimizedWithAppendAlloc4() -> [[Element]] { 447 | var copyOfSelf = self 448 | var result: [[Element]] = [copyOfSelf] 449 | while copyOfSelf.permuteFunctionalOptimized4() { result.append(copyOfSelf) } 450 | return result 451 | } 452 | 453 | func permutationsConcurrent(concurrentThreads: Int, flatMapAtTheEnd: Bool = false) -> [[Element]] { 454 | os_signpost(.begin, log: permutationOSLog, name: SignpostName.permutationsConcurrent) 455 | var result: [[Element]] = [] 456 | let seeds: [[Element]] = (0.. 0 { splitSize += 1 } 462 | let seedSplits = seeds.split(by: splitSize) 463 | let group = DispatchGroup() 464 | var intermediateResults = [[[Element]]].init(repeating: [], count: seedSplits.count) 465 | seedSplits.enumerated().forEach { index, arrayOfSeeds in 466 | DispatchQueue.global(qos: .userInteractive).async(group: group) { 467 | var intermediateResult: [[Element]] = [] 468 | arrayOfSeeds.forEach { (seed) in 469 | intermediateResult += 470 | seed.permutationBackgroundWorker(iterationsCount: (self.count - 1).factorial) 471 | } 472 | intermediateResults[index] = intermediateResult 473 | } 474 | } 475 | group.wait() 476 | if flatMapAtTheEnd { 477 | return intermediateResults.flatMap { $0 } 478 | } 479 | intermediateResults.forEach { intermediateResult in 480 | result += intermediateResult 481 | } 482 | os_signpost(.end, log: permutationOSLog, name: SignpostName.permutationsConcurrent) 483 | return result 484 | } 485 | 486 | private func permutationBackgroundWorker(iterationsCount: Int) -> [[Element]] { 487 | var copyOfSelf = self 488 | var result: [[Element]] = [copyOfSelf] 489 | (1..