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<T>(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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.12</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2016 Cane Media Ltd. All rights reserved.</string>
<key>NSMainStoryboardFile</key>
<string>Main</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
================================================
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..<text.endIndex
let regex = try! NSRegularExpression(pattern: "^\\d*\\.?\\d*ms\\t/", options: [])
rawMeasures.removeAll()
processingDidStart()
while let nextRange = text.rangeOfCharacter(from: characterSet, options: [], range: remainingRange) {
let text = String(text[remainingRange.lowerBound..<nextRange.upperBound])
defer {
remainingRange = nextRange.upperBound..<remainingRange.upperBound
}
// From LuizZak: (text as NSString).length improves the performance by about 2x compared to text.characters.count
let range = NSMakeRange(0, (text as NSString).length)
guard let match = regex.firstMatch(in: text, options: [], range: range) else { continue }
let timeString = text[..<text.index(text.startIndex, offsetBy: match.range.length - 4)]
if let time = Double(timeString) {
let value = String(text[text.index(text.startIndex, offsetBy: match.range.length - 1)...])
if var rawMeasure = rawMeasures[value] {
rawMeasure.time += time
rawMeasure.references += 1
rawMeasures[value] = rawMeasure
} else {
rawMeasures[value] = RawMeasure(time: time, text: value)
}
}
guard !shouldCancel else { break }
}
processingDidFinish()
}
fileprivate func updateResults(didComplete completed: Bool, didCancel: Bool) {
var filteredResults = rawMeasures.values.filter{ $0.time > 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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="24506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24506"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Application-->
<scene sceneID="JPo-4y-FX3">
<objects>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="Build Time Analyzer" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Build Time Analyzer" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About Build Time Analyzer" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem title="GitHub Page" id="ZvE-gI-Jj2">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="visitGitHubPage:" target="Voe-Tx-rLC" id="OKx-FW-Ire"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide TestMenu" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit TestMenu" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="File" id="dMs-cI-mzQ">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="File" id="bib-Uj-vzu">
<items>
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
<connections>
<action selector="performClose:" target="Ady-hI-5gd" id="HmO-Ls-i7Q"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="Kk2-Jz-LKd"/>
<menuItem title="Export data as CSV…" keyEquivalent="e" id="3xt-bK-nQa">
<connections>
<action selector="exportAsCSVClicked:" target="Ady-hI-5gd" id="kbQ-xo-OtY"/>
<binding destination="Voe-Tx-rLC" name="enabled" keyPath="self.canExport" id="nAf-82-Fb2"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="Ady-hI-5gd" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Navigate" id="dzK-lU-FKH" userLabel="Navigate">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Navigate" autoenablesItems="NO" id="4cu-Mh-AKu">
<items>
<menuItem title="Project Selection" enabled="NO" keyEquivalent="0" id="JMA-rD-5Ql">
<connections>
<action selector="navigateToProjectSelection:" target="Voe-Tx-rLC" id="DMt-ga-nyF"/>
</connections>
</menuItem>
<menuItem title="Build Times" enabled="NO" keyEquivalent="1" id="SfY-cQ-Dau">
<connections>
<action selector="navigateToBuildTimes:" target="Voe-Tx-rLC" id="O18-0k-1fo"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="Ady-hI-5gd" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="Ady-hI-5gd" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="Ady-hI-5gd" id="DRN-fu-gQh"/>
</connections>
</menuItem>
<menuItem title="Always in Front" state="on" id="vT5-eJ-cMt">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAlwaysInFront:" target="Voe-Tx-rLC" id="a4N-d5-ic4"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="Xt6-Qh-l1j"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="BuildTimeAnalyzer" customModuleProvider="target">
<connections>
<outlet property="alwaysInFrontMenuItem" destination="vT5-eJ-cMt" id="hyc-ts-nzJ"/>
<outlet property="buildTimesMenuItem" destination="SfY-cQ-Dau" id="aXl-6m-zoY"/>
<outlet property="projectSelectionMenuItem" destination="JMA-rD-5Ql" id="ErS-4H-fO2"/>
</connections>
</customObject>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-33" y="-217"/>
</scene>
<!--Window Controller-->
<scene sceneID="R2V-B0-nI4">
<objects>
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
<window key="window" title="Build Time Analyzer" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="196" y="240" width="999" height="473"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
<value key="minSize" type="size" width="900" height="323"/>
<connections>
<outlet property="delegate" destination="B8D-0N-5wS" id="Rb5-sO-360"/>
</connections>
</window>
<connections>
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
</connections>
</windowController>
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="258" y="196"/>
</scene>
<!--View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="BuildTimeAnalyzer" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" misplaced="YES" id="TUB-te-74g">
<rect key="frame" x="0.0" y="0.0" width="994" height="451"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="cll-Z4-M4w">
<rect key="frame" x="20" y="389" width="34" height="24"/>
<buttonCell key="cell" type="push" bezelStyle="rounded" image="NSGoLeftTemplate" imagePosition="only" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="YqD-Wn-Ga3">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="leftButtonClicked:" target="XfG-lQ-9wD" id="A8o-kz-ykA"/>
</connections>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="wGP-fY-d6m">
<rect key="frame" x="62" y="393" width="51" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Status: " id="Mm0-J1-a5S">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="RvS-xK-GoL">
<rect key="frame" x="109" y="393" width="83" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Processing..." id="9P7-Sg-abF">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<progressIndicator hidden="YES" wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="xEn-wZ-b65">
<rect key="frame" x="195" y="393" width="16" height="16"/>
</progressIndicator>
<button hidden="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Phm-qp-QGE">
<rect key="frame" x="221" y="389" width="66" height="24"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="eJH-bZ-4cr">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancelButtonClicked:" target="XfG-lQ-9wD" id="pcf-gG-1AX"/>
</connections>
</button>
<searchField wantsLayer="YES" focusRingType="none" horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="bBY-pQ-ugQ">
<rect key="frame" x="231" y="389" width="567" height="24"/>
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" id="tSv-aC-SRU">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</searchFieldCell>
<connections>
<outlet property="delegate" destination="XfG-lQ-9wD" id="b1G-WP-MAV"/>
</connections>
</searchField>
<button translatesAutoresizingMaskIntoConstraints="NO" id="N1E-R8-mNr">
<rect key="frame" x="808" y="394" width="60" height="14"/>
<buttonCell key="cell" type="check" title="per file" bezelStyle="regularSquare" imagePosition="left" inset="2" id="zIu-rY-2Qd">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<constraints>
<constraint firstAttribute="height" constant="14" id="Ev3-lW-a4O"/>
<constraint firstAttribute="width" constant="60" id="rhf-80-RU9"/>
</constraints>
<connections>
<action selector="perFileCheckboxClicked:" target="XfG-lQ-9wD" id="Pxx-d4-CZO"/>
</connections>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Px1-f3-h0i">
<rect key="frame" x="886" y="394" width="90" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Build Time: 0s" id="YXf-T1-4h3">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<scrollView hidden="YES" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="K1Q-qf-FZ3">
<rect key="frame" x="20" y="20" width="954" height="354"/>
<clipView key="contentView" id="UfE-vR-QZu">
<rect key="frame" x="1" y="1" width="952" height="352"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnSelection="YES" multipleSelection="NO" autosaveColumns="NO" rowSizeStyle="automatic" headerView="zp0-vs-Vct" viewBased="YES" id="Wag-HG-nHe">
<rect key="frame" x="0.0" y="0.0" width="952" height="329"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn width="90" minWidth="40" maxWidth="1000" id="g67-YV-my9">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Cumulative time">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="FF6-Rn-rW6">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="Cell0" misplaced="YES" id="8cX-uP-CmA">
<rect key="frame" x="1" y="1" width="95" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="JjP-9o-frO">
<rect key="frame" x="-2" y="0.0" width="99" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table " id="suh-0M-gYO">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="JjP-9o-frO" secondAttribute="trailing" id="6Eh-ns-FoN"/>
<constraint firstItem="JjP-9o-frO" firstAttribute="top" secondItem="8cX-uP-CmA" secondAttribute="top" id="N9I-Ay-0V3"/>
<constraint firstItem="JjP-9o-frO" firstAttribute="leading" secondItem="8cX-uP-CmA" secondAttribute="leading" id="nap-PT-9Xu"/>
<constraint firstAttribute="bottom" secondItem="JjP-9o-frO" secondAttribute="bottom" id="tXV-IA-hXr"/>
</constraints>
<connections>
<outlet property="textField" destination="JjP-9o-frO" id="1b8-wn-HmW"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn width="250" minWidth="40" maxWidth="1000" id="I8k-1E-YFi">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Location">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="v7P-IT-kt6">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="Cell1" misplaced="YES" id="rtd-z4-z3H">
<rect key="frame" x="99" y="1" width="250" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="gaM-Z3-6r5">
<rect key="frame" x="-2" y="0.0" width="254" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="ShZ-hG-2RX">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="gaM-Z3-6r5" secondAttribute="trailing" id="CST-mz-Cev"/>
<constraint firstItem="gaM-Z3-6r5" firstAttribute="top" secondItem="rtd-z4-z3H" secondAttribute="top" id="Mhl-GM-DcR"/>
<constraint firstAttribute="bottom" secondItem="gaM-Z3-6r5" secondAttribute="bottom" id="OfP-Vv-Oww"/>
<constraint firstItem="gaM-Z3-6r5" firstAttribute="leading" secondItem="rtd-z4-z3H" secondAttribute="leading" id="cWF-CT-nmB"/>
</constraints>
<connections>
<outlet property="textField" destination="gaM-Z3-6r5" id="VAL-7L-QYZ"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn width="73" minWidth="10" maxWidth="3.4028234663852886e+38" id="pNQ-e7-chu">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Occurrences">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="2FU-BF-t5B">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="Cell2" id="Nfh-T6-pKh">
<rect key="frame" x="352" y="1" width="73" height="40"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="0iN-e0-gn8">
<rect key="frame" x="0.0" y="12" width="109" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="NrA-Gj-lWm">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="0iN-e0-gn8" firstAttribute="centerY" secondItem="Nfh-T6-pKh" secondAttribute="centerY" id="IdL-Mf-JRi"/>
<constraint firstAttribute="trailing" secondItem="0iN-e0-gn8" secondAttribute="trailing" constant="-34" id="LwB-EE-cDe"/>
<constraint firstItem="0iN-e0-gn8" firstAttribute="leading" secondItem="Nfh-T6-pKh" secondAttribute="leading" constant="2" id="qbx-Wq-paT"/>
</constraints>
<connections>
<outlet property="textField" destination="0iN-e0-gn8" id="hYB-5j-Xqa"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn width="518" minWidth="40" maxWidth="1000" id="Rka-lG-OGR">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Function">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="HHv-yh-Ikp">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="Cell3" misplaced="YES" id="t5F-Tt-KtO">
<rect key="frame" x="428" y="1" width="522" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="MfB-eS-1vB">
<rect key="frame" x="-2" y="0.0" width="526" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="lez-yt-53a">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="MfB-eS-1vB" firstAttribute="top" secondItem="t5F-Tt-KtO" secondAttribute="top" id="4aR-hk-T5b"/>
<constraint firstAttribute="trailing" secondItem="MfB-eS-1vB" secondAttribute="trailing" id="5Kg-44-yUS"/>
<constraint firstAttribute="bottom" secondItem="MfB-eS-1vB" secondAttribute="bottom" id="WWU-T4-LN2"/>
<constraint firstItem="MfB-eS-1vB" firstAttribute="leading" secondItem="t5F-Tt-KtO" secondAttribute="leading" id="hzi-Kk-Wyb"/>
</constraints>
<connections>
<outlet property="textField" destination="MfB-eS-1vB" id="ZWE-af-66X"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
</tableColumns>
<connections>
<outlet property="dataSource" destination="XfG-lQ-9wD" id="gzU-SG-v2N"/>
<outlet property="delegate" destination="XfG-lQ-9wD" id="kPR-hV-RmL"/>
</connections>
</tableView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="QJr-l7-VY7">
<rect key="frame" x="1" y="363" width="952" height="17"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="7gW-pK-P4I">
<rect key="frame" x="224" y="17" width="15" height="102"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<tableHeaderView key="headerView" wantsLayer="YES" id="zp0-vs-Vct">
<rect key="frame" x="0.0" y="0.0" width="952" height="23"/>
<autoresizingMask key="autoresizingMask"/>
</tableHeaderView>
</scrollView>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="wyZ-eU-XEt">
<rect key="frame" x="0.0" y="0.0" width="994" height="424"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="x1K-8w-91Y">
<rect key="frame" x="18" y="382" width="97" height="20"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Instructions" id="9Ga-i4-eZ4">
<font key="font" metaFont="systemMedium" size="17"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="c6J-cx-fi8">
<rect key="frame" x="189" y="384" width="404" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="1) Ensure the below flags are added to your target's Build Settings" id="R8V-zK-2ya">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="NbS-C3-WfY">
<rect key="frame" x="606" y="380" width="92" height="24"/>
<buttonCell key="cell" type="push" title="Copy flag 1" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="s9H-YB-qWq">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="clipboardButtonClicked:" target="XfG-lQ-9wD" id="xYh-dG-c8k"/>
</connections>
</button>
<button verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="032-pL-jcU">
<rect key="frame" x="1200" y="211" width="132" height="24"/>
<buttonCell key="cell" type="push" title="Copy flag 2" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ZfB-8n-cDW">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="clipboardButton2Clicked:" target="XfG-lQ-9wD" id="Fw6-Hi-c3r"/>
</connections>
</button>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Um0-zQ-09O">
<rect key="frame" x="191" y="307" width="612" height="69"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ScreenShot" id="vJs-DY-m0y"/>
</imageView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Zkv-Tf-sdq">
<rect key="frame" x="189" y="256" width="224" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="2) Clean your project (⌘ + Shift + K)" id="eMR-lR-OuM">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="PV7-TD-diE">
<rect key="frame" x="189" y="253" width="339" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="3) Build your project (⌘ + B) and wait for it to complete" id="WcG-TO-qa4">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="O5K-4i-z6g">
<rect key="frame" x="189" y="154" width="245" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Alternatively, choose an existing project" id="j64-c6-4L7">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<scrollView wantsLayer="YES" autohidesScrollers="YES" horizontalLineScroll="27" horizontalPageScroll="10" verticalLineScroll="27" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ddp-S7-yG2">
<rect key="frame" x="191" y="77" width="612" height="120"/>
<clipView key="contentView" id="hf9-7R-VGA">
<rect key="frame" x="1" y="1" width="610" height="118"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnSelection="YES" multipleSelection="NO" autosaveColumns="NO" rowHeight="25" headerView="aAe-a7-ga6" viewBased="YES" id="vE3-G0-gZw">
<rect key="frame" x="0.0" y="0.0" width="610" height="95"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn width="380" minWidth="40" maxWidth="1000" id="EHR-zb-SmS">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Name">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="5Pm-ZP-JFn">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="Cell0" id="Od6-nA-b4s">
<rect key="frame" x="1" y="1" width="385" height="40"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eRA-xw-ZhX">
<rect key="frame" x="0.0" y="23" width="385" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="vAr-Ew-aeF">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<connections>
<outlet property="textField" destination="eRA-xw-ZhX" id="5lq-L9-C1L"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn width="215" minWidth="40" maxWidth="1000" id="mpO-wX-dkq">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Date">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="suz-qG-dIq">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="Cell1" id="rFj-Lv-vAO">
<rect key="frame" x="389" y="1" width="219" height="40"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ukd-Oi-mjC">
<rect key="frame" x="0.0" y="23" width="219" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="7jY-Q2-1y9">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<connections>
<outlet property="textField" destination="Ukd-Oi-mjC" id="adD-ul-2zd"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
</tableColumns>
<connections>
<action selector="didSelectCell:" target="Ffg-BK-bSD" id="l29-b7-dcK"/>
<outlet property="dataSource" destination="Ffg-BK-bSD" id="SjW-4d-Dgm"/>
<outlet property="delegate" destination="Ffg-BK-bSD" id="L1S-A9-dAn"/>
</connections>
</tableView>
</subviews>
</clipView>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="120" id="V1C-tO-aNA"/>
</constraints>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="YXc-0k-3G9">
<rect key="frame" x="1" y="102" width="610" height="17"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="OkT-YA-gco">
<rect key="frame" x="224" y="17" width="15" height="102"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<tableHeaderView key="headerView" wantsLayer="YES" id="aAe-a7-ga6">
<rect key="frame" x="0.0" y="0.0" width="610" height="23"/>
<autoresizingMask key="autoresizingMask"/>
</tableHeaderView>
</scrollView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MtA-kf-7kN">
<rect key="frame" x="18" y="31" width="142" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Derived Data Location:" id="TXm-dt-Qp1">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" verticalCompressionResistancePriority="751" translatesAutoresizingMaskIntoConstraints="NO" id="ptj-Cb-LDq">
<rect key="frame" x="191" y="27" width="612" height="24"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="SDJ-mZ-FgV">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="XfG-lQ-9wD" id="kcF-Uo-l1I"/>
</connections>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="coU-tR-Iyy">
<rect key="frame" x="811" y="33" width="22" height="12"/>
<buttonCell key="cell" type="inline" bezelStyle="inline" image="NSFollowLinkFreestandingTemplate" imagePosition="only" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="zPS-Vu-Noe">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystemBold"/>
</buttonCell>
<connections>
<action selector="visitDerivedData:" target="XfG-lQ-9wD" id="HW7-Il-IM3"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="ddp-S7-yG2" firstAttribute="trailing" secondItem="Um0-zQ-09O" secondAttribute="trailing" id="7s6-Yg-1kY"/>
<constraint firstItem="NbS-C3-WfY" firstAttribute="leading" secondItem="c6J-cx-fi8" secondAttribute="trailing" constant="15" id="8SE-Qr-vIk"/>
<constraint firstItem="x1K-8w-91Y" firstAttribute="centerY" secondItem="c6J-cx-fi8" secondAttribute="centerY" id="93U-LK-sEV"/>
<constraint firstItem="ptj-Cb-LDq" firstAttribute="leading" secondItem="ddp-S7-yG2" secondAttribute="leading" id="9ZQ-n0-bEL"/>
<constraint firstItem="PV7-TD-diE" firstAttribute="top" secondItem="Zkv-Tf-sdq" secondAttribute="bottom" constant="35" id="BO5-sq-vNo"/>
<constraint firstItem="Um0-zQ-09O" firstAttribute="top" secondItem="c6J-cx-fi8" secondAttribute="bottom" constant="8" id="CCC-q5-86U"/>
<constraint firstItem="O5K-4i-z6g" firstAttribute="leading" secondItem="PV7-TD-diE" secondAttribute="leading" id="Dpn-sp-Fni"/>
<constraint firstItem="032-pL-jcU" firstAttribute="leading" secondItem="NbS-C3-WfY" secondAttribute="trailing" constant="6" id="Ic5-61-Mq9"/>
<constraint firstItem="c6J-cx-fi8" firstAttribute="leading" secondItem="Um0-zQ-09O" secondAttribute="leading" id="NIy-Q9-e38"/>
<constraint firstItem="Um0-zQ-09O" firstAttribute="centerX" secondItem="wyZ-eU-XEt" secondAttribute="centerX" id="O4l-6w-kjo"/>
<constraint firstItem="coU-tR-Iyy" firstAttribute="leading" secondItem="ptj-Cb-LDq" secondAttribute="trailing" constant="8" id="Ou4-bQ-rT0"/>
<constraint firstItem="ddp-S7-yG2" firstAttribute="leading" secondItem="Um0-zQ-09O" secondAttribute="leading" id="QZf-de-QEb"/>
<constraint firstItem="ptj-Cb-LDq" firstAttribute="trailing" secondItem="ddp-S7-yG2" secondAttribute="trailing" id="QsI-Oh-lIn"/>
<constraint firstItem="PV7-TD-diE" firstAttribute="leading" secondItem="Zkv-Tf-sdq" secondAttribute="leading" id="U3M-6g-IlA"/>
<constraint firstItem="MtA-kf-7kN" firstAttribute="top" secondItem="ddp-S7-yG2" secondAttribute="bottom" constant="30" id="Wsv-7h-dXk"/>
<constraint firstItem="Zkv-Tf-sdq" firstAttribute="leading" secondItem="Um0-zQ-09O" secondAttribute="leading" id="dMc-8a-uJH"/>
<constraint firstItem="032-pL-jcU" firstAttribute="centerY" secondItem="NbS-C3-WfY" secondAttribute="centerY" id="dgp-d4-cU5"/>
<constraint firstItem="MtA-kf-7kN" firstAttribute="leading" secondItem="x1K-8w-91Y" secondAttribute="leading" id="doX-4F-aoa"/>
<constraint firstItem="x1K-8w-91Y" firstAttribute="leading" secondItem="wyZ-eU-XEt" secondAttribute="leading" constant="20" id="fYD-Gu-iO9"/>
<constraint firstItem="ptj-Cb-LDq" firstAttribute="centerY" secondItem="MtA-kf-7kN" secondAttribute="centerY" id="hWs-e1-6dI"/>
<constraint firstAttribute="trailing" secondItem="ddp-S7-yG2" secondAttribute="trailing" constant="191" id="jvl-63-wCZ"/>
<constraint firstItem="NbS-C3-WfY" firstAttribute="centerY" secondItem="c6J-cx-fi8" secondAttribute="centerY" id="lGh-3Z-UEi"/>
<constraint firstAttribute="bottom" secondItem="ptj-Cb-LDq" secondAttribute="bottom" constant="27" id="nQn-b1-UO6"/>
<constraint firstItem="ddp-S7-yG2" firstAttribute="top" secondItem="O5K-4i-z6g" secondAttribute="bottom" constant="5" id="oCY-Ao-8bF"/>
<constraint firstItem="Zkv-Tf-sdq" firstAttribute="top" secondItem="Um0-zQ-09O" secondAttribute="bottom" constant="35" id="oan-Z5-mK3"/>
<constraint firstItem="coU-tR-Iyy" firstAttribute="centerY" secondItem="ptj-Cb-LDq" secondAttribute="centerY" id="rN7-rQ-Q8B"/>
<constraint firstItem="NbS-C3-WfY" firstAttribute="top" secondItem="wyZ-eU-XEt" secondAttribute="top" constant="20" id="rsF-no-xAd"/>
<constraint firstItem="O5K-4i-z6g" firstAttribute="top" secondItem="PV7-TD-diE" secondAttribute="bottom" constant="35" id="wss-bT-SAy"/>
</constraints>
</customView>
</subviews>
<constraints>
<constraint firstItem="cll-Z4-M4w" firstAttribute="leading" secondItem="TUB-te-74g" secondAttribute="leading" constant="20" id="6I5-O1-qtA"/>
<constraint firstItem="Px1-f3-h0i" firstAttribute="centerY" secondItem="N1E-R8-mNr" secondAttribute="centerY" constant="-1" id="6wl-rn-DSR"/>
<constraint firstItem="RvS-xK-GoL" firstAttribute="leading" secondItem="wGP-fY-d6m" secondAttribute="trailing" id="97q-Nd-g4K"/>
<constraint firstAttribute="bottom" secondItem="wyZ-eU-XEt" secondAttribute="bottom" id="9Tx-hc-Cr8"/>
<constraint firstItem="wyZ-eU-XEt" firstAttribute="leading" secondItem="TUB-te-74g" secondAttribute="leading" id="9VV-Zf-ztM"/>
<constraint firstItem="N1E-R8-mNr" firstAttribute="leading" secondItem="bBY-pQ-ugQ" secondAttribute="trailing" constant="10" id="9xK-9A-L8b"/>
<constraint firstItem="xEn-wZ-b65" firstAttribute="leading" secondItem="RvS-xK-GoL" secondAttribute="trailing" constant="5" id="CWs-Zw-U9N"/>
<constraint firstItem="Phm-qp-QGE" firstAttribute="leading" secondItem="xEn-wZ-b65" secondAttribute="trailing" constant="10" id="HCc-AM-YeT"/>
<constraint firstItem="Px1-f3-h0i" firstAttribute="leading" secondItem="N1E-R8-mNr" secondAttribute="trailing" constant="20" id="IWD-nK-o2R"/>
<constraint firstAttribute="trailing" secondItem="K1Q-qf-FZ3" secondAttribute="trailing" constant="20" id="MHW-8Q-xwg"/>
<constraint firstItem="RvS-xK-GoL" firstAttribute="centerY" secondItem="wGP-fY-d6m" secondAttribute="centerY" id="QHM-Pg-2ag"/>
<constraint firstItem="K1Q-qf-FZ3" firstAttribute="top" secondItem="TUB-te-74g" secondAttribute="top" constant="50" id="Qav-aG-fZq"/>
<constraint firstItem="bBY-pQ-ugQ" firstAttribute="centerY" secondItem="cll-Z4-M4w" secondAttribute="centerY" id="Roh-wf-zeZ"/>
<constraint firstItem="xEn-wZ-b65" firstAttribute="centerY" secondItem="RvS-xK-GoL" secondAttribute="centerY" id="UO8-dG-b6I"/>
<constraint firstItem="K1Q-qf-FZ3" firstAttribute="leading" secondItem="TUB-te-74g" secondAttribute="leading" constant="20" id="W02-7b-1rF"/>
<constraint firstItem="bBY-pQ-ugQ" firstAttribute="leading" secondItem="Um0-zQ-09O" secondAttribute="leading" constant="40" id="XGm-QC-YLA"/>
<constraint firstItem="N1E-R8-mNr" firstAttribute="centerY" secondItem="bBY-pQ-ugQ" secondAttribute="centerY" id="ec0-jZ-yN9"/>
<constraint firstAttribute="trailing" secondItem="Px1-f3-h0i" secondAttribute="trailing" constant="20" id="ftF-AU-XEl"/>
<constraint firstAttribute="trailing" secondItem="wyZ-eU-XEt" secondAttribute="trailing" id="fvn-gV-Dbr"/>
<constraint firstItem="wyZ-eU-XEt" firstAttribute="top" secondItem="TUB-te-74g" secondAttribute="top" id="gBL-BQ-VHN"/>
<constraint firstItem="wGP-fY-d6m" firstAttribute="leading" secondItem="cll-Z4-M4w" secondAttribute="trailing" constant="10" id="slo-AQ-lkr"/>
<constraint firstItem="cll-Z4-M4w" firstAttribute="centerY" secondItem="wGP-fY-d6m" secondAttribute="centerY" id="tTb-24-hhh"/>
<constraint firstAttribute="bottom" secondItem="K1Q-qf-FZ3" secondAttribute="bottom" constant="20" id="tYg-T3-Agj"/>
<constraint firstItem="wGP-fY-d6m" firstAttribute="top" secondItem="TUB-te-74g" secondAttribute="top" constant="15" id="xMr-bP-2jr"/>
<constraint firstItem="Phm-qp-QGE" firstAttribute="centerY" secondItem="xEn-wZ-b65" secondAttribute="centerY" id="zhM-OK-89S"/>
</constraints>
</view>
<connections>
<outlet property="buildManager" destination="f4z-Qu-43g" id="aek-kF-HmQ"/>
<outlet property="cancelButton" destination="Phm-qp-QGE" id="q44-19-Lli"/>
<outlet property="compileTimeTextField" destination="Px1-f3-h0i" id="XbZ-D3-wwe"/>
<outlet property="derivedDataTextField" destination="ptj-Cb-LDq" id="Hqz-Sh-zea"/>
<outlet property="instructionsView" destination="wyZ-eU-XEt" id="CSN-0u-vnQ"/>
<outlet property="leftButton" destination="cll-Z4-M4w" id="dZK-eZ-LHa"/>
<outlet property="perFileButton" destination="N1E-R8-mNr" id="CAb-hw-afy"/>
<outlet property="progressIndicator" destination="xEn-wZ-b65" id="ISf-c4-IX4"/>
<outlet property="projectSelection" destination="Ffg-BK-bSD" id="0N9-RU-Xyw"/>
<outlet property="searchField" destination="bBY-pQ-ugQ" id="Eqb-dJ-Eov"/>
<outlet property="statusLabel" destination="wGP-fY-d6m" id="ZJT-MH-vIf"/>
<outlet property="statusTextField" destination="RvS-xK-GoL" id="Vew-jo-Hex"/>
<outlet property="tableView" destination="Wag-HG-nHe" id="TKE-UK-M6w"/>
<outlet property="tableViewContainerView" destination="K1Q-qf-FZ3" id="ufv-Ah-QDA"/>
</connections>
</viewController>
<customObject id="Ffg-BK-bSD" customClass="ProjectSelection" customModule="BuildTimeAnalyzer" customModuleProvider="target">
<connections>
<outlet property="tableView" destination="vE3-G0-gZw" id="Mkh-Ri-cQN"/>
</connections>
</customObject>
<customObject id="f4z-Qu-43g" customClass="BuildManager" customModule="BuildTimeAnalyzer" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="258" y="933"/>
</scene>
</scenes>
<resources>
<image name="NSFollowLinkFreestandingTemplate" width="20" height="20"/>
<image name="NSGoLeftTemplate" width="12" height="17"/>
<image name="ScreenShot" width="616" height="69"/>
</resources>
</document>
================================================
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<UInt8>.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<compression_stream>.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 = "<group>"; };
2839B86A1FD32766004C075C /* ViewControllerDataSourceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerDataSourceTest.swift; sourceTree = "<group>"; };
2A3164C01D21D73F00064045 /* CompileMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompileMeasure.swift; sourceTree = "<group>"; };
2A3164C11D21D73F00064045 /* LogProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogProcessor.swift; sourceTree = "<group>"; };
2A3164C31D21D73F00064045 /* ProcessingState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessingState.swift; sourceTree = "<group>"; };
2A3164C41D21D73F00064045 /* RawMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawMeasure.swift; sourceTree = "<group>"; };
2A3164CF1D21D74A00064045 /* CompileMeasureTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompileMeasureTests.swift; sourceTree = "<group>"; };
2A3698A91D80A1AC002C5CDA /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = "<group>"; };
2A3698AB1D80A33B002C5CDA /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
2A5404001D86D01700DBD44C /* BuildManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildManager.swift; sourceTree = "<group>"; };
2A5404021D86DE0C00DBD44C /* XcodeDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XcodeDatabase.swift; sourceTree = "<group>"; };
2A5404041D86F3C700DBD44C /* File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = "<group>"; };
2A9807DC1D7C71F900B9232C /* DirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryMonitor.swift; sourceTree = "<group>"; };
2A9807DE1D7C76FD00B9232C /* ProjectSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectSelection.swift; sourceTree = "<group>"; };
2ABFB6CD1D81F2DE00D060BF /* NSAlert+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAlert+Extensions.swift"; sourceTree = "<group>"; };
2ACBFD0B1D8835E60009567E /* UserSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = "<group>"; };
2AE775111D225D5D00D1A744 /* DerivedDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DerivedDataManager.swift; sourceTree = "<group>"; };
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 = "<group>"; };
2AF821451D21D6B900D65186 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = BuildTimeAnalyzer/Assets.xcassets; sourceTree = "<group>"; };
2AF8214A1D21D6B900D65186 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
5603EB6121EF93E90013D77B /* CSVExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVExporter.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
};
2A3164D71D21D7A800064045 /* Supporting Files */ = {
isa = PBXGroup;
children = (
2AF821551D21D6B900D65186 /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
2ABFB6CF1D81F34300D060BF /* Extensions */ = {
isa = PBXGroup;
children = (
2ABFB6CD1D81F2DE00D060BF /* NSAlert+Extensions.swift */,
);
name = Extensions;
sourceTree = "<group>";
};
2ABFB6D01D81F35400D060BF /* Application */ = {
isa = PBXGroup;
children = (
2AF821431D21D6B900D65186 /* AppDelegate.swift */,
2A3698A91D80A1AC002C5CDA /* Main.storyboard */,
);
name = Application;
sourceTree = "<group>";
};
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 = "<group>";
};
2ABFB6D21D81F81400D060BF /* ViewControllers */ = {
isa = PBXGroup;
children = (
2A3698AB1D80A33B002C5CDA /* ViewController.swift */,
);
name = ViewControllers;
sourceTree = "<group>";
};
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 = "<group>";
};
2AF821371D21D6B900D65186 = {
isa = PBXGroup;
children = (
2AF821421D21D6B900D65186 /* BuildTimeAnalyzer */,
2AF821451D21D6B900D65186 /* Assets.xcassets */,
2AF821521D21D6B900D65186 /* BuildTimeAnalyzerTests */,
2AF821411D21D6B900D65186 /* Products */,
);
sourceTree = "<group>";
};
2AF821411D21D6B900D65186 /* Products */ = {
isa = PBXGroup;
children = (
2AF821401D21D6B900D65186 /* BuildTimeAnalyzer.app */,
2AF8214F1D21D6B900D65186 /* BuildTimeAnalyzerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
2AF821421D21D6B900D65186 /* BuildTimeAnalyzer */ = {
isa = PBXGroup;
children = (
2ABFB6D01D81F35400D060BF /* Application */,
2ABFB6CF1D81F34300D060BF /* Extensions */,
2ABFB6D11D81F37300D060BF /* Models */,
2A3164D11D21D74F00064045 /* Supporting Files */,
2ABFB6D31D81F82600D060BF /* Utilities */,
2ABFB6D21D81F81400D060BF /* ViewControllers */,
);
path = BuildTimeAnalyzer;
sourceTree = "<group>";
};
2AF821521D21D6B900D65186 /* BuildTimeAnalyzerTests */ = {
isa = PBXGroup;
children = (
2A3164CF1D21D74A00064045 /* CompileMeasureTests.swift */,
2839B86A1FD32766004C075C /* ViewControllerDataSourceTest.swift */,
2A3164D71D21D7A800064045 /* Supporting Files */,
);
path = BuildTimeAnalyzerTests;
sourceTree = "<group>";
};
/* 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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:BuildTimeAnalyzer.xcodeproj">
</FileRef>
</Workspace>
================================================
FILE: BuildTimeAnalyzer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
================================================
FILE: BuildTimeAnalyzer.xcodeproj/xcshareddata/xcschemes/BuildTimeAnalyzer.xcscheme
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2630"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2AF8213F1D21D6B900D65186"
BuildableName = "BuildTimeAnalyzer.app"
BlueprintName = "BuildTimeAnalyzer"
ReferencedContainer = "container:BuildTimeAnalyzer.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2AF8213F1D21D6B900D65186"
BuildableName = "BuildTimeAnalyzer.app"
BlueprintName = "BuildTimeAnalyzer"
ReferencedContainer = "container:BuildTimeAnalyzer.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2AF8214E1D21D6B900D65186"
BuildableName = "BuildTimeAnalyzerTests.xctest"
BlueprintName = "BuildTimeAnalyzerTests"
ReferencedContainer = "container:BuildTimeAnalyzer.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2AF8213F1D21D6B900D65186"
BuildableName = "BuildTimeAnalyzer.app"
BlueprintName = "BuildTimeAnalyzer"
ReferencedContainer = "container:BuildTimeAnalyzer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2AF8213F1D21D6B900D65186"
BuildableName = "BuildTimeAnalyzer.app"
BlueprintName = "BuildTimeAnalyzer"
ReferencedContainer = "container:BuildTimeAnalyzer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
================================================
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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
================================================
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
======================
[](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/releases/latest)
[](https://swift.org)
[](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode)
[](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.

## 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.
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
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (184K chars).
[
{
"path": ".gitignore",
"chars": 1388,
"preview": "# Xcode\n#\n# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore\n\n"
},
{
"path": ".travis.yml",
"chars": 129,
"preview": "language: swift\nosx_image: xcode9.3\nscript: xcodebuild -project BuildTimeAnalyzer.xcodeproj -scheme BuildTimeAnalyzer bu"
},
{
"path": "BuildTimeAnalyzer/AppDelegate.swift",
"chars": 1875,
"preview": "//\n// AppDelegate.swift\n// BuildTimeAnalyzer\n//\n\nimport Cocoa\n\n@NSApplicationMain\nclass AppDelegate: NSObject, NSAppli"
},
{
"path": "BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 1248,
"preview": "{\n \"images\" : [\n {\n \"size\" : \"16x16\",\n \"idiom\" : \"mac\",\n \"filename\" : \"logo16.png\",\n \"scale\" : \""
},
{
"path": "BuildTimeAnalyzer/Assets.xcassets/Contents.json",
"chars": 62,
"preview": "{\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n }\n}"
},
{
"path": "BuildTimeAnalyzer/Assets.xcassets/ScreenShot.imageset/Contents.json",
"chars": 308,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"filename\" : \"Screen Shot.png\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "BuildTimeAnalyzer/BuildManager.swift",
"chars": 2544,
"preview": "//\n// BuildManager.swift\n// BuildTimeAnalyzer\n//\n\nimport Cocoa\n\nprotocol BuildManagerDelegate: AnyObject {\n func de"
},
{
"path": "BuildTimeAnalyzer/BuildTimeAnalyzer-Bridging-Header.h",
"chars": 127,
"preview": "//\n// Use this file to import your target's public headers that you would like to expose to Swift.\n//\n\n#import \"NSData+"
},
{
"path": "BuildTimeAnalyzer/CSVExporter.swift",
"chars": 1988,
"preview": "//\n// CSVExporter.swift\n// BuildTimeAnalyzer\n//\n// Created by Bruno Resende on 16.01.19.\n// Copyright © 2019 Cane Me"
},
{
"path": "BuildTimeAnalyzer/CompileMeasure.swift",
"chars": 2709,
"preview": "//\n// CompileMeasure.swift\n// BuildTimeAnalyzer\n//\n\nimport Foundation\n\n@objcMembers class CompileMeasure: NSObject {\n "
},
{
"path": "BuildTimeAnalyzer/DerivedDataManager.swift",
"chars": 1324,
"preview": "//\n// DerivedDataManager.swift\n// BuildTimeAnalyzer\n//\n\nimport Foundation\n\nclass DerivedDataManager {\n \n static "
},
{
"path": "BuildTimeAnalyzer/DirectoryMonitor.swift",
"chars": 2484,
"preview": "//\n// DirectoryMonitor.swift\n// BuildTimeAnalyzer\n//\n\nimport Foundation\n\nprotocol DirectoryMonitorDelegate: AnyObject "
},
{
"path": "BuildTimeAnalyzer/File.swift",
"chars": 115,
"preview": "//\n// File.swift\n// BuildTimeAnalyzer\n//\n\nimport Foundation\n\nstruct File {\n let date: Date\n let url: URL\n}\n"
},
{
"path": "BuildTimeAnalyzer/Info.plist",
"chars": 1091,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "BuildTimeAnalyzer/LogProcessor.swift",
"chars": 4943,
"preview": "//\n// LogProcessor.swift\n// BuildTimeAnalyzer\n//\n\nimport Foundation\n\ntypealias CMUpdateClosure = (_ result: [CompileMe"
},
{
"path": "BuildTimeAnalyzer/Main.storyboard",
"chars": 80507,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB\" version=\"3.0\" t"
},
{
"path": "BuildTimeAnalyzer/NSAlert+Extensions.swift",
"chars": 417,
"preview": "//\n// NSAlert+Extensions.swift\n// BuildTimeAnalyzer\n//\n\nimport Cocoa\n\nextension NSAlert {\n static func show(withMes"
},
{
"path": "BuildTimeAnalyzer/ProcessingState.swift",
"chars": 833,
"preview": "//\n// ProcessingState.swift\n// BuildTimeAnalyzer\n//\n\nenum ProcessingState {\n case processing\n case waiting\n c"
},
{
"path": "BuildTimeAnalyzer/ProjectSelection.swift",
"chars": 2173,
"preview": "//\n// ProjectSelection.swift\n// BuildTimeAnalyzer\n//\n\nimport Cocoa\n\nprotocol ProjectSelectionDelegate: AnyObject {\n "
},
{
"path": "BuildTimeAnalyzer/RawMeasure.swift",
"chars": 654,
"preview": "//\n// RawMeasure.swift\n// BuildTimeAnalyzer\n//\n\nimport Foundation\n\nstruct RawMeasure {\n var time: Double\n var te"
},
{
"path": "BuildTimeAnalyzer/UserSettings.swift",
"chars": 1580,
"preview": "//\n// UserCache.swift\n// BuildTimeAnalyzer\n//\n\nimport Foundation\n\nclass UserSettings {\n \n static private let der"
},
{
"path": "BuildTimeAnalyzer/ViewController.swift",
"chars": 12742,
"preview": "//\n// ViewController.swift\n// BuildTimeAnalyzer\n//\n\nimport Cocoa\n\nclass ViewController: NSViewController {\n \n @I"
},
{
"path": "BuildTimeAnalyzer/ViewControllerDataSource.swift",
"chars": 2760,
"preview": "//\n// ViewControllerDataSource.swift\n// BuildTimeAnalyzer\n//\n// Created by Dmitrii on 02/12/2017.\n// Copyright © 201"
},
{
"path": "BuildTimeAnalyzer/XcodeDatabase.swift",
"chars": 4707,
"preview": "//\n// XcodeDatabase.swift\n// BuildTimeAnalyzer\n//\n\nimport Foundation\nimport Compression\n\nstruct XcodeDatabase {\n va"
},
{
"path": "BuildTimeAnalyzer.xcodeproj/project.pbxproj",
"chars": 25369,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "BuildTimeAnalyzer.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 162,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:BuildTimeAnalyz"
},
{
"path": "BuildTimeAnalyzer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "BuildTimeAnalyzer.xcodeproj/xcshareddata/xcschemes/BuildTimeAnalyzer.xcscheme",
"chars": 3777,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"2630\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "BuildTimeAnalyzerTests/CompileMeasureTests.swift",
"chars": 1219,
"preview": "//\n// CompileMeasureTests.swift\n// CMBuildTimeAnalyzerTests\n//\n\nimport XCTest\n\n@testable import BuildTimeAnalyzer\n\ncla"
},
{
"path": "BuildTimeAnalyzerTests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "BuildTimeAnalyzerTests/ViewControllerDataSourceTest.swift",
"chars": 8225,
"preview": "//\n// ViewControllerDataSourceTest.swift\n// BuildTimeAnalyzerTests\n//\n// Created by Dmitrii on 02/12/2017.\n// Copyri"
},
{
"path": "LICENSE",
"chars": 1083,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Robert Gummesson\n\nPermission is hereby granted, free of charge, to any person "
},
{
"path": "README.md",
"chars": 2580,
"preview": "Build Time Analyzer for Xcode\n======================\n\n[. The extraction includes 33 files (168.1 KB), approximately 40.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.