diff --git a/BuildTimeAnalyzer.xcodeproj/project.pbxproj b/BuildTimeAnalyzer.xcodeproj/project.pbxproj index 87d8af8..40f96af 100644 --- a/BuildTimeAnalyzer.xcodeproj/project.pbxproj +++ b/BuildTimeAnalyzer.xcodeproj/project.pbxproj @@ -252,7 +252,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1420; ORGANIZATIONNAME = "Cane Media Ltd"; TargetAttributes = { 2AF8213F1D21D6B900D65186 = { @@ -373,6 +373,7 @@ 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; @@ -380,6 +381,7 @@ 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; @@ -429,6 +431,7 @@ 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; @@ -436,6 +439,7 @@ 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; @@ -458,9 +462,12 @@ 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 = 13.0; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzer; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; @@ -472,9 +479,12 @@ 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 = 13.0; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzer; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; @@ -488,8 +498,10 @@ 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 = 13.0; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; @@ -505,8 +517,10 @@ 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 = 13.0; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; diff --git a/BuildTimeAnalyzer.xcodeproj/xcshareddata/xcschemes/BuildTimeAnalyzer.xcscheme b/BuildTimeAnalyzer.xcodeproj/xcshareddata/xcschemes/BuildTimeAnalyzer.xcscheme index 7a47967..545aca3 100644 --- a/BuildTimeAnalyzer.xcodeproj/xcshareddata/xcschemes/BuildTimeAnalyzer.xcscheme +++ b/BuildTimeAnalyzer.xcodeproj/xcshareddata/xcschemes/BuildTimeAnalyzer.xcscheme @@ -1,7 +1,7 @@ + LastUpgradeVersion = "1420" + version = "1.8"> @@ -27,6 +27,15 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -39,17 +48,6 @@ - - - - - - - - XcodeDatabase? { + private func database(forFolder URL: URL) -> XcodeDatabase? { let databaseURL = URL.appendingPathComponent("Cache.db") return XcodeDatabase(fromPath: databaseURL.path) } - func processDerivedData() { + private func processDerivedData() { guard let mostRecent = DerivedDataManager.derivedData().first else { return } let logFolder = mostRecent.url.appendingPathComponent("Logs/Build").path @@ -52,7 +52,7 @@ class BuildManager: NSObject { logFolderDirectoryMonitor.startMonitoring(path: logFolder) } - func processLogFolder(with url: URL) { + private func processLogFolder(with url: URL) { guard let activeDatabase = database(forFolder: url), activeDatabase.isBuildType, activeDatabase != currentDataBase else { return } @@ -62,6 +62,8 @@ class BuildManager: NSObject { } } +// MARK: - DirectoryMonitorDelegate + extension BuildManager: DirectoryMonitorDelegate { func directoryMonitorDidObserveChange(_ directoryMonitor: DirectoryMonitor, isDerivedData: Bool) { if isDerivedData { diff --git a/BuildTimeAnalyzer/CompileMeasure.swift b/BuildTimeAnalyzer/CompileMeasure.swift index 6c7ceb1..21b81b1 100755 --- a/BuildTimeAnalyzer/CompileMeasure.swift +++ b/BuildTimeAnalyzer/CompileMeasure.swift @@ -5,7 +5,7 @@ import Foundation -@objcMembers class CompileMeasure: NSObject { +@objcMembers final class CompileMeasure: NSObject { dynamic var time: Double var path: String @@ -15,12 +15,12 @@ import Foundation private var locationArray: [Int] - public enum Order: String { + enum Order: String { case filename case time } - var fileAndLine: String { + private var fileAndLine: String { return "\(filename):\(locationArray[0])" } diff --git a/BuildTimeAnalyzer/DerivedDataManager.swift b/BuildTimeAnalyzer/DerivedDataManager.swift index a6a82a2..02d161e 100644 --- a/BuildTimeAnalyzer/DerivedDataManager.swift +++ b/BuildTimeAnalyzer/DerivedDataManager.swift @@ -5,8 +5,7 @@ import Foundation -class DerivedDataManager { - +final class DerivedDataManager { static func derivedData() -> [File] { let url = URL(fileURLWithPath: UserSettings.derivedDataLocation) @@ -23,7 +22,7 @@ class DerivedDataManager { }.sorted{ $0.date > $1.date } } - static func listFolders(at url: URL) -> [URL] { + private static func listFolders(at url: URL) -> [URL] { let fileManager = FileManager.default let keys = [URLResourceKey.nameKey, URLResourceKey.isDirectoryKey] let options: FileManager.DirectoryEnumerationOptions = [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants] diff --git a/BuildTimeAnalyzer/DirectoryMonitor.swift b/BuildTimeAnalyzer/DirectoryMonitor.swift index b39d5fe..f99ba6d 100644 --- a/BuildTimeAnalyzer/DirectoryMonitor.swift +++ b/BuildTimeAnalyzer/DirectoryMonitor.swift @@ -5,22 +5,20 @@ import Foundation -protocol DirectoryMonitorDelegate: class { +protocol DirectoryMonitorDelegate: AnyObject { func directoryMonitorDidObserveChange(_ directoryMonitor: DirectoryMonitor, isDerivedData: Bool) } -class DirectoryMonitor { - var dispatchQueue: DispatchQueue +final class DirectoryMonitor { + private var dispatchQueue: DispatchQueue + private var fileDescriptor: Int32 = -1 + private var dispatchSource: DispatchSourceFileSystemObject? + private var isDerivedData: Bool + private var lastDerivedDataDate = Date() + private var isMonitoringDates = false - 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 + weak var delegate: DirectoryMonitorDelegate? init(isDerivedData: Bool) { self.isDerivedData = isDerivedData @@ -63,7 +61,7 @@ class DirectoryMonitor { path = nil } - func monitorModificationDates() { + private func monitorModificationDates() { if let date = DerivedDataManager.derivedData().first?.date, date > lastDerivedDataDate { lastDerivedDataDate = date self.delegate?.directoryMonitorDidObserveChange(self, isDerivedData: self.isDerivedData) diff --git a/BuildTimeAnalyzer/LogProcessor.swift b/BuildTimeAnalyzer/LogProcessor.swift index 4efbc5d..34a8f52 100755 --- a/BuildTimeAnalyzer/LogProcessor.swift +++ b/BuildTimeAnalyzer/LogProcessor.swift @@ -5,130 +5,139 @@ import Foundation -typealias CMUpdateClosure = (_ result: [CompileMeasure], _ didComplete: Bool, _ didCancel: Bool) -> () +typealias CMUpdateClosure = @MainActor (_ result: [CompileMeasure], _ didComplete: Bool, _ didCancel: Bool) -> () fileprivate let regex = try! NSRegularExpression(pattern: "^\\d*\\.?\\d*ms\\t/", options: []) -protocol LogProcessorProtocol: class { - var rawMeasures: [String: RawMeasure] { get set } - var updateHandler: CMUpdateClosure? { get set } - var shouldCancel: Bool { get set } +final fileprivate actor RawMeasures { + private var map: [String: RawMeasure] = [:] - func processingDidStart() - func processingDidFinish() -} - -class LogProcessor: NSObject, LogProcessorProtocol { - - var rawMeasures: [String: RawMeasure] = [:] - var updateHandler: CMUpdateClosure? - var shouldCancel = false - var timer: Timer? + var values: Dictionary.Values { + map.values + } - func processingDidStart() { - DispatchQueue.main.async { - self.timer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(self.timerCallback(_:)), userInfo: nil, repeats: true) - } + func value(forKey key: String) -> RawMeasure? { + map[key] } - func processingDidFinish() { - DispatchQueue.main.async { - self.timer?.invalidate() - self.timer = nil - let didCancel = self.shouldCancel - self.shouldCancel = false - self.updateResults(didComplete: true, didCancel: didCancel) - } + func set(_ value: RawMeasure, forKey key: String) { + map[key] = value } - @objc func timerCallback(_ timer: Timer) { - updateResults(didComplete: false, didCancel: false) + func removeAll() { + map.removeAll() } +} +final class LogProcessor: NSObject { + private var rawMeasures: RawMeasures = .init() + private var updateHandler: CMUpdateClosure? + @MainActor private var timer: Timer? + + var shouldCancel = false + + @MainActor func processDatabase(database: XcodeDatabase, updateHandler: CMUpdateClosure?) { guard let text = database.processLog() else { updateHandler?([], true, false) return } - + self.updateHandler = updateHandler - DispatchQueue.global(qos: .background).async { - self.process(text: text) + Task.detached(priority: .background) { + await self.process(text: text) } } - + // MARK: Private methods - - private func process(text: String) { + + @MainActor + private func processingDidStart() { + self.timer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(self.timerCallback(_:)), userInfo: nil, repeats: true) + } + + @MainActor + private func processingDidFinish() { + self.timer?.invalidate() + self.timer = nil + let didCancel = self.shouldCancel + self.shouldCancel = false + self.updateResults(didComplete: true, didCancel: didCancel) + } + + @objc private func timerCallback(_ timer: Timer) { + updateResults(didComplete: false, didCancel: false) + } + + private func process(text: String) async { let characterSet = CharacterSet(charactersIn:"\r") var remainingRange = text.startIndex...init(match.range, in: text)! let timeString = text[remainingRange.lowerBound.. 10 } if filteredResults.count < 20 { filteredResults = measures.filter{ $0.time > 0.1 } } - + let sortedResults = filteredResults.sorted(by: { $0.time > $1.time }) let result = self.processResult(sortedResults) - + if completed { - self.rawMeasures.removeAll() - } - - DispatchQueue.main.async { - self.updateHandler?(result, completed, didCancel) + await self.rawMeasures.removeAll() } + + + await self.updateHandler?(result, completed, didCancel) } } 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 diff --git a/BuildTimeAnalyzer/ProjectSelection.swift b/BuildTimeAnalyzer/ProjectSelection.swift index a1be15e..f3b676c 100755 --- a/BuildTimeAnalyzer/ProjectSelection.swift +++ b/BuildTimeAnalyzer/ProjectSelection.swift @@ -5,18 +5,18 @@ import Cocoa -protocol ProjectSelectionDelegate: class { +protocol ProjectSelectionDelegate: AnyObject { func didSelectProject(with database: XcodeDatabase) } -class ProjectSelection: NSObject { - - @IBOutlet weak var tableView: NSTableView! - weak var delegate: ProjectSelectionDelegate? +final class ProjectSelection: NSObject { + @IBOutlet private weak var tableView: NSTableView! private var dataSource: [XcodeDatabase] = [] - static private let dateFormatter: DateFormatter = { + weak var delegate: ProjectSelectionDelegate? + + private static let dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.timeStyle = .short dateFormatter.dateStyle = .medium @@ -33,7 +33,7 @@ class ProjectSelection: NSObject { // MARK: Actions - @IBAction func didSelectCell(_ sender: NSTableView) { + @IBAction private func didSelectCell(_ sender: NSTableView) { guard sender.selectedRow != -1 else { return } delegate?.didSelectProject(with: dataSource[sender.selectedRow]) } @@ -50,7 +50,6 @@ extension ProjectSelection: NSTableViewDataSource { // 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 } diff --git a/BuildTimeAnalyzer/RawMeasure.swift b/BuildTimeAnalyzer/RawMeasure.swift index c0aead0..f1a85d7 100755 --- a/BuildTimeAnalyzer/RawMeasure.swift +++ b/BuildTimeAnalyzer/RawMeasure.swift @@ -5,7 +5,7 @@ import Foundation -class RawMeasure { +final class RawMeasure { var time: Double var text: String var references: Int diff --git a/BuildTimeAnalyzer/UserSettings.swift b/BuildTimeAnalyzer/UserSettings.swift index 0124fb6..d4d4026 100644 --- a/BuildTimeAnalyzer/UserSettings.swift +++ b/BuildTimeAnalyzer/UserSettings.swift @@ -5,13 +5,12 @@ import Foundation -class UserSettings { +final class UserSettings { + private static let derivedDataLocationKey = "derivedDataLocationKey" + private static let windowLevelIsNormalKey = "windowLevelIsNormalKey" - static private let derivedDataLocationKey = "derivedDataLocationKey" - static private let windowLevelIsNormalKey = "windowLevelIsNormalKey" - - static private var _derivedDataLocation: String? - static private var _windowLevelIsNormal: Bool? + private static var _derivedDataLocation: String? + private static var _windowLevelIsNormal: Bool? static var derivedDataLocation: String { get { diff --git a/BuildTimeAnalyzer/ViewController.swift b/BuildTimeAnalyzer/ViewController.swift index aec0458..05b335b 100755 --- a/BuildTimeAnalyzer/ViewController.swift +++ b/BuildTimeAnalyzer/ViewController.swift @@ -5,23 +5,22 @@ import Cocoa -class ViewController: NSViewController { +final class ViewController: NSViewController { + @IBOutlet private var buildManager: BuildManager! + @IBOutlet private weak var cancelButton: NSButton! + @IBOutlet private weak var compileTimeTextField: NSTextField! + @IBOutlet private weak var derivedDataTextField: NSTextField! + @IBOutlet private weak var instructionsView: NSView! + @IBOutlet private weak var leftButton: NSButton! + @IBOutlet private weak var perFileButton: NSButton! + @IBOutlet private weak var progressIndicator: NSProgressIndicator! + @IBOutlet private weak var projectSelection: ProjectSelection! + @IBOutlet private weak var searchField: NSSearchField! + @IBOutlet private weak var statusLabel: NSTextField! + @IBOutlet private weak var statusTextField: NSTextField! + @IBOutlet private weak var tableView: NSTableView! + @IBOutlet private weak var tableViewContainerView: NSScrollView! - @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? @@ -29,7 +28,7 @@ class ViewController: NSViewController { private var processor = LogProcessor() - var processingState: ProcessingState = .waiting { + private var processingState: ProcessingState = .waiting { didSet { updateViewForState() } @@ -45,10 +44,10 @@ class ViewController: NSViewController { 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) } @@ -61,13 +60,42 @@ class ViewController: NSViewController { 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) { + // MARK: - Internal functions + + 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 cancelProcessing() { + guard processingState == .processing else { return } + + processor.shouldCancel = true + cancelButton.isHidden = true + } + + 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))) + } + } + + @objc private func windowWillClose(notification: NSNotification) { guard let object = notification.object, !(object is NSPanel) else { return } NotificationCenter.default.removeObserver(self) @@ -75,9 +103,7 @@ class ViewController: NSViewController { NSApp.terminate(self) } - // MARK: Layout - - func configureLayout() { + private func configureLayout() { updateTotalLabel(with: 0) updateViewForState() showInstructions(true) @@ -86,20 +112,7 @@ class ViewController: NSViewController { 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() { + private func updateViewForState() { switch processingState { case .processing: showInstructions(false) @@ -126,35 +139,29 @@ class ViewController: NSViewController { } } - 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) { + @IBAction private func perFileCheckboxClicked(_ sender: NSButton) { dataSource.aggregateByFile = (sender.state.rawValue == 1) tableView.reloadData() } - @IBAction func clipboardButtonClicked(_ sender: AnyObject) { + @IBAction private func clipboardButtonClicked(_ sender: AnyObject) { NSPasteboard.general.clearContents() NSPasteboard.general.writeObjects(["-Xfrontend -debug-time-function-bodies" as NSPasteboardWriting]) } - @IBAction func visitDerivedData(_ sender: AnyObject) { - NSWorkspace.shared.openFile(derivedDataTextField.stringValue) + @IBAction private func visitDerivedData(_ sender: AnyObject) { + guard let url = URL(string: derivedDataTextField.stringValue) else { return } + NSWorkspace.shared.open(url) } - @IBAction func cancelButtonClicked(_ sender: AnyObject) { + @IBAction private func cancelButtonClicked(_ sender: AnyObject) { processor.shouldCancel = true } - @IBAction func leftButtonClicked(_ sender: NSButton) { + @IBAction private func leftButtonClicked(_ sender: NSButton) { configureMenuItems(showBuildTimesMenuItem: true) cancelProcessing() @@ -162,35 +169,15 @@ class ViewController: NSViewController { projectSelection.listFolders() } - 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 + // MARK: - Private functions - func cancelProcessing() { - guard processingState == .processing else { return } - - processor.shouldCancel = true - cancelButton.isHidden = true - } - - func configureMenuItems(showBuildTimesMenuItem: Bool) { + private func configureMenuItems(showBuildTimesMenuItem: Bool) { if let appDelegate = NSApp.delegate as? AppDelegate { appDelegate.configureMenuItems(showBuildTimesMenuItem: showBuildTimesMenuItem) } } - func processLog(with database: XcodeDatabase) { + private func processLog(with database: XcodeDatabase) { guard processingState != .processing else { if let currentKey = currentKey, currentKey != database.key { nextDatabase = database @@ -211,7 +198,7 @@ class ViewController: NSViewController { } } - func handleProcessorUpdate(result: [CompileMeasure], didComplete: Bool, didCancel: Bool) { + private func handleProcessorUpdate(result: [CompileMeasure], didComplete: Bool, didCancel: Bool) { dataSource.resetSourceData(newSourceData: result) tableView.reloadData() @@ -220,7 +207,7 @@ class ViewController: NSViewController { } } - func completeProcessorUpdate(didCancel: Bool) { + private func completeProcessorUpdate(didCancel: Bool) { let didSucceed = !dataSource.isEmpty() var stateName = ProcessingState.failedString @@ -247,7 +234,7 @@ class ViewController: NSViewController { } } - func updateTotalLabel(with buildTime: Int) { + private func updateTotalLabel(with buildTime: Int) { let text = "Build duration: " + (buildTime < 60 ? "\(buildTime)s" : "\(buildTime / 60)m \(buildTime % 60)s") compileTimeTextField.stringValue = text } @@ -262,21 +249,22 @@ extension ViewController: NSTableViewDataSource { func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { guard let item = dataSource.measure(index: row) else { return false } - NSWorkspace.shared.openFile(item.path) - - 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.main.async { - if let script = NSAppleScript(source: gotoLineScript) { - script.executeAndReturnError(nil) + let url = URL(filePath: item.path) + NSWorkspace.shared.open(url, configuration: .init()) { _, error in + guard error == nil else { return } + 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" + Task.detached { + if let script = NSAppleScript(source: gotoLineScript) { + _ = script.executeAndReturnError(nil) + } } } @@ -301,6 +289,19 @@ extension ViewController: NSTableViewDelegate { dataSource.sortDescriptors = tableView.sortDescriptors tableView.reloadData() } + + 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: BuildManagerDelegate diff --git a/BuildTimeAnalyzer/ViewControllerDataSource.swift b/BuildTimeAnalyzer/ViewControllerDataSource.swift index d8865a5..86237bc 100644 --- a/BuildTimeAnalyzer/ViewControllerDataSource.swift +++ b/BuildTimeAnalyzer/ViewControllerDataSource.swift @@ -8,7 +8,7 @@ import Foundation -class ViewControllerDataSource { +final class ViewControllerDataSource { var aggregateByFile = false { didSet {