Repository: RobertGummesson/BuildTimeAnalyzer-for-Xcode Branch: master Commit: b4fe117e11bc Files: 33 Total size: 168.1 KB Directory structure: gitextract_fhrgbqty/ ├── .gitignore ├── .travis.yml ├── BuildTimeAnalyzer/ │ ├── AppDelegate.swift │ ├── Assets.xcassets/ │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── ScreenShot.imageset/ │ │ └── Contents.json │ ├── BuildManager.swift │ ├── BuildTimeAnalyzer-Bridging-Header.h │ ├── CSVExporter.swift │ ├── CompileMeasure.swift │ ├── DerivedDataManager.swift │ ├── DirectoryMonitor.swift │ ├── File.swift │ ├── Info.plist │ ├── LogProcessor.swift │ ├── Main.storyboard │ ├── NSAlert+Extensions.swift │ ├── ProcessingState.swift │ ├── ProjectSelection.swift │ ├── RawMeasure.swift │ ├── UserSettings.swift │ ├── ViewController.swift │ ├── ViewControllerDataSource.swift │ └── XcodeDatabase.swift ├── BuildTimeAnalyzer.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ └── BuildTimeAnalyzer.xcscheme ├── BuildTimeAnalyzerTests/ │ ├── CompileMeasureTests.swift │ ├── Info.plist │ └── ViewControllerDataSourceTest.swift ├── LICENSE └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## Build generated build/ DerivedData ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata ## Other *.xccheckout *.moved-aside *.xcuserstate *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ .build/ # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Pods/ # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build # fastlane # # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md fastlane/report.xml fastlane/screenshots .DS_Store ================================================ FILE: .travis.yml ================================================ language: swift osx_image: xcode9.3 script: xcodebuild -project BuildTimeAnalyzer.xcodeproj -scheme BuildTimeAnalyzer build test ================================================ FILE: BuildTimeAnalyzer/AppDelegate.swift ================================================ // // AppDelegate.swift // BuildTimeAnalyzer // import Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet weak var projectSelectionMenuItem: NSMenuItem! @IBOutlet weak var buildTimesMenuItem: NSMenuItem! @IBOutlet weak var alwaysInFrontMenuItem: NSMenuItem! @objc var canExport: Bool = false var viewController: ViewController? { return NSApplication.shared.mainWindow?.contentViewController as? ViewController } func applicationDidFinishLaunching(_ notification: Notification) { alwaysInFrontMenuItem.state = UserSettings.windowShouldBeTopMost ? .on : .off } func configureMenuItems(showBuildTimesMenuItem: Bool) { projectSelectionMenuItem.isEnabled = !showBuildTimesMenuItem buildTimesMenuItem.isEnabled = showBuildTimesMenuItem } // MARK: Actions @IBAction func navigateToProjectSelection(_ sender: NSMenuItem) { configureMenuItems(showBuildTimesMenuItem: true) viewController?.cancelProcessing() viewController?.showInstructions(true) } @IBAction func navigateToBuildTimes(_ sender: NSMenuItem) { configureMenuItems(showBuildTimesMenuItem: false) viewController?.showInstructions(false) } @IBAction func visitGitHubPage(_ sender: AnyObject) { let path = "https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode" if let url = URL(string: path) { NSWorkspace.shared.open(url) } } @IBAction func toggleAlwaysInFront(_ sender: NSMenuItem) { let alwaysInFront = sender.state == .off sender.state = alwaysInFront ? .on : .off UserSettings.windowShouldBeTopMost = alwaysInFront viewController?.makeWindowTopMost(topMost: alwaysInFront) } } ================================================ FILE: BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "logo16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "logo32-1.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "logo32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "logo64.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "logo128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "logo256-1.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "logo256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "logo512-1.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "logo512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "logo512@2x.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: BuildTimeAnalyzer/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: BuildTimeAnalyzer/Assets.xcassets/ScreenShot.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "Screen Shot.png", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: BuildTimeAnalyzer/BuildManager.swift ================================================ // // BuildManager.swift // BuildTimeAnalyzer // import Cocoa protocol BuildManagerDelegate: AnyObject { func derivedDataDidChange() func buildManager(_ buildManager: BuildManager, shouldParseLogWithDatabase database: XcodeDatabase) } class BuildManager: NSObject { weak var delegate: BuildManagerDelegate? private let derivedDataDirectoryMonitor = DirectoryMonitor(isDerivedData: true) private let logFolderDirectoryMonitor = DirectoryMonitor(isDerivedData: false) private var currentDataBase: XcodeDatabase? override func awakeFromNib() { super.awakeFromNib() derivedDataDirectoryMonitor.delegate = self logFolderDirectoryMonitor.delegate = self startMonitoring() } func startMonitoring() { stopMonitoring() derivedDataDirectoryMonitor.startMonitoring(path: UserSettings.derivedDataLocation) } func stopMonitoring() { derivedDataDirectoryMonitor.stopMonitoring() } func database(forFolder URL: URL) -> XcodeDatabase? { let databaseURL = URL.appendingPathComponent("Cache.db") return XcodeDatabase(fromPath: databaseURL.path) } func processDerivedData() { guard let mostRecent = DerivedDataManager.derivedData().first else { return } let logFolder = mostRecent.url.appendingPathComponent("Logs/Build").path guard logFolderDirectoryMonitor.path != logFolder else { return } logFolderDirectoryMonitor.stopMonitoring() logFolderDirectoryMonitor.startMonitoring(path: logFolder) } func processLogFolder(with url: URL) { guard let activeDatabase = database(forFolder: url), activeDatabase.isBuildType, activeDatabase != currentDataBase else { return } currentDataBase = activeDatabase delegate?.buildManager(self, shouldParseLogWithDatabase: activeDatabase) } } extension BuildManager: DirectoryMonitorDelegate { func directoryMonitorDidObserveChange(_ directoryMonitor: DirectoryMonitor, isDerivedData: Bool) { if isDerivedData { delegate?.derivedDataDidChange() processDerivedData() } else if let path = directoryMonitor.path { // TODO: If we don't dispatch, it seems it fires off too soon DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.processLogFolder(with: URL(fileURLWithPath: path)) } } } } ================================================ FILE: BuildTimeAnalyzer/BuildTimeAnalyzer-Bridging-Header.h ================================================ // // Use this file to import your target's public headers that you would like to expose to Swift. // #import "NSData+GZIP.h" ================================================ FILE: BuildTimeAnalyzer/CSVExporter.swift ================================================ // // CSVExporter.swift // BuildTimeAnalyzer // // Created by Bruno Resende on 16.01.19. // Copyright © 2019 Cane Media Ltd. All rights reserved. // import Foundation struct CSVExporter { static var filenameDateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd-HHmmss" return formatter }() func filename(with prefix: String) -> String { return "\(prefix)_\(CSVExporter.filenameDateFormatter.string(from: Date())).csv" } func export(elements: [T], to url: URL) throws where T: CSVExportable { guard let data = elements.joinedAsCSVString(delimiter: .doubleQuote).data(using: .utf8) else { throw ExportErrors.couldNotParseStringAsUTF8 } do { try data.write(to: url, options: .atomic) } catch { throw ExportErrors.fileIO(error) } } enum ExportErrors: Error { case couldNotParseStringAsUTF8 case fileIO(Error) } } enum CSVDelimiter: String { case singleQuote = "'" case doubleQuote = "\"" case none = "" } protocol CSVExportable { static var csvHeaderLine: String { get } var csvLine: String { get } } extension Array where Element: CSVExportable { func joinedAsCSVString(delimiter: CSVDelimiter) -> String { return ([Element.csvHeaderLine] + self.map({ $0.csvLine })).joined(separator: "\n") } } extension Array where Element == String { func joinedAsCSVLine(delimiter: CSVDelimiter) -> String { let formatter: (String) -> String switch delimiter { case .singleQuote: formatter = { $0.replacingOccurrences(of: "'", with: "\\'") } case .doubleQuote: formatter = { $0.replacingOccurrences(of: "\"", with: "\\\"") } case .none: formatter = { $0 } } return self.map({ "\(delimiter.rawValue)\(formatter($0))\(delimiter.rawValue)" }).joined(separator: ",") } } ================================================ FILE: BuildTimeAnalyzer/CompileMeasure.swift ================================================ // // CompileMeasure.swift // BuildTimeAnalyzer // import Foundation @objcMembers class CompileMeasure: NSObject { dynamic var time: Double var path: String var code: String dynamic var filename: String var references: Int private var locationArray: [Int] public enum Order: String { case filename case time } var fileAndLine: String { return "\(filename):\(locationArray[0])" } var fileInfo: String { return "\(fileAndLine):\(locationArray[1])" } var fileRow: String { "\(locationArray[0])" } var fileColumn: String { "\(locationArray[1])" } var location: Int { return locationArray[0] } var timeString: String { return String(format: "%.f", time) } init?(time: Double, rawPath: String, code: String, references: Int) { let untrimmedFilename = rawPath.split(separator: "/").map(String.init).last guard let filepath = rawPath.split(separator: ":").map(String.init).first, let filename = untrimmedFilename?.split(separator: ":").map(String.init).first else { return nil } let locationString = String(rawPath[filepath.endIndex...].dropFirst()) let locations = locationString.split(separator: ":").compactMap{ Int(String.init($0)) } guard locations.count == 2 else { return nil } self.time = time self.code = code self.path = filepath self.filename = filename self.locationArray = locations self.references = references } init?(rawPath: String, time: Double) { let untrimmedFilename = rawPath.split(separator: "/").map(String.init).last guard let filepath = rawPath.split(separator: ":").map(String.init).first, let filename = untrimmedFilename?.split(separator: ":").map(String.init).first else { return nil } self.time = time self.code = "" self.path = filepath self.filename = filename self.locationArray = [1,1] self.references = 1 } subscript(column: Int) -> String { switch column { case 0: return timeString case 1: return fileInfo case 2: return "\(references)" default: return code } } } extension CompileMeasure: CSVExportable { static var csvHeaderLine: String = ["time", "file", "row", "column", "references", "code"].joinedAsCSVLine(delimiter: .doubleQuote) var csvLine: String { return [timeString, filename, fileRow, fileColumn, "\(references)", code].joinedAsCSVLine(delimiter: .doubleQuote) } } ================================================ FILE: BuildTimeAnalyzer/DerivedDataManager.swift ================================================ // // DerivedDataManager.swift // BuildTimeAnalyzer // import Foundation class DerivedDataManager { static func derivedData() -> [File] { let url = URL(fileURLWithPath: UserSettings.derivedDataLocation) let folders = DerivedDataManager.listFolders(at: url) let fileManager = FileManager.default return folders.compactMap{ (url) -> File? in if url.lastPathComponent != "ModuleCache", let properties = try? fileManager.attributesOfItem(atPath: url.path), let modificationDate = properties[FileAttributeKey.modificationDate] as? Date { return File(date: modificationDate, url: url) } return nil }.sorted{ $0.date > $1.date } } static func listFolders(at url: URL) -> [URL] { let fileManager = FileManager.default let keys = [URLResourceKey.nameKey, URLResourceKey.isDirectoryKey] let options: FileManager.DirectoryEnumerationOptions = [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants] guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, options: options, errorHandler: nil) else { return [] } return enumerator.map{ $0 as! URL } } } ================================================ FILE: BuildTimeAnalyzer/DirectoryMonitor.swift ================================================ // // DirectoryMonitor.swift // BuildTimeAnalyzer // import Foundation protocol DirectoryMonitorDelegate: AnyObject { func directoryMonitorDidObserveChange(_ directoryMonitor: DirectoryMonitor, isDerivedData: Bool) } class DirectoryMonitor { var dispatchQueue: DispatchQueue weak var delegate: DirectoryMonitorDelegate? var fileDescriptor: Int32 = -1 var dispatchSource: DispatchSourceFileSystemObject? var isDerivedData: Bool var path: String? var timer: Timer? var lastDerivedDataDate = Date() var isMonitoringDates = false init(isDerivedData: Bool) { self.isDerivedData = isDerivedData let suffix = isDerivedData ? "deriveddata" : "logfolder" dispatchQueue = DispatchQueue(label: "uk.co.canemedia.directorymonitor.\(suffix)", attributes: .concurrent) } func startMonitoring(path: String) { self.path = path guard dispatchSource == nil && fileDescriptor == -1 else { return } fileDescriptor = open(path, O_EVTONLY) guard fileDescriptor != -1 else { return } dispatchSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .all, queue: dispatchQueue) dispatchSource?.setEventHandler { DispatchQueue.main.async { self.delegate?.directoryMonitorDidObserveChange(self, isDerivedData: self.isDerivedData) } } dispatchSource?.setCancelHandler { close(self.fileDescriptor) self.fileDescriptor = -1 self.dispatchSource = nil self.path = nil } dispatchSource?.resume() if isDerivedData && !isMonitoringDates { isMonitoringDates = true monitorModificationDates() } } func stopMonitoring() { dispatchSource?.cancel() path = nil } func monitorModificationDates() { if let date = DerivedDataManager.derivedData().first?.date, date > lastDerivedDataDate { lastDerivedDataDate = date self.delegate?.directoryMonitorDidObserveChange(self, isDerivedData: self.isDerivedData) } if path != nil { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.monitorModificationDates() } } else { isMonitoringDates = false } } } ================================================ FILE: BuildTimeAnalyzer/File.swift ================================================ // // File.swift // BuildTimeAnalyzer // import Foundation struct File { let date: Date let url: URL } ================================================ FILE: BuildTimeAnalyzer/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0.12 CFBundleSignature ???? CFBundleVersion 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright © 2016 Cane Media Ltd. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass NSApplication ================================================ FILE: BuildTimeAnalyzer/LogProcessor.swift ================================================ // // LogProcessor.swift // BuildTimeAnalyzer // import Foundation typealias CMUpdateClosure = (_ result: [CompileMeasure], _ didComplete: Bool, _ didCancel: Bool) -> () protocol LogProcessorProtocol: AnyObject { var rawMeasures: [String: RawMeasure] { get set } var updateHandler: CMUpdateClosure? { get set } var shouldCancel: Bool { get set } func processingDidStart() func processingDidFinish() } extension LogProcessorProtocol { func processDatabase(database: XcodeDatabase, updateHandler: CMUpdateClosure?) { guard let text = database.processLog() else { updateHandler?([], true, false) return } self.updateHandler = updateHandler DispatchQueue.global().async { self.process(text: text) } } // MARK: Private methods private func process(text: String) { let characterSet = CharacterSet(charactersIn:"\r\"") var remainingRange = text.startIndex.. 10 } if filteredResults.count < 20 { filteredResults = rawMeasures.values.filter{ $0.time > 0.1 } } let sortedResults = filteredResults.sorted(by: { $0.time > $1.time }) updateHandler?(processResult(sortedResults), completed, didCancel) if completed { rawMeasures.removeAll() } } private func processResult(_ unprocessedResult: [RawMeasure]) -> [CompileMeasure] { let characterSet = CharacterSet(charactersIn:"\r\"") var result: [CompileMeasure] = [] for entry in unprocessedResult { let code = entry.text.split(separator: "\t").map(String.init) let method = code.count >= 2 ? trimPrefixes(code[1]) : "-" if let path = code.first?.trimmingCharacters(in: characterSet), let measure = CompileMeasure(time: entry.time, rawPath: path, code: method, references: entry.references) { result.append(measure) } } return result } private func trimPrefixes(_ code: String) -> String { var code = code ["@objc ", "final ", "@IBAction "].forEach { (prefix) in if code.hasPrefix(prefix) { code = String(code[code.index(code.startIndex, offsetBy: prefix.count)...]) } } return code } } class LogProcessor: NSObject, LogProcessorProtocol { var rawMeasures: [String: RawMeasure] = [:] var updateHandler: CMUpdateClosure? var shouldCancel = false var timer: Timer? func processingDidStart() { DispatchQueue.main.async { self.timer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(self.timerCallback(_:)), userInfo: nil, repeats: true) } } func processingDidFinish() { DispatchQueue.main.async { self.timer?.invalidate() self.timer = nil let didCancel = self.shouldCancel self.shouldCancel = false self.updateResults(didComplete: true, didCancel: didCancel) } } @objc func timerCallback(_ timer: Timer) { updateResults(didComplete: false, didCancel: false) } } ================================================ FILE: BuildTimeAnalyzer/Main.storyboard ================================================ ================================================ FILE: BuildTimeAnalyzer/NSAlert+Extensions.swift ================================================ // // NSAlert+Extensions.swift // BuildTimeAnalyzer // import Cocoa extension NSAlert { static func show(withMessage message: String, andInformativeText informativeText: String = "") { let alert = NSAlert() alert.messageText = message alert.informativeText = informativeText alert.alertStyle = .warning alert.addButton(withTitle: "OK") alert.runModal() } } ================================================ FILE: BuildTimeAnalyzer/ProcessingState.swift ================================================ // // ProcessingState.swift // BuildTimeAnalyzer // enum ProcessingState { case processing case waiting case completed(didSucceed: Bool, stateName: String) static let cancelledString = "Cancelled" static let completedString = "Completed" static let failedString = "No valid logs found" static let processingString = "Processing log..." static let waitingForBuildString = "Waiting..." } extension ProcessingState : Equatable {} func ==(lhs: ProcessingState, rhs: ProcessingState) -> Bool { switch (lhs, rhs) { case (let .completed(didSucceed1, _), let .completed(didSucceed2, _)): return didSucceed1 == didSucceed2 case (.processing, .processing), (.waiting, .waiting): return true default: return false } } ================================================ FILE: BuildTimeAnalyzer/ProjectSelection.swift ================================================ // // ProjectSelection.swift // BuildTimeAnalyzer // import Cocoa protocol ProjectSelectionDelegate: AnyObject { func didSelectProject(with database: XcodeDatabase) } class ProjectSelection: NSObject { @IBOutlet weak var tableView: NSTableView! weak var delegate: ProjectSelectionDelegate? private var dataSource: [XcodeDatabase] = [] static private let dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.timeStyle = .short dateFormatter.dateStyle = .medium return dateFormatter }() func listFolders() { dataSource = DerivedDataManager.derivedData().compactMap{ XcodeDatabase(fromPath: $0.url.appendingPathComponent("Logs/Build/LogStoreManifest.plist").path) }.sorted(by: { $0.modificationDate > $1.modificationDate }) tableView.reloadData() } // MARK: Actions @IBAction func didSelectCell(_ sender: NSTableView) { guard sender.selectedRow != -1 else { return } delegate?.didSelectProject(with: dataSource[sender.selectedRow]) } } // MARK: NSTableViewDataSource extension ProjectSelection: NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { return dataSource.count } } // MARK: NSTableViewDelegate extension ProjectSelection: NSTableViewDelegate { func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { guard let tableColumn = tableColumn, let columnIndex = tableView.tableColumns.firstIndex(of: tableColumn) else { return nil } let cellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell\(columnIndex)"), owner: self) as? NSTableCellView let source = dataSource[row] var value = "" switch columnIndex { case 0: value = source.schemeName default: value = ProjectSelection.dateFormatter.string(from: source.modificationDate) } cellView?.textField?.stringValue = value return cellView } } ================================================ FILE: BuildTimeAnalyzer/RawMeasure.swift ================================================ // // RawMeasure.swift // BuildTimeAnalyzer // import Foundation struct RawMeasure { var time: Double var text: String var references: Int init(time: Double, text: String) { self.time = time self.text = text self.references = 1 } } // MARK: Equatable extension RawMeasure: Equatable {} func ==(lhs: RawMeasure, rhs: RawMeasure) -> Bool { return lhs.time == rhs.time && lhs.text == rhs.text } // MARK: Hashable extension RawMeasure: Hashable { func hash(into hasher: inout Hasher) { hasher.combine(time) hasher.combine(text) hasher.combine(references) } } ================================================ FILE: BuildTimeAnalyzer/UserSettings.swift ================================================ // // UserCache.swift // BuildTimeAnalyzer // import Foundation class UserSettings { static private let derivedDataLocationKey = "derivedDataLocationKey" static private let windowLevelIsNormalKey = "windowLevelIsNormalKey" static private var _derivedDataLocation: String? static private var _windowLevelIsNormal: Bool? static var derivedDataLocation: String { get { if _derivedDataLocation == nil { _derivedDataLocation = UserDefaults.standard.string(forKey: derivedDataLocationKey) } if _derivedDataLocation == nil, let libraryFolder = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first { _derivedDataLocation = "\(libraryFolder)/Developer/Xcode/DerivedData" } return _derivedDataLocation ?? "" } set { _derivedDataLocation = newValue UserDefaults.standard.set(newValue, forKey: derivedDataLocationKey) UserDefaults.standard.synchronize() } } static var windowShouldBeTopMost: Bool { get { if _windowLevelIsNormal == nil { _windowLevelIsNormal = UserDefaults.standard.bool(forKey: windowLevelIsNormalKey) } return !(_windowLevelIsNormal ?? false) } set { _windowLevelIsNormal = !newValue UserDefaults.standard.set(_windowLevelIsNormal, forKey: windowLevelIsNormalKey) UserDefaults.standard.synchronize() } } } ================================================ FILE: BuildTimeAnalyzer/ViewController.swift ================================================ // // ViewController.swift // BuildTimeAnalyzer // import Cocoa class ViewController: NSViewController { @IBOutlet var buildManager: BuildManager! @IBOutlet weak var cancelButton: NSButton! @IBOutlet weak var compileTimeTextField: NSTextField! @IBOutlet weak var derivedDataTextField: NSTextField! @IBOutlet weak var instructionsView: NSView! @IBOutlet weak var leftButton: NSButton! @IBOutlet weak var perFileButton: NSButton! @IBOutlet weak var progressIndicator: NSProgressIndicator! @IBOutlet weak var projectSelection: ProjectSelection! @IBOutlet weak var searchField: NSSearchField! @IBOutlet weak var statusLabel: NSTextField! @IBOutlet weak var statusTextField: NSTextField! @IBOutlet weak var tableView: NSTableView! @IBOutlet weak var tableViewContainerView: NSScrollView! private let dataSource = ViewControllerDataSource() private var currentKey: String? private var nextDatabase: XcodeDatabase? private(set) var lastProcessedDatabaseSchemeName: String? = nil { didSet { (NSApp.delegate as? AppDelegate)?.canExport = lastProcessedDatabaseSchemeName != nil } } private var processor = LogProcessor() var processingState: ProcessingState = .waiting { didSet { updateViewForState() } } // MARK: Lifecycle override func viewDidLoad() { super.viewDidLoad() configureLayout() buildManager.delegate = self projectSelection.delegate = self projectSelection.listFolders() tableView.tableColumns[0].sortDescriptorPrototype = NSSortDescriptor(key: CompileMeasure.Order.time.rawValue, ascending: true) tableView.tableColumns[1].sortDescriptorPrototype = NSSortDescriptor(key: CompileMeasure.Order.filename.rawValue, ascending: true) NotificationCenter.default.addObserver(self, selector: #selector(windowWillClose(notification:)), name: NSWindow.willCloseNotification, object: nil) } override func viewWillAppear() { super.viewWillAppear() // Set window level before view is displayed makeWindowTopMost(topMost: UserSettings.windowShouldBeTopMost) } override func viewWillDisappear() { super.viewWillDisappear() // Reset window level before view is hidden // Reference: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/WinPanel/Concepts/WindowLevel.html makeWindowTopMost(topMost: false) } @objc func windowWillClose(notification: NSNotification) { guard let object = notification.object, !(object is NSPanel) else { return } NotificationCenter.default.removeObserver(self) processor.shouldCancel = true NSApp.terminate(self) } // MARK: Layout func configureLayout() { updateTotalLabel(with: 0) updateViewForState() showInstructions(true) derivedDataTextField.stringValue = UserSettings.derivedDataLocation makeWindowTopMost(topMost: UserSettings.windowShouldBeTopMost) } func showInstructions(_ show: Bool) { instructionsView.isHidden = !show let views: [NSView] = [compileTimeTextField, leftButton, perFileButton, searchField, statusLabel, statusTextField, tableViewContainerView] views.forEach{ $0.isHidden = show } if show && processingState == .processing { processor.shouldCancel = true cancelButton.isHidden = true progressIndicator.isHidden = true } } func updateViewForState() { switch processingState { case .processing: showInstructions(false) progressIndicator.isHidden = false progressIndicator.startAnimation(self) statusTextField.stringValue = ProcessingState.processingString cancelButton.isHidden = false case .completed(_, let stateName): progressIndicator.isHidden = true progressIndicator.stopAnimation(self) statusTextField.stringValue = stateName cancelButton.isHidden = true case .waiting: progressIndicator.isHidden = true progressIndicator.stopAnimation(self) statusTextField.stringValue = ProcessingState.waitingForBuildString cancelButton.isHidden = true } if instructionsView.isHidden { searchField.isHidden = !cancelButton.isHidden } } func makeWindowTopMost(topMost: Bool) { if let window = NSApplication.shared.windows.first { let level: CGWindowLevelKey = topMost ? .floatingWindow : .normalWindow window.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(level))) } } // MARK: Actions @IBAction func perFileCheckboxClicked(_ sender: NSButton) { dataSource.aggregateByFile = (sender.state.rawValue == 1) tableView.reloadData() } @IBAction func clipboardButtonClicked(_ sender: AnyObject) { NSPasteboard.general.clearContents() NSPasteboard.general.writeObjects(["-Xfrontend" as NSPasteboardWriting]) } @IBAction func clipboardButton2Clicked(_ sender: AnyObject) { NSPasteboard.general.clearContents() NSPasteboard.general.writeObjects(["-debug-time-function-bodies" as NSPasteboardWriting]) } @IBAction func visitDerivedData(_ sender: AnyObject) { let url = URL(fileURLWithPath: derivedDataTextField.stringValue, isDirectory: true) NSWorkspace.shared.open(url) } @IBAction func cancelButtonClicked(_ sender: AnyObject) { processor.shouldCancel = true } @IBAction func leftButtonClicked(_ sender: NSButton) { configureMenuItems(showBuildTimesMenuItem: true) cancelProcessing() showInstructions(true) projectSelection.listFolders() } @IBAction func exportAsCSVClicked(_ sender: Any?) { guard let keyWindow = NSApp.keyWindow, let scheme = lastProcessedDatabaseSchemeName else { return } let exporter = CSVExporter() let savePanel = NSSavePanel() savePanel.title = "Exporting data as CSV…" savePanel.message = "Pick location for CSV file to be exported:" savePanel.prompt = "Export" savePanel.allowedFileTypes = ["csv"] savePanel.nameFieldStringValue = exporter.filename(with: scheme) savePanel.beginSheetModal(for: keyWindow) { [dataSource] (response) in guard response == NSApplication.ModalResponse.OK, let fileUrl = savePanel.url else { return } do { try dataSource.exportProcessedData(using: exporter, to: fileUrl) } catch { NSAlert(error: error).runModal() } } } func controlTextDidChange(_ obj: Notification) { if let field = obj.object as? NSSearchField, field == searchField { dataSource.filter = searchField.stringValue tableView.reloadData() } else if let field = obj.object as? NSTextField, field == derivedDataTextField { buildManager.stopMonitoring() UserSettings.derivedDataLocation = field.stringValue projectSelection.listFolders() buildManager.startMonitoring() } } // MARK: Utilities func cancelProcessing() { guard processingState == .processing else { return } processor.shouldCancel = true cancelButton.isHidden = true } func configureMenuItems(showBuildTimesMenuItem: Bool) { if let appDelegate = NSApp.delegate as? AppDelegate { appDelegate.configureMenuItems(showBuildTimesMenuItem: showBuildTimesMenuItem) } } func processLog(with database: XcodeDatabase) { guard processingState != .processing else { if let currentKey = currentKey, currentKey != database.key { nextDatabase = database processor.shouldCancel = true } return } configureMenuItems(showBuildTimesMenuItem: false) processingState = .processing currentKey = database.key lastProcessedDatabaseSchemeName = database.schemeName updateTotalLabel(with: database.buildTime) processor.processDatabase(database: database) { [weak self] (result, didComplete, didCancel) in self?.handleProcessorUpdate(result: result, didComplete: didComplete, didCancel: didCancel) } } func handleProcessorUpdate(result: [CompileMeasure], didComplete: Bool, didCancel: Bool) { dataSource.resetSourceData(newSourceData: result) tableView.reloadData() if didComplete { completeProcessorUpdate(didCancel: didCancel) } } func completeProcessorUpdate(didCancel: Bool) { let didSucceed = !dataSource.isEmpty() var stateName = ProcessingState.failedString if didCancel { stateName = ProcessingState.cancelledString } else if didSucceed { stateName = ProcessingState.completedString } processingState = .completed(didSucceed: didSucceed, stateName: stateName) currentKey = nil if let nextDatabase = nextDatabase { self.nextDatabase = nil processLog(with: nextDatabase) } if !didSucceed { let text = "Ensure the Swift compiler flags has been added." NSAlert.show(withMessage: ProcessingState.failedString, andInformativeText: text) showInstructions(true) configureMenuItems(showBuildTimesMenuItem: true) } } func updateTotalLabel(with buildTime: Int) { let text = "Build duration: " + (buildTime < 60 ? "\(buildTime)s" : "\(buildTime / 60)m \(buildTime % 60)s") compileTimeTextField.stringValue = text } } // MARK: NSTableViewDataSource extension ViewController: NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { return dataSource.count() } func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { guard let item = dataSource.measure(index: row) else { return false } let url = URL(fileURLWithPath: item.path, isDirectory: true) NSWorkspace.shared.open(url) let gotoLineScript = "tell application \"Xcode\"\n" + " activate\n" + "end tell\n" + "tell application \"System Events\"\n" + " keystroke \"l\" using command down\n" + " keystroke \"\(item.location)\"\n" + " keystroke return\n" + "end tell" DispatchQueue.global().async { if let script = NSAppleScript(source: gotoLineScript) { script.executeAndReturnError(nil) } } return true } } // MARK: NSTableViewDelegate extension ViewController: NSTableViewDelegate { func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { guard let tableColumn = tableColumn, let columnIndex = tableView.tableColumns.firstIndex(of: tableColumn) else { return nil } guard let item = dataSource.measure(index: row) else { return nil } let result = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell\(columnIndex)"), owner: self) as? NSTableCellView result?.textField?.stringValue = item[columnIndex] return result } func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) { dataSource.sortDescriptors = tableView.sortDescriptors tableView.reloadData() } } // MARK: BuildManagerDelegate extension ViewController: BuildManagerDelegate { func buildManager(_ buildManager: BuildManager, shouldParseLogWithDatabase database: XcodeDatabase) { processLog(with: database) } func derivedDataDidChange() { projectSelection.listFolders() } } // MARK: ProjectSelectionDelegate extension ViewController: ProjectSelectionDelegate { func didSelectProject(with database: XcodeDatabase) { processLog(with: database) } } ================================================ FILE: BuildTimeAnalyzer/ViewControllerDataSource.swift ================================================ // // ViewControllerDataSource.swift // BuildTimeAnalyzer // // Created by Dmitrii on 02/12/2017. // Copyright © 2017 Cane Media Ltd. All rights reserved. // import Foundation class ViewControllerDataSource { var aggregateByFile = false { didSet { processData() } } var filter = "" { didSet { processData() } } var sortDescriptors = [NSSortDescriptor]() { didSet { processData() } } private var originalData = [CompileMeasure]() private var processedData = [CompileMeasure]() func resetSourceData(newSourceData: [CompileMeasure]) { originalData = newSourceData processData() } func isEmpty() -> Bool { return processedData.isEmpty } func count() -> Int { return processedData.count } func measure(index: Int) -> CompileMeasure? { guard index < processedData.count && index >= 0 else { return nil } return processedData[index] } func exportProcessedData(using exporter: CSVExporter, to url: URL) throws { try exporter.export(elements: processedData, to: url) } // MARK: - Private methods private func processData() { var newProcessedData = aggregateIfNeeded(originalData) newProcessedData = applySortingIfNeeded(newProcessedData) newProcessedData = applyFilteringIfNeeded(newProcessedData) processedData = newProcessedData } private func aggregateIfNeeded(_ input: [CompileMeasure]) -> [CompileMeasure] { guard aggregateByFile else { return input } var fileTimes: [String: CompileMeasure] = [:] for measure in input { if let fileMeasure = fileTimes[measure.path] { fileMeasure.time += measure.time fileTimes[measure.path] = fileMeasure } else { let newFileMeasure = CompileMeasure(rawPath: measure.path, time: measure.time) fileTimes[measure.path] = newFileMeasure } } return Array(fileTimes.values) } private func applySortingIfNeeded(_ input: [CompileMeasure]) -> [CompileMeasure] { if sortDescriptors.isEmpty { return input } return (input as NSArray).sortedArray(using: sortDescriptors) as! Array } private func applyFilteringIfNeeded(_ input: [CompileMeasure]) -> [CompileMeasure] { guard !filter.isEmpty else { return input } return input.filter{ textContains($0.code, pattern: filter) || textContains($0.filename, pattern: filter) } } private func textContains(_ text: String, pattern: String) -> Bool { return text.lowercased().contains(pattern.lowercased()) } } ================================================ FILE: BuildTimeAnalyzer/XcodeDatabase.swift ================================================ // // XcodeDatabase.swift // BuildTimeAnalyzer // import Foundation import Compression struct XcodeDatabase { var path: String var modificationDate: Date var key: String var schemeName: String var title: String var timeStartedRecording: Int var timeStoppedRecording: Int var isBuildType: Bool { return title.hasPrefix("Build ") || title.hasPrefix("Compile ") } var url: URL { return URL(fileURLWithPath: path) } var logUrl: URL { return folderPath.appendingPathComponent("\(key).xcactivitylog") } var folderPath: URL { return url.deletingLastPathComponent() } var buildTime: Int { return timeStoppedRecording - timeStartedRecording } init?(fromPath path: String) { guard let data = NSDictionary(contentsOfFile: path)?["logs"] as? [String: AnyObject], let key = XcodeDatabase.sortKeys(usingData: data).last?.key, let value = data[key] as? [String : AnyObject], let schemeName = value["schemeIdentifier-schemeName"] as? String, let title = value["title"] as? String, let timeStartedRecording = value["timeStartedRecording"] as? NSNumber, let timeStoppedRecording = value["timeStoppedRecording"] as? NSNumber, let fileAttributes = try? FileManager.default.attributesOfItem(atPath: path), let modificationDate = fileAttributes[FileAttributeKey.modificationDate] as? Date else { return nil } self.modificationDate = modificationDate self.path = path self.key = key self.schemeName = schemeName self.title = title self.timeStartedRecording = timeStartedRecording.intValue self.timeStoppedRecording = timeStoppedRecording.intValue } func processLog() -> String? { guard let rawData = try? Data(contentsOf: URL(fileURLWithPath: logUrl.path)), let data = rawData.gunzipped() else { return nil } return String(data: data, encoding: .utf8) } private static let gzipHeaderSize = 10 static func gunzip(_ data: Data) -> Data? { guard data.count > gzipHeaderSize else { return nil } // Skip the gzip header (10 bytes) to get raw deflate data let deflateData = data.dropFirst(gzipHeaderSize) let bufferSize = data.count * 4 let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) defer { buffer.deallocate() } var result = Data() deflateData.withUnsafeBytes { rawBuffer in guard let sourcePointer = rawBuffer.baseAddress?.bindMemory(to: UInt8.self, capacity: deflateData.count) else { return } let stream = UnsafeMutablePointer.allocate(capacity: 1) defer { stream.deallocate() } var status = compression_stream_init(stream, COMPRESSION_STREAM_DECODE, COMPRESSION_ZLIB) guard status == COMPRESSION_STATUS_OK else { return } defer { compression_stream_destroy(stream) } stream.pointee.src_ptr = sourcePointer stream.pointee.src_size = deflateData.count stream.pointee.dst_ptr = buffer stream.pointee.dst_size = bufferSize repeat { status = compression_stream_process(stream, 0) if stream.pointee.dst_size == 0 || status == COMPRESSION_STATUS_END { let outputSize = bufferSize - stream.pointee.dst_size result.append(buffer, count: outputSize) stream.pointee.dst_ptr = buffer stream.pointee.dst_size = bufferSize } } while status == COMPRESSION_STATUS_OK } return result.isEmpty ? nil : result } static private func sortKeys(usingData data: [String: AnyObject]) -> [(Int, key: String)] { var sortedKeys: [(Int, key: String)] = [] for key in data.keys { if let value = data[key] as? [String: AnyObject], let timeStoppedRecording = value["timeStoppedRecording"] as? NSNumber { sortedKeys.append((timeStoppedRecording.intValue, key)) } } return sortedKeys.sorted{ $0.0 < $1.0 } } } private extension Data { func gunzipped() -> Data? { return XcodeDatabase.gunzip(self) } } extension XcodeDatabase : Equatable {} func ==(lhs: XcodeDatabase, rhs: XcodeDatabase) -> Bool { return lhs.path == rhs.path && lhs.modificationDate == rhs.modificationDate } ================================================ FILE: BuildTimeAnalyzer.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 2839B8691FD2896F004C075C /* ViewControllerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2839B8681FD2896F004C075C /* ViewControllerDataSource.swift */; }; 2839B86B1FD32766004C075C /* ViewControllerDataSourceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2839B86A1FD32766004C075C /* ViewControllerDataSourceTest.swift */; }; 2A3164C81D21D73F00064045 /* CompileMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C01D21D73F00064045 /* CompileMeasure.swift */; }; 2A3164C91D21D73F00064045 /* LogProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C11D21D73F00064045 /* LogProcessor.swift */; }; 2A3164CB1D21D73F00064045 /* ProcessingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C31D21D73F00064045 /* ProcessingState.swift */; }; 2A3164CC1D21D73F00064045 /* RawMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C41D21D73F00064045 /* RawMeasure.swift */; }; 2A3164D01D21D74A00064045 /* CompileMeasureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164CF1D21D74A00064045 /* CompileMeasureTests.swift */; }; 2A3698AA1D80A1AC002C5CDA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A3698A91D80A1AC002C5CDA /* Main.storyboard */; }; 2A3698AC1D80A33B002C5CDA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3698AB1D80A33B002C5CDA /* ViewController.swift */; }; 2A5404011D86D01700DBD44C /* BuildManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5404001D86D01700DBD44C /* BuildManager.swift */; }; 2A5404031D86DE0C00DBD44C /* XcodeDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5404021D86DE0C00DBD44C /* XcodeDatabase.swift */; }; 2A5404051D86F3C700DBD44C /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5404041D86F3C700DBD44C /* File.swift */; }; 2A9807DD1D7C71F900B9232C /* DirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9807DC1D7C71F900B9232C /* DirectoryMonitor.swift */; }; 2A9807DF1D7C76FD00B9232C /* ProjectSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9807DE1D7C76FD00B9232C /* ProjectSelection.swift */; }; 2ABFB6CE1D81F2DE00D060BF /* NSAlert+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ABFB6CD1D81F2DE00D060BF /* NSAlert+Extensions.swift */; }; 2ACBFD0C1D8835E60009567E /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ACBFD0B1D8835E60009567E /* UserSettings.swift */; }; 2AE775121D225D5D00D1A744 /* DerivedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE775111D225D5D00D1A744 /* DerivedDataManager.swift */; }; 2AF821441D21D6B900D65186 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF821431D21D6B900D65186 /* AppDelegate.swift */; }; 2AF821461D21D6B900D65186 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2AF821451D21D6B900D65186 /* Assets.xcassets */; }; 5603EB6221EF93E90013D77B /* CSVExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603EB6121EF93E90013D77B /* CSVExporter.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 2AF821501D21D6B900D65186 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 2AF821381D21D6B900D65186 /* Project object */; proxyType = 1; remoteGlobalIDString = 2AF8213F1D21D6B900D65186; remoteInfo = BuildTimeAnalyzer; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 2839B8681FD2896F004C075C /* ViewControllerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerDataSource.swift; sourceTree = ""; }; 2839B86A1FD32766004C075C /* ViewControllerDataSourceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerDataSourceTest.swift; sourceTree = ""; }; 2A3164C01D21D73F00064045 /* CompileMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompileMeasure.swift; sourceTree = ""; }; 2A3164C11D21D73F00064045 /* LogProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogProcessor.swift; sourceTree = ""; }; 2A3164C31D21D73F00064045 /* ProcessingState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessingState.swift; sourceTree = ""; }; 2A3164C41D21D73F00064045 /* RawMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawMeasure.swift; sourceTree = ""; }; 2A3164CF1D21D74A00064045 /* CompileMeasureTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompileMeasureTests.swift; sourceTree = ""; }; 2A3698A91D80A1AC002C5CDA /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 2A3698AB1D80A33B002C5CDA /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 2A5404001D86D01700DBD44C /* BuildManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildManager.swift; sourceTree = ""; }; 2A5404021D86DE0C00DBD44C /* XcodeDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XcodeDatabase.swift; sourceTree = ""; }; 2A5404041D86F3C700DBD44C /* File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; 2A9807DC1D7C71F900B9232C /* DirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryMonitor.swift; sourceTree = ""; }; 2A9807DE1D7C76FD00B9232C /* ProjectSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectSelection.swift; sourceTree = ""; }; 2ABFB6CD1D81F2DE00D060BF /* NSAlert+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAlert+Extensions.swift"; sourceTree = ""; }; 2ACBFD0B1D8835E60009567E /* UserSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; 2AE775111D225D5D00D1A744 /* DerivedDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DerivedDataManager.swift; sourceTree = ""; }; 2AF821401D21D6B900D65186 /* BuildTimeAnalyzer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BuildTimeAnalyzer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2AF821431D21D6B900D65186 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 2AF821451D21D6B900D65186 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = BuildTimeAnalyzer/Assets.xcassets; sourceTree = ""; }; 2AF8214A1D21D6B900D65186 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2AF8214F1D21D6B900D65186 /* BuildTimeAnalyzerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BuildTimeAnalyzerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 2AF821551D21D6B900D65186 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5603EB6121EF93E90013D77B /* CSVExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVExporter.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 2AF8213D1D21D6B900D65186 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 2AF8214C1D21D6B900D65186 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 2A3164D11D21D74F00064045 /* Supporting Files */ = { isa = PBXGroup; children = ( 2AF8214A1D21D6B900D65186 /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; 2A3164D71D21D7A800064045 /* Supporting Files */ = { isa = PBXGroup; children = ( 2AF821551D21D6B900D65186 /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; 2ABFB6CF1D81F34300D060BF /* Extensions */ = { isa = PBXGroup; children = ( 2ABFB6CD1D81F2DE00D060BF /* NSAlert+Extensions.swift */, ); name = Extensions; sourceTree = ""; }; 2ABFB6D01D81F35400D060BF /* Application */ = { isa = PBXGroup; children = ( 2AF821431D21D6B900D65186 /* AppDelegate.swift */, 2A3698A91D80A1AC002C5CDA /* Main.storyboard */, ); name = Application; sourceTree = ""; }; 2ABFB6D11D81F37300D060BF /* Models */ = { isa = PBXGroup; children = ( 2A3164C01D21D73F00064045 /* CompileMeasure.swift */, 2A5404041D86F3C700DBD44C /* File.swift */, 2A3164C31D21D73F00064045 /* ProcessingState.swift */, 2A3164C41D21D73F00064045 /* RawMeasure.swift */, 2839B8681FD2896F004C075C /* ViewControllerDataSource.swift */, 2A5404021D86DE0C00DBD44C /* XcodeDatabase.swift */, ); name = Models; sourceTree = ""; }; 2ABFB6D21D81F81400D060BF /* ViewControllers */ = { isa = PBXGroup; children = ( 2A3698AB1D80A33B002C5CDA /* ViewController.swift */, ); name = ViewControllers; sourceTree = ""; }; 2ABFB6D31D81F82600D060BF /* Utilities */ = { isa = PBXGroup; children = ( 2A5404001D86D01700DBD44C /* BuildManager.swift */, 2AE775111D225D5D00D1A744 /* DerivedDataManager.swift */, 2A9807DC1D7C71F900B9232C /* DirectoryMonitor.swift */, 2A3164C11D21D73F00064045 /* LogProcessor.swift */, 2A9807DE1D7C76FD00B9232C /* ProjectSelection.swift */, 2ACBFD0B1D8835E60009567E /* UserSettings.swift */, 5603EB6121EF93E90013D77B /* CSVExporter.swift */, ); name = Utilities; sourceTree = ""; }; 2AF821371D21D6B900D65186 = { isa = PBXGroup; children = ( 2AF821421D21D6B900D65186 /* BuildTimeAnalyzer */, 2AF821451D21D6B900D65186 /* Assets.xcassets */, 2AF821521D21D6B900D65186 /* BuildTimeAnalyzerTests */, 2AF821411D21D6B900D65186 /* Products */, ); sourceTree = ""; }; 2AF821411D21D6B900D65186 /* Products */ = { isa = PBXGroup; children = ( 2AF821401D21D6B900D65186 /* BuildTimeAnalyzer.app */, 2AF8214F1D21D6B900D65186 /* BuildTimeAnalyzerTests.xctest */, ); name = Products; sourceTree = ""; }; 2AF821421D21D6B900D65186 /* BuildTimeAnalyzer */ = { isa = PBXGroup; children = ( 2ABFB6D01D81F35400D060BF /* Application */, 2ABFB6CF1D81F34300D060BF /* Extensions */, 2ABFB6D11D81F37300D060BF /* Models */, 2A3164D11D21D74F00064045 /* Supporting Files */, 2ABFB6D31D81F82600D060BF /* Utilities */, 2ABFB6D21D81F81400D060BF /* ViewControllers */, ); path = BuildTimeAnalyzer; sourceTree = ""; }; 2AF821521D21D6B900D65186 /* BuildTimeAnalyzerTests */ = { isa = PBXGroup; children = ( 2A3164CF1D21D74A00064045 /* CompileMeasureTests.swift */, 2839B86A1FD32766004C075C /* ViewControllerDataSourceTest.swift */, 2A3164D71D21D7A800064045 /* Supporting Files */, ); path = BuildTimeAnalyzerTests; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 2AF8213F1D21D6B900D65186 /* BuildTimeAnalyzer */ = { isa = PBXNativeTarget; buildConfigurationList = 2AF821581D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzer" */; buildPhases = ( 2AF8213C1D21D6B900D65186 /* Sources */, 2AF8213D1D21D6B900D65186 /* Frameworks */, 2AF8213E1D21D6B900D65186 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = BuildTimeAnalyzer; productName = BuildTimeAnalyzer; productReference = 2AF821401D21D6B900D65186 /* BuildTimeAnalyzer.app */; productType = "com.apple.product-type.application"; }; 2AF8214E1D21D6B900D65186 /* BuildTimeAnalyzerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 2AF8215B1D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzerTests" */; buildPhases = ( 2AF8214B1D21D6B900D65186 /* Sources */, 2AF8214C1D21D6B900D65186 /* Frameworks */, 2AF8214D1D21D6B900D65186 /* Resources */, ); buildRules = ( ); dependencies = ( 2AF821511D21D6B900D65186 /* PBXTargetDependency */, ); name = BuildTimeAnalyzerTests; productName = BuildTimeAnalyzerTests; productReference = 2AF8214F1D21D6B900D65186 /* BuildTimeAnalyzerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 2AF821381D21D6B900D65186 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 2630; ORGANIZATIONNAME = "Cane Media Ltd"; TargetAttributes = { 2AF8213F1D21D6B900D65186 = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1250; }; 2AF8214E1D21D6B900D65186 = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1250; TestTargetID = 2AF8213F1D21D6B900D65186; }; }; }; buildConfigurationList = 2AF8213B1D21D6B900D65186 /* Build configuration list for PBXProject "BuildTimeAnalyzer" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 2AF821371D21D6B900D65186; productRefGroup = 2AF821411D21D6B900D65186 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 2AF8213F1D21D6B900D65186 /* BuildTimeAnalyzer */, 2AF8214E1D21D6B900D65186 /* BuildTimeAnalyzerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 2AF8213E1D21D6B900D65186 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 2A3698AA1D80A1AC002C5CDA /* Main.storyboard in Resources */, 2AF821461D21D6B900D65186 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 2AF8214D1D21D6B900D65186 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 2AF8213C1D21D6B900D65186 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 2A9807DD1D7C71F900B9232C /* DirectoryMonitor.swift in Sources */, 2A3164C91D21D73F00064045 /* LogProcessor.swift in Sources */, 2839B8691FD2896F004C075C /* ViewControllerDataSource.swift in Sources */, 2A5404011D86D01700DBD44C /* BuildManager.swift in Sources */, 5603EB6221EF93E90013D77B /* CSVExporter.swift in Sources */, 2A5404051D86F3C700DBD44C /* File.swift in Sources */, 2ABFB6CE1D81F2DE00D060BF /* NSAlert+Extensions.swift in Sources */, 2A5404031D86DE0C00DBD44C /* XcodeDatabase.swift in Sources */, 2A3164CB1D21D73F00064045 /* ProcessingState.swift in Sources */, 2AF821441D21D6B900D65186 /* AppDelegate.swift in Sources */, 2AE775121D225D5D00D1A744 /* DerivedDataManager.swift in Sources */, 2A3698AC1D80A33B002C5CDA /* ViewController.swift in Sources */, 2A3164CC1D21D73F00064045 /* RawMeasure.swift in Sources */, 2ACBFD0C1D8835E60009567E /* UserSettings.swift in Sources */, 2A3164C81D21D73F00064045 /* CompileMeasure.swift in Sources */, 2A9807DF1D7C76FD00B9232C /* ProjectSelection.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 2AF8214B1D21D6B900D65186 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 2A3164D01D21D74A00064045 /* CompileMeasureTests.swift in Sources */, 2839B86B1FD32766004C075C /* ViewControllerDataSourceTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 2AF821511D21D6B900D65186 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 2AF8213F1D21D6B900D65186 /* BuildTimeAnalyzer */; targetProxy = 2AF821501D21D6B900D65186 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 2AF821561D21D6B900D65186 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 2AF821571D21D6B900D65186 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; }; name = Release; }; 2AF821591D21D6B900D65186 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; INFOPLIST_FILE = BuildTimeAnalyzer/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 26.0; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzer; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; }; name = Debug; }; 2AF8215A1D21D6B900D65186 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; INFOPLIST_FILE = BuildTimeAnalyzer/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 26.0; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzer; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; }; name = Release; }; 2AF8215C1D21D6B900D65186 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; INFOPLIST_FILE = BuildTimeAnalyzerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 26.0; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BuildTimeAnalyzer.app/Contents/MacOS/BuildTimeAnalyzer"; }; name = Debug; }; 2AF8215D1D21D6B900D65186 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; INFOPLIST_FILE = BuildTimeAnalyzerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 26.0; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BuildTimeAnalyzer.app/Contents/MacOS/BuildTimeAnalyzer"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 2AF8213B1D21D6B900D65186 /* Build configuration list for PBXProject "BuildTimeAnalyzer" */ = { isa = XCConfigurationList; buildConfigurations = ( 2AF821561D21D6B900D65186 /* Debug */, 2AF821571D21D6B900D65186 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 2AF821581D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzer" */ = { isa = XCConfigurationList; buildConfigurations = ( 2AF821591D21D6B900D65186 /* Debug */, 2AF8215A1D21D6B900D65186 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 2AF8215B1D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 2AF8215C1D21D6B900D65186 /* Debug */, 2AF8215D1D21D6B900D65186 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 2AF821381D21D6B900D65186 /* Project object */; } ================================================ FILE: BuildTimeAnalyzer.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: BuildTimeAnalyzer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: BuildTimeAnalyzer.xcodeproj/xcshareddata/xcschemes/BuildTimeAnalyzer.xcscheme ================================================ ================================================ FILE: BuildTimeAnalyzerTests/CompileMeasureTests.swift ================================================ // // CompileMeasureTests.swift // CMBuildTimeAnalyzerTests // import XCTest @testable import BuildTimeAnalyzer class BuildTimeAnalyzerTests: XCTestCase { func testInit() { // Given let time = 25.3 let timeString = "\(time)ms" let filename = "Code.Swift" let fileInfo = "\(filename):10:23" let location = 10 let folder = "/User/JohnAppleseed/" let path = "\(folder)\(filename)" let rawPath = "\(folder)\(fileInfo)" let code = "some code" let references = 2 // When let resultOptional = CompileMeasure(time: time, rawPath: rawPath, code: code, references: references) // Then XCTAssertNotNil(resultOptional) guard let result = resultOptional else { return } XCTAssertEqual(result.time, time) XCTAssertEqual(result.code, code) XCTAssertEqual(result.path, path) XCTAssertEqual(result.fileInfo, fileInfo) XCTAssertEqual(result.filename, filename) XCTAssertEqual(result.location, location) XCTAssertEqual(result.timeString, timeString) XCTAssertEqual(result.references, references) } } ================================================ FILE: BuildTimeAnalyzerTests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: BuildTimeAnalyzerTests/ViewControllerDataSourceTest.swift ================================================ // // ViewControllerDataSourceTest.swift // BuildTimeAnalyzerTests // // Created by Dmitrii on 02/12/2017. // Copyright © 2017 Cane Media Ltd. All rights reserved. // import XCTest @testable import BuildTimeAnalyzer class ViewControllerDataSourceTest: XCTestCase { var measArray: [CompileMeasure]! override func setUp() { super.setUp() let meas1 = CompileMeasure(rawPath: "FileName1.swift:1:1", time: 10) let meas2 = CompileMeasure(rawPath: "FileName2.swift:2:2", time: 2) let meas3 = CompileMeasure(rawPath: "FileName3.swift:3:3", time: 8) let meas4 = CompileMeasure(rawPath: "FileName3.swift:4:4", time: 0) let meas5 = CompileMeasure(rawPath: "FileName1.swift:5:5", time: 2) measArray = [meas4!, meas5!, meas2!, meas3!, meas1!] } func testInit() { let dataSource = ViewControllerDataSource() XCTAssertFalse(dataSource.aggregateByFile) XCTAssertEqual(dataSource.filter, "") XCTAssertNotNil(dataSource.sortDescriptors) XCTAssertEqual(dataSource.sortDescriptors.count, 0) XCTAssertTrue(dataSource.isEmpty()) } func testAggregate() { let dataSource = ViewControllerDataSource() dataSource.resetSourceData(newSourceData: measArray) dataSource.aggregateByFile = true XCTAssertEqual(dataSource.count(), 3) XCTAssertFalse(dataSource.isEmpty()) } func testFilter_1() { let dataSource = ViewControllerDataSource() dataSource.resetSourceData(newSourceData: measArray) dataSource.filter = "1" XCTAssertFalse(dataSource.isEmpty()) XCTAssertEqual(dataSource.count(), 2) XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName1.swift") XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName1.swift") } func testFilter_2() { let dataSource = ViewControllerDataSource() dataSource.resetSourceData(newSourceData: measArray) dataSource.filter = "2" XCTAssertFalse(dataSource.isEmpty()) XCTAssertEqual(dataSource.count(), 1) XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName2.swift") } func testFilter_noMatch() { let dataSource = ViewControllerDataSource() dataSource.resetSourceData(newSourceData: measArray) dataSource.filter = "noMatch" XCTAssertTrue(dataSource.isEmpty()) XCTAssertEqual(dataSource.count(), 0) } func testSortTimeAscending() { let dataSource = ViewControllerDataSource() dataSource.resetSourceData(newSourceData: measArray) let desc = NSSortDescriptor(key: "time", ascending: true) dataSource.sortDescriptors = [desc] XCTAssertFalse(dataSource.isEmpty()) XCTAssertEqual(dataSource.count(), 5) XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName1.swift") XCTAssertEqual(dataSource.measure(index: 2)!.filename, "FileName2.swift") XCTAssertEqual(dataSource.measure(index: 3)!.filename, "FileName3.swift") XCTAssertEqual(dataSource.measure(index: 4)!.filename, "FileName1.swift") } func testSortFilenameDescending() { let dataSource = ViewControllerDataSource() dataSource.resetSourceData(newSourceData: measArray) let desc = NSSortDescriptor(key: "filename", ascending: false) dataSource.sortDescriptors = [desc] XCTAssertFalse(dataSource.isEmpty()) XCTAssertEqual(dataSource.count(), 5) XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName3.swift") XCTAssertEqual(dataSource.measure(index: 2)!.filename, "FileName2.swift") XCTAssertEqual(dataSource.measure(index: 3)!.filename, "FileName1.swift") XCTAssertEqual(dataSource.measure(index: 4)!.filename, "FileName1.swift") } func testSortFilenameAscending_TimeAscending() { let dataSource = ViewControllerDataSource() dataSource.resetSourceData(newSourceData: measArray) let descFilename = NSSortDescriptor(key: "filename", ascending: true) let descTime = NSSortDescriptor(key: "time", ascending: true) dataSource.sortDescriptors = [descFilename, descTime] XCTAssertFalse(dataSource.isEmpty()) XCTAssertEqual(dataSource.count(), 5) XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName1.swift") XCTAssertEqual(dataSource.measure(index: 0)!.time, 2) XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName1.swift") XCTAssertEqual(dataSource.measure(index: 1)!.time, 10) XCTAssertEqual(dataSource.measure(index: 2)!.filename, "FileName2.swift") XCTAssertEqual(dataSource.measure(index: 3)!.filename, "FileName3.swift") XCTAssertEqual(dataSource.measure(index: 3)!.time, 0) XCTAssertEqual(dataSource.measure(index: 4)!.filename, "FileName3.swift") XCTAssertEqual(dataSource.measure(index: 4)!.time, 8) } func testSortTimeAscending_FilenameDescending() { let dataSource = ViewControllerDataSource() dataSource.resetSourceData(newSourceData: measArray) let descTime = NSSortDescriptor(key: "time", ascending: true) let descFilename = NSSortDescriptor(key: "filename", ascending: false) dataSource.sortDescriptors = [descTime, descFilename] XCTAssertFalse(dataSource.isEmpty()) XCTAssertEqual(dataSource.count(), 5) XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") XCTAssertEqual(dataSource.measure(index: 0)!.time, 0) XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName2.swift") XCTAssertEqual(dataSource.measure(index: 1)!.time, 2) XCTAssertEqual(dataSource.measure(index: 2)!.filename, "FileName1.swift") XCTAssertEqual(dataSource.measure(index: 2)!.time, 2) XCTAssertEqual(dataSource.measure(index: 3)!.filename, "FileName3.swift") XCTAssertEqual(dataSource.measure(index: 3)!.time, 8) XCTAssertEqual(dataSource.measure(index: 4)!.filename, "FileName1.swift") XCTAssertEqual(dataSource.measure(index: 4)!.time, 10) } func testSortTimeAscending_Filter3() { let dataSource = ViewControllerDataSource() dataSource.resetSourceData(newSourceData: measArray) let descTime = NSSortDescriptor(key: "time", ascending: true) dataSource.sortDescriptors = [descTime] dataSource.filter = "3" XCTAssertFalse(dataSource.isEmpty()) XCTAssertEqual(dataSource.count(), 2) XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") XCTAssertEqual(dataSource.measure(index: 0)!.time, 0) XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName3.swift") XCTAssertEqual(dataSource.measure(index: 1)!.time, 8) } func testFilter3_Aggregate() { let dataSource = ViewControllerDataSource() dataSource.resetSourceData(newSourceData: measArray) dataSource.filter = "3" dataSource.aggregateByFile = true XCTAssertFalse(dataSource.isEmpty()) XCTAssertEqual(dataSource.count(), 1) XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") } func testSortFilenameDescending_FilterCanceled_Aggregate() { let dataSource = ViewControllerDataSource() dataSource.resetSourceData(newSourceData: measArray) let descFilename = NSSortDescriptor(key: "filename", ascending: false) dataSource.sortDescriptors = [descFilename] dataSource.filter = "2" dataSource.aggregateByFile = true dataSource.filter = "" XCTAssertFalse(dataSource.isEmpty()) XCTAssertEqual(dataSource.count(), 3) XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName2.swift") XCTAssertEqual(dataSource.measure(index: 2)!.filename, "FileName1.swift") } } ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Robert Gummesson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ Build Time Analyzer for Xcode ====================== [![GitHub release](https://img.shields.io/github/release/RobertGummesson/BuildTimeAnalyzer-for-Xcode.svg)](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/releases/latest) [![Swift](https://img.shields.io/badge/Swift-5-orange.svg)](https://swift.org) [![Platform](https://img.shields.io/badge/platform-macOS-blue.svg)](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode) [![MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) ## Overview Build Time Analyzer is a macOS app that shows you a break down of Swift build times. See [this post]( https://medium.com/p/fc92cdd91e31) and [this post](https://medium.com/p/37b0a7514cbe) on Medium for context. ## Usage Open up the app and follow the instructions. ![screenshot.png](https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/master/Screenshots/screenshot.png) ## Installation Download the code and open it in Xcode, archive the project and export the build. Easy, right? ## Contributions If you encounter any issues or have ideas for improvement, I am open to code contributions. ## License Copyright (c) 2016-2026, Robert Gummesson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.