├── .gitignore ├── .travis.yml ├── BuildTimeAnalyzer.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── BuildTimeAnalyzer.xcscheme ├── BuildTimeAnalyzer ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── logo128.png │ │ ├── logo16.png │ │ ├── logo256-1.png │ │ ├── logo256.png │ │ ├── logo32-1.png │ │ ├── logo32.png │ │ ├── logo512-1.png │ │ ├── logo512.png │ │ ├── logo512@2x.png │ │ └── logo64.png │ ├── Contents.json │ └── ScreenShot.imageset │ │ ├── Contents.json │ │ └── Screen Shot.png ├── 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 ├── BuildTimeAnalyzerTests-Bridging-Header.h ├── BuildTimeAnalyzerTests ├── CompileMeasureTests.swift ├── Info.plist └── ViewControllerDataSourceTest.swift ├── GZIP ├── NSData+GZIP.h └── NSData+GZIP.m ├── LICENSE ├── README.md └── Screenshots └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | .DS_Store 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode9.3 3 | script: xcodebuild -project BuildTimeAnalyzer.xcodeproj -scheme BuildTimeAnalyzer build test 4 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2839B8691FD2896F004C075C /* ViewControllerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2839B8681FD2896F004C075C /* ViewControllerDataSource.swift */; }; 11 | 2839B86B1FD32766004C075C /* ViewControllerDataSourceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2839B86A1FD32766004C075C /* ViewControllerDataSourceTest.swift */; }; 12 | 2A3164C81D21D73F00064045 /* CompileMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C01D21D73F00064045 /* CompileMeasure.swift */; }; 13 | 2A3164C91D21D73F00064045 /* LogProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C11D21D73F00064045 /* LogProcessor.swift */; }; 14 | 2A3164CB1D21D73F00064045 /* ProcessingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C31D21D73F00064045 /* ProcessingState.swift */; }; 15 | 2A3164CC1D21D73F00064045 /* RawMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C41D21D73F00064045 /* RawMeasure.swift */; }; 16 | 2A3164D01D21D74A00064045 /* CompileMeasureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164CF1D21D74A00064045 /* CompileMeasureTests.swift */; }; 17 | 2A3164D51D21D77500064045 /* NSData+GZIP.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164D41D21D77500064045 /* NSData+GZIP.m */; }; 18 | 2A3164DA1D21D90100064045 /* NSData+GZIP.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164D41D21D77500064045 /* NSData+GZIP.m */; }; 19 | 2A3698AA1D80A1AC002C5CDA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A3698A91D80A1AC002C5CDA /* Main.storyboard */; }; 20 | 2A3698AC1D80A33B002C5CDA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3698AB1D80A33B002C5CDA /* ViewController.swift */; }; 21 | 2A5404011D86D01700DBD44C /* BuildManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5404001D86D01700DBD44C /* BuildManager.swift */; }; 22 | 2A5404031D86DE0C00DBD44C /* XcodeDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5404021D86DE0C00DBD44C /* XcodeDatabase.swift */; }; 23 | 2A5404051D86F3C700DBD44C /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5404041D86F3C700DBD44C /* File.swift */; }; 24 | 2A9807DD1D7C71F900B9232C /* DirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9807DC1D7C71F900B9232C /* DirectoryMonitor.swift */; }; 25 | 2A9807DF1D7C76FD00B9232C /* ProjectSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9807DE1D7C76FD00B9232C /* ProjectSelection.swift */; }; 26 | 2ABFB6CE1D81F2DE00D060BF /* NSAlert+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ABFB6CD1D81F2DE00D060BF /* NSAlert+Extensions.swift */; }; 27 | 2ACBFD0C1D8835E60009567E /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ACBFD0B1D8835E60009567E /* UserSettings.swift */; }; 28 | 2AE775121D225D5D00D1A744 /* DerivedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE775111D225D5D00D1A744 /* DerivedDataManager.swift */; }; 29 | 2AF821441D21D6B900D65186 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF821431D21D6B900D65186 /* AppDelegate.swift */; }; 30 | 2AF821461D21D6B900D65186 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2AF821451D21D6B900D65186 /* Assets.xcassets */; }; 31 | 5603EB6221EF93E90013D77B /* CSVExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603EB6121EF93E90013D77B /* CSVExporter.swift */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXContainerItemProxy section */ 35 | 2AF821501D21D6B900D65186 /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 2AF821381D21D6B900D65186 /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 2AF8213F1D21D6B900D65186; 40 | remoteInfo = BuildTimeAnalyzer; 41 | }; 42 | /* End PBXContainerItemProxy section */ 43 | 44 | /* Begin PBXFileReference section */ 45 | 2839B8681FD2896F004C075C /* ViewControllerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerDataSource.swift; sourceTree = ""; }; 46 | 2839B86A1FD32766004C075C /* ViewControllerDataSourceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerDataSourceTest.swift; sourceTree = ""; }; 47 | 2A3164C01D21D73F00064045 /* CompileMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompileMeasure.swift; sourceTree = ""; }; 48 | 2A3164C11D21D73F00064045 /* LogProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogProcessor.swift; sourceTree = ""; }; 49 | 2A3164C31D21D73F00064045 /* ProcessingState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessingState.swift; sourceTree = ""; }; 50 | 2A3164C41D21D73F00064045 /* RawMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawMeasure.swift; sourceTree = ""; }; 51 | 2A3164CF1D21D74A00064045 /* CompileMeasureTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompileMeasureTests.swift; sourceTree = ""; }; 52 | 2A3164D21D21D77500064045 /* BuildTimeAnalyzerTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BuildTimeAnalyzerTests-Bridging-Header.h"; sourceTree = SOURCE_ROOT; }; 53 | 2A3164D31D21D77500064045 /* NSData+GZIP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSData+GZIP.h"; path = "GZIP/NSData+GZIP.h"; sourceTree = ""; }; 54 | 2A3164D41D21D77500064045 /* NSData+GZIP.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSData+GZIP.m"; path = "GZIP/NSData+GZIP.m"; sourceTree = ""; }; 55 | 2A3698A91D80A1AC002C5CDA /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 56 | 2A3698AB1D80A33B002C5CDA /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 57 | 2A5404001D86D01700DBD44C /* BuildManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildManager.swift; sourceTree = ""; }; 58 | 2A5404021D86DE0C00DBD44C /* XcodeDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XcodeDatabase.swift; sourceTree = ""; }; 59 | 2A5404041D86F3C700DBD44C /* File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; 60 | 2A9807DC1D7C71F900B9232C /* DirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryMonitor.swift; sourceTree = ""; }; 61 | 2A9807DE1D7C76FD00B9232C /* ProjectSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectSelection.swift; sourceTree = ""; }; 62 | 2ABFB6CD1D81F2DE00D060BF /* NSAlert+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAlert+Extensions.swift"; sourceTree = ""; }; 63 | 2ACBFD0B1D8835E60009567E /* UserSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; 64 | 2AE775111D225D5D00D1A744 /* DerivedDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DerivedDataManager.swift; sourceTree = ""; }; 65 | 2AF821401D21D6B900D65186 /* BuildTimeAnalyzer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BuildTimeAnalyzer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | 2AF821431D21D6B900D65186 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 67 | 2AF821451D21D6B900D65186 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = BuildTimeAnalyzer/Assets.xcassets; sourceTree = ""; }; 68 | 2AF8214A1D21D6B900D65186 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 69 | 2AF8214F1D21D6B900D65186 /* BuildTimeAnalyzerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BuildTimeAnalyzerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | 2AF821551D21D6B900D65186 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 71 | 5603EB6121EF93E90013D77B /* CSVExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVExporter.swift; sourceTree = ""; }; 72 | /* End PBXFileReference section */ 73 | 74 | /* Begin PBXFrameworksBuildPhase section */ 75 | 2AF8213D1D21D6B900D65186 /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | 2AF8214C1D21D6B900D65186 /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | /* End PBXFrameworksBuildPhase section */ 90 | 91 | /* Begin PBXGroup section */ 92 | 2A3164D11D21D74F00064045 /* Supporting Files */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 2A3164D21D21D77500064045 /* BuildTimeAnalyzerTests-Bridging-Header.h */, 96 | 2AF8214A1D21D6B900D65186 /* Info.plist */, 97 | ); 98 | name = "Supporting Files"; 99 | sourceTree = ""; 100 | }; 101 | 2A3164D61D21D77E00064045 /* GZIP */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 2A3164D31D21D77500064045 /* NSData+GZIP.h */, 105 | 2A3164D41D21D77500064045 /* NSData+GZIP.m */, 106 | ); 107 | name = GZIP; 108 | sourceTree = ""; 109 | }; 110 | 2A3164D71D21D7A800064045 /* Supporting Files */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 2AF821551D21D6B900D65186 /* Info.plist */, 114 | ); 115 | name = "Supporting Files"; 116 | sourceTree = ""; 117 | }; 118 | 2ABFB6CF1D81F34300D060BF /* Extensions */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 2ABFB6CD1D81F2DE00D060BF /* NSAlert+Extensions.swift */, 122 | ); 123 | name = Extensions; 124 | sourceTree = ""; 125 | }; 126 | 2ABFB6D01D81F35400D060BF /* Application */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 2AF821431D21D6B900D65186 /* AppDelegate.swift */, 130 | 2A3698A91D80A1AC002C5CDA /* Main.storyboard */, 131 | ); 132 | name = Application; 133 | sourceTree = ""; 134 | }; 135 | 2ABFB6D11D81F37300D060BF /* Models */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 2A3164C01D21D73F00064045 /* CompileMeasure.swift */, 139 | 2A5404041D86F3C700DBD44C /* File.swift */, 140 | 2A3164C31D21D73F00064045 /* ProcessingState.swift */, 141 | 2A3164C41D21D73F00064045 /* RawMeasure.swift */, 142 | 2839B8681FD2896F004C075C /* ViewControllerDataSource.swift */, 143 | 2A5404021D86DE0C00DBD44C /* XcodeDatabase.swift */, 144 | ); 145 | name = Models; 146 | sourceTree = ""; 147 | }; 148 | 2ABFB6D21D81F81400D060BF /* ViewControllers */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 2A3698AB1D80A33B002C5CDA /* ViewController.swift */, 152 | ); 153 | name = ViewControllers; 154 | sourceTree = ""; 155 | }; 156 | 2ABFB6D31D81F82600D060BF /* Utilities */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 2A5404001D86D01700DBD44C /* BuildManager.swift */, 160 | 2AE775111D225D5D00D1A744 /* DerivedDataManager.swift */, 161 | 2A9807DC1D7C71F900B9232C /* DirectoryMonitor.swift */, 162 | 2A3164C11D21D73F00064045 /* LogProcessor.swift */, 163 | 2A9807DE1D7C76FD00B9232C /* ProjectSelection.swift */, 164 | 2ACBFD0B1D8835E60009567E /* UserSettings.swift */, 165 | 5603EB6121EF93E90013D77B /* CSVExporter.swift */, 166 | ); 167 | name = Utilities; 168 | sourceTree = ""; 169 | }; 170 | 2AF821371D21D6B900D65186 = { 171 | isa = PBXGroup; 172 | children = ( 173 | 2AF821421D21D6B900D65186 /* BuildTimeAnalyzer */, 174 | 2AF821451D21D6B900D65186 /* Assets.xcassets */, 175 | 2AF821521D21D6B900D65186 /* BuildTimeAnalyzerTests */, 176 | 2A3164D61D21D77E00064045 /* GZIP */, 177 | 2AF821411D21D6B900D65186 /* Products */, 178 | ); 179 | sourceTree = ""; 180 | }; 181 | 2AF821411D21D6B900D65186 /* Products */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | 2AF821401D21D6B900D65186 /* BuildTimeAnalyzer.app */, 185 | 2AF8214F1D21D6B900D65186 /* BuildTimeAnalyzerTests.xctest */, 186 | ); 187 | name = Products; 188 | sourceTree = ""; 189 | }; 190 | 2AF821421D21D6B900D65186 /* BuildTimeAnalyzer */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | 2ABFB6D01D81F35400D060BF /* Application */, 194 | 2ABFB6CF1D81F34300D060BF /* Extensions */, 195 | 2ABFB6D11D81F37300D060BF /* Models */, 196 | 2A3164D11D21D74F00064045 /* Supporting Files */, 197 | 2ABFB6D31D81F82600D060BF /* Utilities */, 198 | 2ABFB6D21D81F81400D060BF /* ViewControllers */, 199 | ); 200 | path = BuildTimeAnalyzer; 201 | sourceTree = ""; 202 | }; 203 | 2AF821521D21D6B900D65186 /* BuildTimeAnalyzerTests */ = { 204 | isa = PBXGroup; 205 | children = ( 206 | 2A3164CF1D21D74A00064045 /* CompileMeasureTests.swift */, 207 | 2839B86A1FD32766004C075C /* ViewControllerDataSourceTest.swift */, 208 | 2A3164D71D21D7A800064045 /* Supporting Files */, 209 | ); 210 | path = BuildTimeAnalyzerTests; 211 | sourceTree = ""; 212 | }; 213 | /* End PBXGroup section */ 214 | 215 | /* Begin PBXNativeTarget section */ 216 | 2AF8213F1D21D6B900D65186 /* BuildTimeAnalyzer */ = { 217 | isa = PBXNativeTarget; 218 | buildConfigurationList = 2AF821581D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzer" */; 219 | buildPhases = ( 220 | 2AF8213C1D21D6B900D65186 /* Sources */, 221 | 2AF8213D1D21D6B900D65186 /* Frameworks */, 222 | 2AF8213E1D21D6B900D65186 /* Resources */, 223 | ); 224 | buildRules = ( 225 | ); 226 | dependencies = ( 227 | ); 228 | name = BuildTimeAnalyzer; 229 | productName = BuildTimeAnalyzer; 230 | productReference = 2AF821401D21D6B900D65186 /* BuildTimeAnalyzer.app */; 231 | productType = "com.apple.product-type.application"; 232 | }; 233 | 2AF8214E1D21D6B900D65186 /* BuildTimeAnalyzerTests */ = { 234 | isa = PBXNativeTarget; 235 | buildConfigurationList = 2AF8215B1D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzerTests" */; 236 | buildPhases = ( 237 | 2AF8214B1D21D6B900D65186 /* Sources */, 238 | 2AF8214C1D21D6B900D65186 /* Frameworks */, 239 | 2AF8214D1D21D6B900D65186 /* Resources */, 240 | ); 241 | buildRules = ( 242 | ); 243 | dependencies = ( 244 | 2AF821511D21D6B900D65186 /* PBXTargetDependency */, 245 | ); 246 | name = BuildTimeAnalyzerTests; 247 | productName = BuildTimeAnalyzerTests; 248 | productReference = 2AF8214F1D21D6B900D65186 /* BuildTimeAnalyzerTests.xctest */; 249 | productType = "com.apple.product-type.bundle.unit-test"; 250 | }; 251 | /* End PBXNativeTarget section */ 252 | 253 | /* Begin PBXProject section */ 254 | 2AF821381D21D6B900D65186 /* Project object */ = { 255 | isa = PBXProject; 256 | attributes = { 257 | BuildIndependentTargetsInParallel = YES; 258 | LastSwiftUpdateCheck = 0730; 259 | LastUpgradeCheck = 1500; 260 | ORGANIZATIONNAME = "Cane Media Ltd"; 261 | TargetAttributes = { 262 | 2AF8213F1D21D6B900D65186 = { 263 | CreatedOnToolsVersion = 7.3.1; 264 | LastSwiftMigration = 1250; 265 | }; 266 | 2AF8214E1D21D6B900D65186 = { 267 | CreatedOnToolsVersion = 7.3.1; 268 | LastSwiftMigration = 1250; 269 | TestTargetID = 2AF8213F1D21D6B900D65186; 270 | }; 271 | }; 272 | }; 273 | buildConfigurationList = 2AF8213B1D21D6B900D65186 /* Build configuration list for PBXProject "BuildTimeAnalyzer" */; 274 | compatibilityVersion = "Xcode 3.2"; 275 | developmentRegion = en; 276 | hasScannedForEncodings = 0; 277 | knownRegions = ( 278 | en, 279 | Base, 280 | ); 281 | mainGroup = 2AF821371D21D6B900D65186; 282 | productRefGroup = 2AF821411D21D6B900D65186 /* Products */; 283 | projectDirPath = ""; 284 | projectRoot = ""; 285 | targets = ( 286 | 2AF8213F1D21D6B900D65186 /* BuildTimeAnalyzer */, 287 | 2AF8214E1D21D6B900D65186 /* BuildTimeAnalyzerTests */, 288 | ); 289 | }; 290 | /* End PBXProject section */ 291 | 292 | /* Begin PBXResourcesBuildPhase section */ 293 | 2AF8213E1D21D6B900D65186 /* Resources */ = { 294 | isa = PBXResourcesBuildPhase; 295 | buildActionMask = 2147483647; 296 | files = ( 297 | 2A3698AA1D80A1AC002C5CDA /* Main.storyboard in Resources */, 298 | 2AF821461D21D6B900D65186 /* Assets.xcassets in Resources */, 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | 2AF8214D1D21D6B900D65186 /* Resources */ = { 303 | isa = PBXResourcesBuildPhase; 304 | buildActionMask = 2147483647; 305 | files = ( 306 | ); 307 | runOnlyForDeploymentPostprocessing = 0; 308 | }; 309 | /* End PBXResourcesBuildPhase section */ 310 | 311 | /* Begin PBXSourcesBuildPhase section */ 312 | 2AF8213C1D21D6B900D65186 /* Sources */ = { 313 | isa = PBXSourcesBuildPhase; 314 | buildActionMask = 2147483647; 315 | files = ( 316 | 2A9807DD1D7C71F900B9232C /* DirectoryMonitor.swift in Sources */, 317 | 2A3164DA1D21D90100064045 /* NSData+GZIP.m in Sources */, 318 | 2A3164C91D21D73F00064045 /* LogProcessor.swift in Sources */, 319 | 2839B8691FD2896F004C075C /* ViewControllerDataSource.swift in Sources */, 320 | 2A5404011D86D01700DBD44C /* BuildManager.swift in Sources */, 321 | 5603EB6221EF93E90013D77B /* CSVExporter.swift in Sources */, 322 | 2A5404051D86F3C700DBD44C /* File.swift in Sources */, 323 | 2ABFB6CE1D81F2DE00D060BF /* NSAlert+Extensions.swift in Sources */, 324 | 2A5404031D86DE0C00DBD44C /* XcodeDatabase.swift in Sources */, 325 | 2A3164CB1D21D73F00064045 /* ProcessingState.swift in Sources */, 326 | 2AF821441D21D6B900D65186 /* AppDelegate.swift in Sources */, 327 | 2AE775121D225D5D00D1A744 /* DerivedDataManager.swift in Sources */, 328 | 2A3698AC1D80A33B002C5CDA /* ViewController.swift in Sources */, 329 | 2A3164CC1D21D73F00064045 /* RawMeasure.swift in Sources */, 330 | 2ACBFD0C1D8835E60009567E /* UserSettings.swift in Sources */, 331 | 2A3164C81D21D73F00064045 /* CompileMeasure.swift in Sources */, 332 | 2A9807DF1D7C76FD00B9232C /* ProjectSelection.swift in Sources */, 333 | ); 334 | runOnlyForDeploymentPostprocessing = 0; 335 | }; 336 | 2AF8214B1D21D6B900D65186 /* Sources */ = { 337 | isa = PBXSourcesBuildPhase; 338 | buildActionMask = 2147483647; 339 | files = ( 340 | 2A3164D51D21D77500064045 /* NSData+GZIP.m in Sources */, 341 | 2A3164D01D21D74A00064045 /* CompileMeasureTests.swift in Sources */, 342 | 2839B86B1FD32766004C075C /* ViewControllerDataSourceTest.swift in Sources */, 343 | ); 344 | runOnlyForDeploymentPostprocessing = 0; 345 | }; 346 | /* End PBXSourcesBuildPhase section */ 347 | 348 | /* Begin PBXTargetDependency section */ 349 | 2AF821511D21D6B900D65186 /* PBXTargetDependency */ = { 350 | isa = PBXTargetDependency; 351 | target = 2AF8213F1D21D6B900D65186 /* BuildTimeAnalyzer */; 352 | targetProxy = 2AF821501D21D6B900D65186 /* PBXContainerItemProxy */; 353 | }; 354 | /* End PBXTargetDependency section */ 355 | 356 | /* Begin XCBuildConfiguration section */ 357 | 2AF821561D21D6B900D65186 /* Debug */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | ALWAYS_SEARCH_USER_PATHS = NO; 361 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 362 | CLANG_ANALYZER_NONNULL = YES; 363 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 364 | CLANG_CXX_LIBRARY = "libc++"; 365 | CLANG_ENABLE_MODULES = YES; 366 | CLANG_ENABLE_OBJC_ARC = YES; 367 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 368 | CLANG_WARN_BOOL_CONVERSION = YES; 369 | CLANG_WARN_COMMA = YES; 370 | CLANG_WARN_CONSTANT_CONVERSION = YES; 371 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 372 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 373 | CLANG_WARN_EMPTY_BODY = YES; 374 | CLANG_WARN_ENUM_CONVERSION = YES; 375 | CLANG_WARN_INFINITE_RECURSION = YES; 376 | CLANG_WARN_INT_CONVERSION = YES; 377 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 378 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 379 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 380 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 381 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 382 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 383 | CLANG_WARN_STRICT_PROTOTYPES = YES; 384 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 385 | CLANG_WARN_UNREACHABLE_CODE = YES; 386 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 387 | CODE_SIGN_IDENTITY = "-"; 388 | COPY_PHASE_STRIP = NO; 389 | DEAD_CODE_STRIPPING = YES; 390 | DEBUG_INFORMATION_FORMAT = dwarf; 391 | ENABLE_STRICT_OBJC_MSGSEND = YES; 392 | ENABLE_TESTABILITY = YES; 393 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 394 | GCC_C_LANGUAGE_STANDARD = gnu99; 395 | GCC_DYNAMIC_NO_PIC = NO; 396 | GCC_NO_COMMON_BLOCKS = YES; 397 | GCC_OPTIMIZATION_LEVEL = 0; 398 | GCC_PREPROCESSOR_DEFINITIONS = ( 399 | "DEBUG=1", 400 | "$(inherited)", 401 | ); 402 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 403 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 404 | GCC_WARN_UNDECLARED_SELECTOR = YES; 405 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 406 | GCC_WARN_UNUSED_FUNCTION = YES; 407 | GCC_WARN_UNUSED_VARIABLE = YES; 408 | MACOSX_DEPLOYMENT_TARGET = 11.0; 409 | MTL_ENABLE_DEBUG_INFO = YES; 410 | ONLY_ACTIVE_ARCH = YES; 411 | SDKROOT = macosx; 412 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 413 | SWIFT_VERSION = 5.0; 414 | }; 415 | name = Debug; 416 | }; 417 | 2AF821571D21D6B900D65186 /* Release */ = { 418 | isa = XCBuildConfiguration; 419 | buildSettings = { 420 | ALWAYS_SEARCH_USER_PATHS = NO; 421 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 422 | CLANG_ANALYZER_NONNULL = YES; 423 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 424 | CLANG_CXX_LIBRARY = "libc++"; 425 | CLANG_ENABLE_MODULES = YES; 426 | CLANG_ENABLE_OBJC_ARC = YES; 427 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 428 | CLANG_WARN_BOOL_CONVERSION = YES; 429 | CLANG_WARN_COMMA = YES; 430 | CLANG_WARN_CONSTANT_CONVERSION = YES; 431 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 432 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 433 | CLANG_WARN_EMPTY_BODY = YES; 434 | CLANG_WARN_ENUM_CONVERSION = YES; 435 | CLANG_WARN_INFINITE_RECURSION = YES; 436 | CLANG_WARN_INT_CONVERSION = YES; 437 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 438 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 439 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 440 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 441 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 442 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 443 | CLANG_WARN_STRICT_PROTOTYPES = YES; 444 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 445 | CLANG_WARN_UNREACHABLE_CODE = YES; 446 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 447 | CODE_SIGN_IDENTITY = "-"; 448 | COPY_PHASE_STRIP = NO; 449 | DEAD_CODE_STRIPPING = YES; 450 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 451 | ENABLE_NS_ASSERTIONS = NO; 452 | ENABLE_STRICT_OBJC_MSGSEND = YES; 453 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 454 | GCC_C_LANGUAGE_STANDARD = gnu99; 455 | GCC_NO_COMMON_BLOCKS = YES; 456 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 457 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 458 | GCC_WARN_UNDECLARED_SELECTOR = YES; 459 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 460 | GCC_WARN_UNUSED_FUNCTION = YES; 461 | GCC_WARN_UNUSED_VARIABLE = YES; 462 | MACOSX_DEPLOYMENT_TARGET = 11.0; 463 | MTL_ENABLE_DEBUG_INFO = NO; 464 | SDKROOT = macosx; 465 | SWIFT_COMPILATION_MODE = wholemodule; 466 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 467 | SWIFT_VERSION = 5.0; 468 | }; 469 | name = Release; 470 | }; 471 | 2AF821591D21D6B900D65186 /* Debug */ = { 472 | isa = XCBuildConfiguration; 473 | buildSettings = { 474 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 475 | CODE_SIGN_IDENTITY = "-"; 476 | COMBINE_HIDPI_IMAGES = YES; 477 | DEAD_CODE_STRIPPING = YES; 478 | INFOPLIST_FILE = BuildTimeAnalyzer/Info.plist; 479 | LD_RUNPATH_SEARCH_PATHS = ( 480 | "$(inherited)", 481 | "@executable_path/../Frameworks", 482 | ); 483 | MACOSX_DEPLOYMENT_TARGET = 11.0; 484 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzer; 485 | PRODUCT_NAME = "$(TARGET_NAME)"; 486 | SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; 487 | }; 488 | name = Debug; 489 | }; 490 | 2AF8215A1D21D6B900D65186 /* Release */ = { 491 | isa = XCBuildConfiguration; 492 | buildSettings = { 493 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 494 | CODE_SIGN_IDENTITY = "-"; 495 | COMBINE_HIDPI_IMAGES = YES; 496 | DEAD_CODE_STRIPPING = YES; 497 | INFOPLIST_FILE = BuildTimeAnalyzer/Info.plist; 498 | LD_RUNPATH_SEARCH_PATHS = ( 499 | "$(inherited)", 500 | "@executable_path/../Frameworks", 501 | ); 502 | MACOSX_DEPLOYMENT_TARGET = 11.0; 503 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzer; 504 | PRODUCT_NAME = "$(TARGET_NAME)"; 505 | SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; 506 | }; 507 | name = Release; 508 | }; 509 | 2AF8215C1D21D6B900D65186 /* Debug */ = { 510 | isa = XCBuildConfiguration; 511 | buildSettings = { 512 | BUNDLE_LOADER = "$(TEST_HOST)"; 513 | CLANG_ENABLE_MODULES = YES; 514 | COMBINE_HIDPI_IMAGES = YES; 515 | DEAD_CODE_STRIPPING = YES; 516 | INFOPLIST_FILE = BuildTimeAnalyzerTests/Info.plist; 517 | LD_RUNPATH_SEARCH_PATHS = ( 518 | "$(inherited)", 519 | "@executable_path/../Frameworks", 520 | "@loader_path/../Frameworks", 521 | ); 522 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzerTests; 523 | PRODUCT_NAME = "$(TARGET_NAME)"; 524 | SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; 525 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 526 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BuildTimeAnalyzer.app/Contents/MacOS/BuildTimeAnalyzer"; 527 | }; 528 | name = Debug; 529 | }; 530 | 2AF8215D1D21D6B900D65186 /* Release */ = { 531 | isa = XCBuildConfiguration; 532 | buildSettings = { 533 | BUNDLE_LOADER = "$(TEST_HOST)"; 534 | CLANG_ENABLE_MODULES = YES; 535 | COMBINE_HIDPI_IMAGES = YES; 536 | DEAD_CODE_STRIPPING = YES; 537 | INFOPLIST_FILE = BuildTimeAnalyzerTests/Info.plist; 538 | LD_RUNPATH_SEARCH_PATHS = ( 539 | "$(inherited)", 540 | "@executable_path/../Frameworks", 541 | "@loader_path/../Frameworks", 542 | ); 543 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzerTests; 544 | PRODUCT_NAME = "$(TARGET_NAME)"; 545 | SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; 546 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BuildTimeAnalyzer.app/Contents/MacOS/BuildTimeAnalyzer"; 547 | }; 548 | name = Release; 549 | }; 550 | /* End XCBuildConfiguration section */ 551 | 552 | /* Begin XCConfigurationList section */ 553 | 2AF8213B1D21D6B900D65186 /* Build configuration list for PBXProject "BuildTimeAnalyzer" */ = { 554 | isa = XCConfigurationList; 555 | buildConfigurations = ( 556 | 2AF821561D21D6B900D65186 /* Debug */, 557 | 2AF821571D21D6B900D65186 /* Release */, 558 | ); 559 | defaultConfigurationIsVisible = 0; 560 | defaultConfigurationName = Release; 561 | }; 562 | 2AF821581D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzer" */ = { 563 | isa = XCConfigurationList; 564 | buildConfigurations = ( 565 | 2AF821591D21D6B900D65186 /* Debug */, 566 | 2AF8215A1D21D6B900D65186 /* Release */, 567 | ); 568 | defaultConfigurationIsVisible = 0; 569 | defaultConfigurationName = Release; 570 | }; 571 | 2AF8215B1D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzerTests" */ = { 572 | isa = XCConfigurationList; 573 | buildConfigurations = ( 574 | 2AF8215C1D21D6B900D65186 /* Debug */, 575 | 2AF8215D1D21D6B900D65186 /* Release */, 576 | ); 577 | defaultConfigurationIsVisible = 0; 578 | defaultConfigurationName = Release; 579 | }; 580 | /* End XCConfigurationList section */ 581 | }; 582 | rootObject = 2AF821381D21D6B900D65186 /* Project object */; 583 | } 584 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer.xcodeproj/xcshareddata/xcschemes/BuildTimeAnalyzer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Cocoa 7 | 8 | @NSApplicationMain 9 | class AppDelegate: NSObject, NSApplicationDelegate { 10 | 11 | @IBOutlet weak var projectSelectionMenuItem: NSMenuItem! 12 | @IBOutlet weak var buildTimesMenuItem: NSMenuItem! 13 | @IBOutlet weak var alwaysInFrontMenuItem: NSMenuItem! 14 | 15 | @objc var canExport: Bool = false 16 | 17 | var viewController: ViewController? { 18 | return NSApplication.shared.mainWindow?.contentViewController as? ViewController 19 | } 20 | 21 | func applicationDidFinishLaunching(_ notification: Notification) { 22 | alwaysInFrontMenuItem.state = UserSettings.windowShouldBeTopMost ? .on : .off 23 | } 24 | 25 | func configureMenuItems(showBuildTimesMenuItem: Bool) { 26 | projectSelectionMenuItem.isEnabled = !showBuildTimesMenuItem 27 | buildTimesMenuItem.isEnabled = showBuildTimesMenuItem 28 | } 29 | 30 | // MARK: Actions 31 | 32 | @IBAction func navigateToProjectSelection(_ sender: NSMenuItem) { 33 | configureMenuItems(showBuildTimesMenuItem: true) 34 | 35 | viewController?.cancelProcessing() 36 | viewController?.showInstructions(true) 37 | } 38 | 39 | @IBAction func navigateToBuildTimes(_ sender: NSMenuItem) { 40 | configureMenuItems(showBuildTimesMenuItem: false) 41 | viewController?.showInstructions(false) 42 | } 43 | 44 | @IBAction func visitGitHubPage(_ sender: AnyObject) { 45 | let path = "https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode" 46 | if let url = URL(string: path) { 47 | NSWorkspace.shared.open(url) 48 | } 49 | } 50 | 51 | @IBAction func toggleAlwaysInFront(_ sender: NSMenuItem) { 52 | let alwaysInFront = sender.state == .off 53 | 54 | sender.state = alwaysInFront ? .on : .off 55 | UserSettings.windowShouldBeTopMost = alwaysInFront 56 | 57 | viewController?.makeWindowTopMost(topMost: alwaysInFront) 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "logo16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "logo32-1.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "logo32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "logo64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "logo128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "logo256-1.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "logo256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "logo512-1.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "logo512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "logo512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo128.png -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo16.png -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo256-1.png -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo256.png -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo32-1.png -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo32.png -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512-1.png -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512.png -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512@2x.png -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo64.png -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/ScreenShot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Screen Shot.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Assets.xcassets/ScreenShot.imageset/Screen Shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/BuildTimeAnalyzer/Assets.xcassets/ScreenShot.imageset/Screen Shot.png -------------------------------------------------------------------------------- /BuildTimeAnalyzer/BuildManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildManager.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Cocoa 7 | 8 | protocol BuildManagerDelegate: AnyObject { 9 | func derivedDataDidChange() 10 | func buildManager(_ buildManager: BuildManager, shouldParseLogWithDatabase database: XcodeDatabase) 11 | } 12 | 13 | class BuildManager: NSObject { 14 | 15 | weak var delegate: BuildManagerDelegate? 16 | 17 | private let derivedDataDirectoryMonitor = DirectoryMonitor(isDerivedData: true) 18 | private let logFolderDirectoryMonitor = DirectoryMonitor(isDerivedData: false) 19 | 20 | private var currentDataBase: XcodeDatabase? 21 | 22 | override func awakeFromNib() { 23 | super.awakeFromNib() 24 | 25 | derivedDataDirectoryMonitor.delegate = self 26 | logFolderDirectoryMonitor.delegate = self 27 | 28 | startMonitoring() 29 | } 30 | 31 | func startMonitoring() { 32 | stopMonitoring() 33 | derivedDataDirectoryMonitor.startMonitoring(path: UserSettings.derivedDataLocation) 34 | } 35 | 36 | func stopMonitoring() { 37 | derivedDataDirectoryMonitor.stopMonitoring() 38 | } 39 | 40 | func database(forFolder URL: URL) -> XcodeDatabase? { 41 | let databaseURL = URL.appendingPathComponent("Cache.db") 42 | return XcodeDatabase(fromPath: databaseURL.path) 43 | } 44 | 45 | func processDerivedData() { 46 | guard let mostRecent = DerivedDataManager.derivedData().first else { return } 47 | 48 | let logFolder = mostRecent.url.appendingPathComponent("Logs/Build").path 49 | guard logFolderDirectoryMonitor.path != logFolder else { return } 50 | 51 | logFolderDirectoryMonitor.stopMonitoring() 52 | logFolderDirectoryMonitor.startMonitoring(path: logFolder) 53 | } 54 | 55 | func processLogFolder(with url: URL) { 56 | guard let activeDatabase = database(forFolder: url), 57 | activeDatabase.isBuildType, 58 | activeDatabase != currentDataBase else { return } 59 | 60 | currentDataBase = activeDatabase 61 | delegate?.buildManager(self, shouldParseLogWithDatabase: activeDatabase) 62 | } 63 | } 64 | 65 | extension BuildManager: DirectoryMonitorDelegate { 66 | func directoryMonitorDidObserveChange(_ directoryMonitor: DirectoryMonitor, isDerivedData: Bool) { 67 | if isDerivedData { 68 | delegate?.derivedDataDidChange() 69 | processDerivedData() 70 | } else if let path = directoryMonitor.path { 71 | // TODO: If we don't dispatch, it seems it fires off too soon 72 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 73 | self.processLogFolder(with: URL(fileURLWithPath: path)) 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/BuildTimeAnalyzer-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "NSData+GZIP.h" -------------------------------------------------------------------------------- /BuildTimeAnalyzer/CSVExporter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CSVExporter.swift 3 | // BuildTimeAnalyzer 4 | // 5 | // Created by Bruno Resende on 16.01.19. 6 | // Copyright © 2019 Cane Media Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct CSVExporter { 12 | 13 | static var filenameDateFormatter: DateFormatter = { 14 | let formatter = DateFormatter() 15 | formatter.dateFormat = "yyyyMMdd-HHmmss" 16 | return formatter 17 | }() 18 | 19 | func filename(with prefix: String) -> String { 20 | return "\(prefix)_\(CSVExporter.filenameDateFormatter.string(from: Date())).csv" 21 | } 22 | 23 | func export(elements: [T], to url: URL) throws where T: CSVExportable { 24 | 25 | guard let data = elements.joinedAsCSVString(delimiter: .doubleQuote).data(using: .utf8) else { 26 | throw ExportErrors.couldNotParseStringAsUTF8 27 | } 28 | 29 | do { 30 | try data.write(to: url, options: .atomic) 31 | } catch { 32 | throw ExportErrors.fileIO(error) 33 | } 34 | } 35 | 36 | enum ExportErrors: Error { 37 | case couldNotParseStringAsUTF8 38 | case fileIO(Error) 39 | } 40 | } 41 | 42 | enum CSVDelimiter: String { 43 | case singleQuote = "'" 44 | case doubleQuote = "\"" 45 | case none = "" 46 | } 47 | 48 | protocol CSVExportable { 49 | 50 | static var csvHeaderLine: String { get } 51 | 52 | var csvLine: String { get } 53 | } 54 | 55 | extension Array where Element: CSVExportable { 56 | 57 | func joinedAsCSVString(delimiter: CSVDelimiter) -> String { 58 | 59 | return ([Element.csvHeaderLine] + self.map({ $0.csvLine })).joined(separator: "\n") 60 | } 61 | } 62 | 63 | extension Array where Element == String { 64 | 65 | func joinedAsCSVLine(delimiter: CSVDelimiter) -> String { 66 | 67 | let formatter: (String) -> String 68 | 69 | switch delimiter { 70 | case .singleQuote: formatter = { $0.replacingOccurrences(of: "'", with: "\\'") } 71 | case .doubleQuote: formatter = { $0.replacingOccurrences(of: "\"", with: "\\\"") } 72 | case .none: formatter = { $0 } 73 | } 74 | 75 | return self.map({ "\(delimiter.rawValue)\(formatter($0))\(delimiter.rawValue)" }).joined(separator: ",") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/CompileMeasure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompileMeasure.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Foundation 7 | 8 | @objcMembers class CompileMeasure: NSObject { 9 | 10 | dynamic var time: Double 11 | var path: String 12 | var code: String 13 | dynamic var filename: String 14 | var references: Int 15 | 16 | private var locationArray: [Int] 17 | 18 | public enum Order: String { 19 | case filename 20 | case time 21 | } 22 | 23 | var fileAndLine: String { 24 | return "\(filename):\(locationArray[0])" 25 | } 26 | 27 | var fileInfo: String { 28 | return "\(fileAndLine):\(locationArray[1])" 29 | } 30 | 31 | var fileRow: String { 32 | "\(locationArray[0])" 33 | } 34 | 35 | var fileColumn: String { 36 | "\(locationArray[1])" 37 | } 38 | 39 | var location: Int { 40 | return locationArray[0] 41 | } 42 | 43 | var timeString: String { 44 | return String(format: "%.f", time) 45 | } 46 | 47 | init?(time: Double, rawPath: String, code: String, references: Int) { 48 | let untrimmedFilename = rawPath.split(separator: "/").map(String.init).last 49 | 50 | guard let filepath = rawPath.split(separator: ":").map(String.init).first, 51 | let filename = untrimmedFilename?.split(separator: ":").map(String.init).first else { return nil } 52 | 53 | let locationString = String(rawPath[filepath.endIndex...].dropFirst()) 54 | let locations = locationString.split(separator: ":").compactMap{ Int(String.init($0)) } 55 | guard locations.count == 2 else { return nil } 56 | 57 | self.time = time 58 | self.code = code 59 | self.path = filepath 60 | self.filename = filename 61 | self.locationArray = locations 62 | self.references = references 63 | } 64 | 65 | init?(rawPath: String, time: Double) { 66 | let untrimmedFilename = rawPath.split(separator: "/").map(String.init).last 67 | 68 | guard let filepath = rawPath.split(separator: ":").map(String.init).first, 69 | let filename = untrimmedFilename?.split(separator: ":").map(String.init).first else { return nil } 70 | 71 | self.time = time 72 | self.code = "" 73 | self.path = filepath 74 | self.filename = filename 75 | self.locationArray = [1,1] 76 | self.references = 1 77 | } 78 | 79 | subscript(column: Int) -> String { 80 | switch column { 81 | case 0: 82 | return timeString 83 | case 1: 84 | return fileInfo 85 | case 2: 86 | return "\(references)" 87 | default: 88 | return code 89 | } 90 | } 91 | } 92 | 93 | extension CompileMeasure: CSVExportable { 94 | 95 | static var csvHeaderLine: String = ["time", "file", "row", "column", "references", "code"].joinedAsCSVLine(delimiter: .doubleQuote) 96 | 97 | var csvLine: String 98 | { 99 | return [timeString, filename, fileRow, fileColumn, "\(references)", code].joinedAsCSVLine(delimiter: .doubleQuote) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/DerivedDataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DerivedDataManager.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Foundation 7 | 8 | class DerivedDataManager { 9 | 10 | static func derivedData() -> [File] { 11 | let url = URL(fileURLWithPath: UserSettings.derivedDataLocation) 12 | 13 | let folders = DerivedDataManager.listFolders(at: url) 14 | let fileManager = FileManager.default 15 | 16 | return folders.compactMap{ (url) -> File? in 17 | if url.lastPathComponent != "ModuleCache", 18 | let properties = try? fileManager.attributesOfItem(atPath: url.path), 19 | let modificationDate = properties[FileAttributeKey.modificationDate] as? Date { 20 | return File(date: modificationDate, url: url) 21 | } 22 | return nil 23 | }.sorted{ $0.date > $1.date } 24 | } 25 | 26 | static func listFolders(at url: URL) -> [URL] { 27 | let fileManager = FileManager.default 28 | let keys = [URLResourceKey.nameKey, URLResourceKey.isDirectoryKey] 29 | let options: FileManager.DirectoryEnumerationOptions = [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants] 30 | 31 | guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, options: options, errorHandler: nil) else { return [] } 32 | 33 | return enumerator.map{ $0 as! URL } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/DirectoryMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DirectoryMonitor.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Foundation 7 | 8 | protocol DirectoryMonitorDelegate: AnyObject { 9 | func directoryMonitorDidObserveChange(_ directoryMonitor: DirectoryMonitor, isDerivedData: Bool) 10 | } 11 | 12 | class DirectoryMonitor { 13 | var dispatchQueue: DispatchQueue 14 | 15 | weak var delegate: DirectoryMonitorDelegate? 16 | 17 | var fileDescriptor: Int32 = -1 18 | var dispatchSource: DispatchSourceFileSystemObject? 19 | var isDerivedData: Bool 20 | var path: String? 21 | var timer: Timer? 22 | var lastDerivedDataDate = Date() 23 | var isMonitoringDates = false 24 | 25 | init(isDerivedData: Bool) { 26 | self.isDerivedData = isDerivedData 27 | 28 | let suffix = isDerivedData ? "deriveddata" : "logfolder" 29 | dispatchQueue = DispatchQueue(label: "uk.co.canemedia.directorymonitor.\(suffix)", attributes: .concurrent) 30 | } 31 | 32 | func startMonitoring(path: String) { 33 | self.path = path 34 | 35 | guard dispatchSource == nil && fileDescriptor == -1 else { return } 36 | 37 | fileDescriptor = open(path, O_EVTONLY) 38 | guard fileDescriptor != -1 else { return } 39 | 40 | dispatchSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .all, queue: dispatchQueue) 41 | dispatchSource?.setEventHandler { 42 | DispatchQueue.main.async { 43 | self.delegate?.directoryMonitorDidObserveChange(self, isDerivedData: self.isDerivedData) 44 | } 45 | } 46 | dispatchSource?.setCancelHandler { 47 | close(self.fileDescriptor) 48 | 49 | self.fileDescriptor = -1 50 | self.dispatchSource = nil 51 | self.path = nil 52 | } 53 | dispatchSource?.resume() 54 | 55 | if isDerivedData && !isMonitoringDates { 56 | isMonitoringDates = true 57 | monitorModificationDates() 58 | } 59 | } 60 | 61 | func stopMonitoring() { 62 | dispatchSource?.cancel() 63 | path = nil 64 | } 65 | 66 | func monitorModificationDates() { 67 | if let date = DerivedDataManager.derivedData().first?.date, date > lastDerivedDataDate { 68 | lastDerivedDataDate = date 69 | self.delegate?.directoryMonitorDidObserveChange(self, isDerivedData: self.isDerivedData) 70 | } 71 | 72 | if path != nil { 73 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 74 | self.monitorModificationDates() 75 | } 76 | } else { 77 | isMonitoringDates = false 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Foundation 7 | 8 | struct File { 9 | let date: Date 10 | let url: URL 11 | } 12 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0.11 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2016 Cane Media Ltd. All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/LogProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogProcessor.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Foundation 7 | 8 | typealias CMUpdateClosure = (_ result: [CompileMeasure], _ didComplete: Bool, _ didCancel: Bool) -> () 9 | 10 | protocol LogProcessorProtocol: AnyObject { 11 | var rawMeasures: [String: RawMeasure] { get set } 12 | var updateHandler: CMUpdateClosure? { get set } 13 | var shouldCancel: Bool { get set } 14 | 15 | func processingDidStart() 16 | func processingDidFinish() 17 | } 18 | 19 | extension LogProcessorProtocol { 20 | func processDatabase(database: XcodeDatabase, updateHandler: CMUpdateClosure?) { 21 | guard let text = database.processLog() else { 22 | updateHandler?([], true, false) 23 | return 24 | } 25 | 26 | self.updateHandler = updateHandler 27 | DispatchQueue.global().async { 28 | self.process(text: text) 29 | } 30 | } 31 | 32 | // MARK: Private methods 33 | 34 | private func process(text: String) { 35 | let characterSet = CharacterSet(charactersIn:"\r\"") 36 | var remainingRange = text.startIndex.. 10 } 72 | if filteredResults.count < 20 { 73 | filteredResults = rawMeasures.values.filter{ $0.time > 0.1 } 74 | } 75 | 76 | let sortedResults = filteredResults.sorted(by: { $0.time > $1.time }) 77 | updateHandler?(processResult(sortedResults), completed, didCancel) 78 | 79 | if completed { 80 | rawMeasures.removeAll() 81 | } 82 | } 83 | 84 | private func processResult(_ unprocessedResult: [RawMeasure]) -> [CompileMeasure] { 85 | let characterSet = CharacterSet(charactersIn:"\r\"") 86 | 87 | var result: [CompileMeasure] = [] 88 | for entry in unprocessedResult { 89 | let code = entry.text.split(separator: "\t").map(String.init) 90 | let method = code.count >= 2 ? trimPrefixes(code[1]) : "-" 91 | 92 | if let path = code.first?.trimmingCharacters(in: characterSet), let measure = CompileMeasure(time: entry.time, rawPath: path, code: method, references: entry.references) { 93 | result.append(measure) 94 | } 95 | } 96 | return result 97 | } 98 | 99 | private func trimPrefixes(_ code: String) -> String { 100 | var code = code 101 | ["@objc ", "final ", "@IBAction "].forEach { (prefix) in 102 | if code.hasPrefix(prefix) { 103 | code = String(code[code.index(code.startIndex, offsetBy: prefix.count)...]) 104 | } 105 | } 106 | return code 107 | } 108 | } 109 | 110 | class LogProcessor: NSObject, LogProcessorProtocol { 111 | 112 | var rawMeasures: [String: RawMeasure] = [:] 113 | var updateHandler: CMUpdateClosure? 114 | var shouldCancel = false 115 | var timer: Timer? 116 | 117 | func processingDidStart() { 118 | DispatchQueue.main.async { 119 | self.timer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(self.timerCallback(_:)), userInfo: nil, repeats: true) 120 | } 121 | } 122 | 123 | func processingDidFinish() { 124 | DispatchQueue.main.async { 125 | self.timer?.invalidate() 126 | self.timer = nil 127 | let didCancel = self.shouldCancel 128 | self.shouldCancel = false 129 | self.updateResults(didComplete: true, didCancel: didCancel) 130 | } 131 | } 132 | 133 | @objc func timerCallback(_ timer: Timer) { 134 | updateResults(didComplete: false, didCancel: false) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 271 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 654 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/NSAlert+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAlert+Extensions.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Cocoa 7 | 8 | extension NSAlert { 9 | static func show(withMessage message: String, andInformativeText informativeText: String = "") { 10 | let alert = NSAlert() 11 | alert.messageText = message 12 | alert.informativeText = informativeText 13 | alert.alertStyle = .warning 14 | alert.addButton(withTitle: "OK") 15 | alert.runModal() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/ProcessingState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProcessingState.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | enum ProcessingState { 7 | case processing 8 | case waiting 9 | case completed(didSucceed: Bool, stateName: String) 10 | 11 | static let cancelledString = "Cancelled" 12 | static let completedString = "Completed" 13 | static let failedString = "No valid logs found" 14 | static let processingString = "Processing log..." 15 | static let waitingForBuildString = "Waiting..." 16 | } 17 | 18 | extension ProcessingState : Equatable {} 19 | 20 | func ==(lhs: ProcessingState, rhs: ProcessingState) -> Bool { 21 | switch (lhs, rhs) { 22 | case (let .completed(didSucceed1, _), let .completed(didSucceed2, _)): 23 | return didSucceed1 == didSucceed2 24 | 25 | case (.processing, .processing), (.waiting, .waiting): 26 | return true 27 | 28 | default: 29 | return false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/ProjectSelection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectSelection.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Cocoa 7 | 8 | protocol ProjectSelectionDelegate: AnyObject { 9 | func didSelectProject(with database: XcodeDatabase) 10 | } 11 | 12 | class ProjectSelection: NSObject { 13 | 14 | @IBOutlet weak var tableView: NSTableView! 15 | weak var delegate: ProjectSelectionDelegate? 16 | 17 | private var dataSource: [XcodeDatabase] = [] 18 | 19 | static private let dateFormatter: DateFormatter = { 20 | let dateFormatter = DateFormatter() 21 | dateFormatter.timeStyle = .short 22 | dateFormatter.dateStyle = .medium 23 | return dateFormatter 24 | }() 25 | 26 | func listFolders() { 27 | dataSource = DerivedDataManager.derivedData().compactMap{ 28 | XcodeDatabase(fromPath: $0.url.appendingPathComponent("Logs/Build/LogStoreManifest.plist").path) 29 | }.sorted(by: { $0.modificationDate > $1.modificationDate }) 30 | 31 | tableView.reloadData() 32 | } 33 | 34 | // MARK: Actions 35 | 36 | @IBAction func didSelectCell(_ sender: NSTableView) { 37 | guard sender.selectedRow != -1 else { return } 38 | delegate?.didSelectProject(with: dataSource[sender.selectedRow]) 39 | } 40 | } 41 | 42 | // MARK: NSTableViewDataSource 43 | 44 | extension ProjectSelection: NSTableViewDataSource { 45 | func numberOfRows(in tableView: NSTableView) -> Int { 46 | return dataSource.count 47 | } 48 | } 49 | 50 | // MARK: NSTableViewDelegate 51 | 52 | extension ProjectSelection: NSTableViewDelegate { 53 | 54 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 55 | guard let tableColumn = tableColumn, let columnIndex = tableView.tableColumns.firstIndex(of: tableColumn) else { return nil } 56 | 57 | let cellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell\(columnIndex)"), owner: self) as? NSTableCellView 58 | 59 | let source = dataSource[row] 60 | var value = "" 61 | 62 | switch columnIndex { 63 | case 0: 64 | value = source.schemeName 65 | default: 66 | value = ProjectSelection.dateFormatter.string(from: source.modificationDate) 67 | } 68 | cellView?.textField?.stringValue = value 69 | 70 | return cellView 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/RawMeasure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawMeasure.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Foundation 7 | 8 | struct RawMeasure { 9 | var time: Double 10 | var text: String 11 | var references: Int 12 | 13 | init(time: Double, text: String) { 14 | self.time = time 15 | self.text = text 16 | self.references = 1 17 | } 18 | } 19 | 20 | // MARK: Equatable 21 | 22 | extension RawMeasure: Equatable {} 23 | 24 | func ==(lhs: RawMeasure, rhs: RawMeasure) -> Bool { 25 | return lhs.time == rhs.time && lhs.text == rhs.text 26 | } 27 | 28 | // MARK: Hashable 29 | 30 | extension RawMeasure: Hashable { 31 | 32 | func hash(into hasher: inout Hasher) { 33 | hasher.combine(time) 34 | hasher.combine(text) 35 | hasher.combine(references) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/UserSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserCache.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Foundation 7 | 8 | class UserSettings { 9 | 10 | static private let derivedDataLocationKey = "derivedDataLocationKey" 11 | static private let windowLevelIsNormalKey = "windowLevelIsNormalKey" 12 | 13 | static private var _derivedDataLocation: String? 14 | static private var _windowLevelIsNormal: Bool? 15 | 16 | static var derivedDataLocation: String { 17 | get { 18 | if _derivedDataLocation == nil { 19 | _derivedDataLocation = UserDefaults.standard.string(forKey: derivedDataLocationKey) 20 | } 21 | if _derivedDataLocation == nil, let libraryFolder = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first { 22 | _derivedDataLocation = "\(libraryFolder)/Developer/Xcode/DerivedData" 23 | } 24 | return _derivedDataLocation ?? "" 25 | } 26 | set { 27 | _derivedDataLocation = newValue 28 | UserDefaults.standard.set(newValue, forKey: derivedDataLocationKey) 29 | UserDefaults.standard.synchronize() 30 | } 31 | } 32 | 33 | static var windowShouldBeTopMost: Bool { 34 | get { 35 | if _windowLevelIsNormal == nil { 36 | _windowLevelIsNormal = UserDefaults.standard.bool(forKey: windowLevelIsNormalKey) 37 | } 38 | return !(_windowLevelIsNormal ?? false) 39 | } 40 | set { 41 | _windowLevelIsNormal = !newValue 42 | UserDefaults.standard.set(_windowLevelIsNormal, forKey: windowLevelIsNormalKey) 43 | UserDefaults.standard.synchronize() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Cocoa 7 | 8 | class ViewController: NSViewController { 9 | 10 | @IBOutlet var buildManager: BuildManager! 11 | @IBOutlet weak var cancelButton: NSButton! 12 | @IBOutlet weak var compileTimeTextField: NSTextField! 13 | @IBOutlet weak var derivedDataTextField: NSTextField! 14 | @IBOutlet weak var instructionsView: NSView! 15 | @IBOutlet weak var leftButton: NSButton! 16 | @IBOutlet weak var perFileButton: NSButton! 17 | @IBOutlet weak var progressIndicator: NSProgressIndicator! 18 | @IBOutlet weak var projectSelection: ProjectSelection! 19 | @IBOutlet weak var searchField: NSSearchField! 20 | @IBOutlet weak var statusLabel: NSTextField! 21 | @IBOutlet weak var statusTextField: NSTextField! 22 | @IBOutlet weak var tableView: NSTableView! 23 | @IBOutlet weak var tableViewContainerView: NSScrollView! 24 | 25 | private let dataSource = ViewControllerDataSource() 26 | 27 | private var currentKey: String? 28 | private var nextDatabase: XcodeDatabase? 29 | 30 | private(set) var lastProcessedDatabaseSchemeName: String? = nil 31 | { 32 | didSet 33 | { 34 | (NSApp.delegate as? AppDelegate)?.canExport = lastProcessedDatabaseSchemeName != nil 35 | } 36 | } 37 | 38 | private var processor = LogProcessor() 39 | 40 | var processingState: ProcessingState = .waiting { 41 | didSet { 42 | updateViewForState() 43 | } 44 | } 45 | 46 | // MARK: Lifecycle 47 | 48 | override func viewDidLoad() { 49 | super.viewDidLoad() 50 | 51 | configureLayout() 52 | 53 | buildManager.delegate = self 54 | projectSelection.delegate = self 55 | projectSelection.listFolders() 56 | 57 | tableView.tableColumns[0].sortDescriptorPrototype = NSSortDescriptor(key: CompileMeasure.Order.time.rawValue, ascending: true) 58 | tableView.tableColumns[1].sortDescriptorPrototype = NSSortDescriptor(key: CompileMeasure.Order.filename.rawValue, ascending: true) 59 | 60 | NotificationCenter.default.addObserver(self, selector: #selector(windowWillClose(notification:)), name: NSWindow.willCloseNotification, object: nil) 61 | } 62 | 63 | override func viewWillAppear() { 64 | super.viewWillAppear() 65 | 66 | // Set window level before view is displayed 67 | makeWindowTopMost(topMost: UserSettings.windowShouldBeTopMost) 68 | } 69 | 70 | override func viewWillDisappear() { 71 | super.viewWillDisappear() 72 | 73 | // Reset window level before view is hidden 74 | // Reference: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/WinPanel/Concepts/WindowLevel.html 75 | makeWindowTopMost(topMost: false) 76 | } 77 | 78 | @objc func windowWillClose(notification: NSNotification) { 79 | guard let object = notification.object, !(object is NSPanel) else { return } 80 | NotificationCenter.default.removeObserver(self) 81 | 82 | processor.shouldCancel = true 83 | NSApp.terminate(self) 84 | } 85 | 86 | // MARK: Layout 87 | 88 | func configureLayout() { 89 | updateTotalLabel(with: 0) 90 | updateViewForState() 91 | showInstructions(true) 92 | 93 | derivedDataTextField.stringValue = UserSettings.derivedDataLocation 94 | makeWindowTopMost(topMost: UserSettings.windowShouldBeTopMost) 95 | } 96 | 97 | func showInstructions(_ show: Bool) { 98 | instructionsView.isHidden = !show 99 | 100 | let views: [NSView] = [compileTimeTextField, leftButton, perFileButton, searchField, statusLabel, statusTextField, tableViewContainerView] 101 | views.forEach{ $0.isHidden = show } 102 | 103 | if show && processingState == .processing { 104 | processor.shouldCancel = true 105 | cancelButton.isHidden = true 106 | progressIndicator.isHidden = true 107 | } 108 | } 109 | 110 | func updateViewForState() { 111 | switch processingState { 112 | case .processing: 113 | showInstructions(false) 114 | progressIndicator.isHidden = false 115 | progressIndicator.startAnimation(self) 116 | statusTextField.stringValue = ProcessingState.processingString 117 | cancelButton.isHidden = false 118 | 119 | case .completed(_, let stateName): 120 | progressIndicator.isHidden = true 121 | progressIndicator.stopAnimation(self) 122 | statusTextField.stringValue = stateName 123 | cancelButton.isHidden = true 124 | 125 | case .waiting: 126 | progressIndicator.isHidden = true 127 | progressIndicator.stopAnimation(self) 128 | statusTextField.stringValue = ProcessingState.waitingForBuildString 129 | cancelButton.isHidden = true 130 | } 131 | 132 | if instructionsView.isHidden { 133 | searchField.isHidden = !cancelButton.isHidden 134 | } 135 | } 136 | 137 | func makeWindowTopMost(topMost: Bool) { 138 | if let window = NSApplication.shared.windows.first { 139 | let level: CGWindowLevelKey = topMost ? .floatingWindow : .normalWindow 140 | window.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(level))) 141 | } 142 | } 143 | 144 | // MARK: Actions 145 | 146 | @IBAction func perFileCheckboxClicked(_ sender: NSButton) { 147 | dataSource.aggregateByFile = (sender.state.rawValue == 1) 148 | tableView.reloadData() 149 | } 150 | 151 | @IBAction func clipboardButtonClicked(_ sender: AnyObject) { 152 | NSPasteboard.general.clearContents() 153 | NSPasteboard.general.writeObjects(["-Xfrontend -debug-time-function-bodies" as NSPasteboardWriting]) 154 | } 155 | 156 | @IBAction func visitDerivedData(_ sender: AnyObject) { 157 | let url = URL(fileURLWithPath: derivedDataTextField.stringValue, isDirectory: true) 158 | NSWorkspace.shared.open(url) 159 | } 160 | 161 | 162 | @IBAction func cancelButtonClicked(_ sender: AnyObject) { 163 | processor.shouldCancel = true 164 | } 165 | 166 | @IBAction func leftButtonClicked(_ sender: NSButton) { 167 | configureMenuItems(showBuildTimesMenuItem: true) 168 | 169 | cancelProcessing() 170 | showInstructions(true) 171 | projectSelection.listFolders() 172 | } 173 | 174 | @IBAction func exportAsCSVClicked(_ sender: Any?) { 175 | guard let keyWindow = NSApp.keyWindow, let scheme = lastProcessedDatabaseSchemeName else { 176 | return 177 | } 178 | 179 | let exporter = CSVExporter() 180 | 181 | let savePanel = NSSavePanel() 182 | savePanel.title = "Exporting data as CSV…" 183 | savePanel.message = "Pick location for CSV file to be exported:" 184 | savePanel.prompt = "Export" 185 | savePanel.allowedFileTypes = ["csv"] 186 | savePanel.nameFieldStringValue = exporter.filename(with: scheme) 187 | 188 | savePanel.beginSheetModal(for: keyWindow) { [dataSource] (response) in 189 | guard response == NSApplication.ModalResponse.OK, let fileUrl = savePanel.url else { 190 | return 191 | } 192 | 193 | do 194 | { 195 | try dataSource.exportProcessedData(using: exporter, to: fileUrl) 196 | } 197 | catch 198 | { 199 | NSAlert(error: error).runModal() 200 | } 201 | } 202 | } 203 | 204 | func controlTextDidChange(_ obj: Notification) { 205 | if let field = obj.object as? NSSearchField, field == searchField { 206 | dataSource.filter = searchField.stringValue 207 | tableView.reloadData() 208 | } else if let field = obj.object as? NSTextField, field == derivedDataTextField { 209 | buildManager.stopMonitoring() 210 | UserSettings.derivedDataLocation = field.stringValue 211 | 212 | projectSelection.listFolders() 213 | buildManager.startMonitoring() 214 | } 215 | } 216 | 217 | // MARK: Utilities 218 | 219 | func cancelProcessing() { 220 | guard processingState == .processing else { return } 221 | 222 | processor.shouldCancel = true 223 | cancelButton.isHidden = true 224 | } 225 | 226 | func configureMenuItems(showBuildTimesMenuItem: Bool) { 227 | if let appDelegate = NSApp.delegate as? AppDelegate { 228 | appDelegate.configureMenuItems(showBuildTimesMenuItem: showBuildTimesMenuItem) 229 | } 230 | } 231 | 232 | func processLog(with database: XcodeDatabase) { 233 | guard processingState != .processing else { 234 | if let currentKey = currentKey, currentKey != database.key { 235 | nextDatabase = database 236 | processor.shouldCancel = true 237 | } 238 | return 239 | } 240 | 241 | configureMenuItems(showBuildTimesMenuItem: false) 242 | 243 | processingState = .processing 244 | currentKey = database.key 245 | lastProcessedDatabaseSchemeName = database.schemeName 246 | 247 | updateTotalLabel(with: database.buildTime) 248 | 249 | processor.processDatabase(database: database) { [weak self] (result, didComplete, didCancel) in 250 | self?.handleProcessorUpdate(result: result, didComplete: didComplete, didCancel: didCancel) 251 | } 252 | } 253 | 254 | func handleProcessorUpdate(result: [CompileMeasure], didComplete: Bool, didCancel: Bool) { 255 | dataSource.resetSourceData(newSourceData: result) 256 | tableView.reloadData() 257 | 258 | if didComplete { 259 | completeProcessorUpdate(didCancel: didCancel) 260 | } 261 | } 262 | 263 | func completeProcessorUpdate(didCancel: Bool) { 264 | let didSucceed = !dataSource.isEmpty() 265 | 266 | var stateName = ProcessingState.failedString 267 | if didCancel { 268 | stateName = ProcessingState.cancelledString 269 | } else if didSucceed { 270 | stateName = ProcessingState.completedString 271 | } 272 | 273 | processingState = .completed(didSucceed: didSucceed, stateName: stateName) 274 | currentKey = nil 275 | 276 | if let nextDatabase = nextDatabase { 277 | self.nextDatabase = nil 278 | processLog(with: nextDatabase) 279 | } 280 | 281 | if !didSucceed { 282 | let text = "Ensure the Swift compiler flags has been added." 283 | NSAlert.show(withMessage: ProcessingState.failedString, andInformativeText: text) 284 | 285 | showInstructions(true) 286 | configureMenuItems(showBuildTimesMenuItem: true) 287 | } 288 | } 289 | 290 | func updateTotalLabel(with buildTime: Int) { 291 | let text = "Build duration: " + (buildTime < 60 ? "\(buildTime)s" : "\(buildTime / 60)m \(buildTime % 60)s") 292 | compileTimeTextField.stringValue = text 293 | } 294 | } 295 | 296 | // MARK: NSTableViewDataSource 297 | 298 | extension ViewController: NSTableViewDataSource { 299 | func numberOfRows(in tableView: NSTableView) -> Int { 300 | return dataSource.count() 301 | } 302 | 303 | func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { 304 | guard let item = dataSource.measure(index: row) else { return false } 305 | let url = URL(fileURLWithPath: item.path, isDirectory: true) 306 | NSWorkspace.shared.open(url) 307 | 308 | let gotoLineScript = 309 | "tell application \"Xcode\"\n" + 310 | " activate\n" + 311 | "end tell\n" + 312 | "tell application \"System Events\"\n" + 313 | " keystroke \"l\" using command down\n" + 314 | " keystroke \"\(item.location)\"\n" + 315 | " keystroke return\n" + 316 | "end tell" 317 | 318 | DispatchQueue.global().async { 319 | if let script = NSAppleScript(source: gotoLineScript) { 320 | script.executeAndReturnError(nil) 321 | } 322 | } 323 | 324 | return true 325 | } 326 | } 327 | 328 | // MARK: NSTableViewDelegate 329 | 330 | extension ViewController: NSTableViewDelegate { 331 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 332 | guard let tableColumn = tableColumn, let columnIndex = tableView.tableColumns.firstIndex(of: tableColumn) else { return nil } 333 | guard let item = dataSource.measure(index: row) else { return nil } 334 | 335 | let result = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell\(columnIndex)"), owner: self) as? NSTableCellView 336 | result?.textField?.stringValue = item[columnIndex] 337 | 338 | return result 339 | } 340 | 341 | func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) { 342 | dataSource.sortDescriptors = tableView.sortDescriptors 343 | tableView.reloadData() 344 | } 345 | } 346 | 347 | // MARK: BuildManagerDelegate 348 | 349 | extension ViewController: BuildManagerDelegate { 350 | func buildManager(_ buildManager: BuildManager, shouldParseLogWithDatabase database: XcodeDatabase) { 351 | processLog(with: database) 352 | } 353 | 354 | func derivedDataDidChange() { 355 | projectSelection.listFolders() 356 | } 357 | } 358 | 359 | // MARK: ProjectSelectionDelegate 360 | 361 | extension ViewController: ProjectSelectionDelegate { 362 | func didSelectProject(with database: XcodeDatabase) { 363 | processLog(with: database) 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/ViewControllerDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerDataSource.swift 3 | // BuildTimeAnalyzer 4 | // 5 | // Created by Dmitrii on 02/12/2017. 6 | // Copyright © 2017 Cane Media Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ViewControllerDataSource { 12 | 13 | var aggregateByFile = false { 14 | didSet { 15 | processData() 16 | } 17 | } 18 | 19 | var filter = "" { 20 | didSet { 21 | processData() 22 | } 23 | } 24 | 25 | var sortDescriptors = [NSSortDescriptor]() { 26 | didSet { 27 | processData() 28 | } 29 | } 30 | 31 | private var originalData = [CompileMeasure]() 32 | private var processedData = [CompileMeasure]() 33 | 34 | func resetSourceData(newSourceData: [CompileMeasure]) { 35 | originalData = newSourceData 36 | processData() 37 | } 38 | 39 | func isEmpty() -> Bool { 40 | return processedData.isEmpty 41 | } 42 | 43 | func count() -> Int { 44 | return processedData.count 45 | } 46 | 47 | func measure(index: Int) -> CompileMeasure? { 48 | guard index < processedData.count && index >= 0 else { return nil } 49 | return processedData[index] 50 | } 51 | 52 | func exportProcessedData(using exporter: CSVExporter, to url: URL) throws { 53 | try exporter.export(elements: processedData, to: url) 54 | } 55 | 56 | // MARK: - Private methods 57 | 58 | private func processData() { 59 | var newProcessedData = aggregateIfNeeded(originalData) 60 | newProcessedData = applySortingIfNeeded(newProcessedData) 61 | newProcessedData = applyFilteringIfNeeded(newProcessedData) 62 | 63 | processedData = newProcessedData 64 | } 65 | 66 | private func aggregateIfNeeded(_ input: [CompileMeasure]) -> [CompileMeasure] { 67 | guard aggregateByFile else { return input } 68 | var fileTimes: [String: CompileMeasure] = [:] 69 | for measure in input { 70 | if let fileMeasure = fileTimes[measure.path] { 71 | fileMeasure.time += measure.time 72 | fileTimes[measure.path] = fileMeasure 73 | } else { 74 | let newFileMeasure = CompileMeasure(rawPath: measure.path, time: measure.time) 75 | fileTimes[measure.path] = newFileMeasure 76 | } 77 | } 78 | return Array(fileTimes.values) 79 | } 80 | 81 | private func applySortingIfNeeded(_ input: [CompileMeasure]) -> [CompileMeasure] { 82 | if sortDescriptors.isEmpty { return input } 83 | return (input as NSArray).sortedArray(using: sortDescriptors) as! Array 84 | } 85 | 86 | private func applyFilteringIfNeeded(_ input: [CompileMeasure]) -> [CompileMeasure] { 87 | guard !filter.isEmpty else { return input } 88 | return input.filter{ textContains($0.code, pattern: filter) || textContains($0.filename, pattern: filter) } 89 | } 90 | 91 | private func textContains(_ text: String, pattern: String) -> Bool { 92 | return text.lowercased().contains(pattern.lowercased()) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /BuildTimeAnalyzer/XcodeDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcodeDatabase.swift 3 | // BuildTimeAnalyzer 4 | // 5 | 6 | import Foundation 7 | 8 | struct XcodeDatabase { 9 | var path: String 10 | var modificationDate: Date 11 | 12 | var key: String 13 | var schemeName: String 14 | var title: String 15 | var timeStartedRecording: Int 16 | var timeStoppedRecording: Int 17 | 18 | var isBuildType: Bool { 19 | return title.hasPrefix("Build ") || title.hasPrefix("Compile ") 20 | } 21 | 22 | var url: URL { 23 | return URL(fileURLWithPath: path) 24 | } 25 | 26 | var logUrl: URL { 27 | return folderPath.appendingPathComponent("\(key).xcactivitylog") 28 | } 29 | 30 | var folderPath: URL { 31 | return url.deletingLastPathComponent() 32 | } 33 | 34 | var buildTime: Int { 35 | return timeStoppedRecording - timeStartedRecording 36 | } 37 | 38 | init?(fromPath path: String) { 39 | guard let data = NSDictionary(contentsOfFile: path)?["logs"] as? [String: AnyObject], 40 | let key = XcodeDatabase.sortKeys(usingData: data).last?.key, 41 | let value = data[key] as? [String : AnyObject], 42 | let schemeName = value["schemeIdentifier-schemeName"] as? String, 43 | let title = value["title"] as? String, 44 | let timeStartedRecording = value["timeStartedRecording"] as? NSNumber, 45 | let timeStoppedRecording = value["timeStoppedRecording"] as? NSNumber, 46 | let fileAttributes = try? FileManager.default.attributesOfItem(atPath: path), 47 | let modificationDate = fileAttributes[FileAttributeKey.modificationDate] as? Date 48 | else { return nil } 49 | 50 | self.modificationDate = modificationDate 51 | self.path = path 52 | self.key = key 53 | self.schemeName = schemeName 54 | self.title = title 55 | self.timeStartedRecording = timeStartedRecording.intValue 56 | self.timeStoppedRecording = timeStoppedRecording.intValue 57 | } 58 | 59 | func processLog() -> String? { 60 | if let rawData = try? Data(contentsOf: URL(fileURLWithPath: logUrl.path)), 61 | let data = (rawData as NSData).gunzipped() { 62 | return String(data: data, encoding: String.Encoding.utf8) 63 | } 64 | return nil 65 | } 66 | 67 | static private func sortKeys(usingData data: [String: AnyObject]) -> [(Int, key: String)] { 68 | var sortedKeys: [(Int, key: String)] = [] 69 | for key in data.keys { 70 | if let value = data[key] as? [String: AnyObject], 71 | let timeStoppedRecording = value["timeStoppedRecording"] as? NSNumber { 72 | sortedKeys.append((timeStoppedRecording.intValue, key)) 73 | } 74 | } 75 | return sortedKeys.sorted{ $0.0 < $1.0 } 76 | } 77 | } 78 | 79 | extension XcodeDatabase : Equatable {} 80 | 81 | func ==(lhs: XcodeDatabase, rhs: XcodeDatabase) -> Bool { 82 | return lhs.path == rhs.path && lhs.modificationDate == rhs.modificationDate 83 | } 84 | -------------------------------------------------------------------------------- /BuildTimeAnalyzerTests-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "NSData+GZIP.h" -------------------------------------------------------------------------------- /BuildTimeAnalyzerTests/CompileMeasureTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompileMeasureTests.swift 3 | // CMBuildTimeAnalyzerTests 4 | // 5 | 6 | import XCTest 7 | 8 | @testable import BuildTimeAnalyzer 9 | 10 | class BuildTimeAnalyzerTests: XCTestCase { 11 | 12 | func testInit() { 13 | // Given 14 | let time = 25.3 15 | let timeString = "\(time)ms" 16 | let filename = "Code.Swift" 17 | let fileInfo = "\(filename):10:23" 18 | let location = 10 19 | let folder = "/User/JohnAppleseed/" 20 | let path = "\(folder)\(filename)" 21 | let rawPath = "\(folder)\(fileInfo)" 22 | let code = "some code" 23 | let references = 2 24 | 25 | // When 26 | let resultOptional = CompileMeasure(time: time, rawPath: rawPath, code: code, references: references) 27 | 28 | // Then 29 | XCTAssertNotNil(resultOptional) 30 | guard let result = resultOptional else { return } 31 | 32 | XCTAssertEqual(result.time, time) 33 | XCTAssertEqual(result.code, code) 34 | XCTAssertEqual(result.path, path) 35 | XCTAssertEqual(result.fileInfo, fileInfo) 36 | XCTAssertEqual(result.filename, filename) 37 | XCTAssertEqual(result.location, location) 38 | XCTAssertEqual(result.timeString, timeString) 39 | XCTAssertEqual(result.references, references) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /BuildTimeAnalyzerTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /BuildTimeAnalyzerTests/ViewControllerDataSourceTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerDataSourceTest.swift 3 | // BuildTimeAnalyzerTests 4 | // 5 | // Created by Dmitrii on 02/12/2017. 6 | // Copyright © 2017 Cane Media Ltd. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import BuildTimeAnalyzer 11 | 12 | class ViewControllerDataSourceTest: XCTestCase { 13 | 14 | var measArray: [CompileMeasure]! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | let meas1 = CompileMeasure(rawPath: "FileName1.swift:1:1", time: 10) 19 | let meas2 = CompileMeasure(rawPath: "FileName2.swift:2:2", time: 2) 20 | let meas3 = CompileMeasure(rawPath: "FileName3.swift:3:3", time: 8) 21 | let meas4 = CompileMeasure(rawPath: "FileName3.swift:4:4", time: 0) 22 | let meas5 = CompileMeasure(rawPath: "FileName1.swift:5:5", time: 2) 23 | measArray = [meas4!, meas5!, meas2!, meas3!, meas1!] 24 | } 25 | 26 | func testInit() { 27 | let dataSource = ViewControllerDataSource() 28 | 29 | XCTAssertFalse(dataSource.aggregateByFile) 30 | XCTAssertEqual(dataSource.filter, "") 31 | XCTAssertNotNil(dataSource.sortDescriptors) 32 | XCTAssertEqual(dataSource.sortDescriptors.count, 0) 33 | XCTAssertTrue(dataSource.isEmpty()) 34 | } 35 | 36 | func testAggregate() { 37 | let dataSource = ViewControllerDataSource() 38 | dataSource.resetSourceData(newSourceData: measArray) 39 | dataSource.aggregateByFile = true 40 | 41 | XCTAssertEqual(dataSource.count(), 3) 42 | XCTAssertFalse(dataSource.isEmpty()) 43 | } 44 | 45 | func testFilter_1() { 46 | let dataSource = ViewControllerDataSource() 47 | dataSource.resetSourceData(newSourceData: measArray) 48 | dataSource.filter = "1" 49 | 50 | XCTAssertFalse(dataSource.isEmpty()) 51 | XCTAssertEqual(dataSource.count(), 2) 52 | XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName1.swift") 53 | XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName1.swift") 54 | } 55 | 56 | func testFilter_2() { 57 | let dataSource = ViewControllerDataSource() 58 | dataSource.resetSourceData(newSourceData: measArray) 59 | dataSource.filter = "2" 60 | 61 | XCTAssertFalse(dataSource.isEmpty()) 62 | XCTAssertEqual(dataSource.count(), 1) 63 | XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName2.swift") 64 | } 65 | 66 | func testFilter_noMatch() { 67 | let dataSource = ViewControllerDataSource() 68 | dataSource.resetSourceData(newSourceData: measArray) 69 | dataSource.filter = "noMatch" 70 | 71 | XCTAssertTrue(dataSource.isEmpty()) 72 | XCTAssertEqual(dataSource.count(), 0) 73 | } 74 | 75 | func testSortTimeAscending() { 76 | let dataSource = ViewControllerDataSource() 77 | dataSource.resetSourceData(newSourceData: measArray) 78 | let desc = NSSortDescriptor(key: "time", ascending: true) 79 | dataSource.sortDescriptors = [desc] 80 | 81 | XCTAssertFalse(dataSource.isEmpty()) 82 | XCTAssertEqual(dataSource.count(), 5) 83 | XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") 84 | XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName1.swift") 85 | XCTAssertEqual(dataSource.measure(index: 2)!.filename, "FileName2.swift") 86 | XCTAssertEqual(dataSource.measure(index: 3)!.filename, "FileName3.swift") 87 | XCTAssertEqual(dataSource.measure(index: 4)!.filename, "FileName1.swift") 88 | } 89 | 90 | func testSortFilenameDescending() { 91 | let dataSource = ViewControllerDataSource() 92 | dataSource.resetSourceData(newSourceData: measArray) 93 | let desc = NSSortDescriptor(key: "filename", ascending: false) 94 | dataSource.sortDescriptors = [desc] 95 | 96 | XCTAssertFalse(dataSource.isEmpty()) 97 | XCTAssertEqual(dataSource.count(), 5) 98 | XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") 99 | XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName3.swift") 100 | XCTAssertEqual(dataSource.measure(index: 2)!.filename, "FileName2.swift") 101 | XCTAssertEqual(dataSource.measure(index: 3)!.filename, "FileName1.swift") 102 | XCTAssertEqual(dataSource.measure(index: 4)!.filename, "FileName1.swift") 103 | } 104 | 105 | func testSortFilenameAscending_TimeAscending() { 106 | let dataSource = ViewControllerDataSource() 107 | dataSource.resetSourceData(newSourceData: measArray) 108 | let descFilename = NSSortDescriptor(key: "filename", ascending: true) 109 | let descTime = NSSortDescriptor(key: "time", ascending: true) 110 | dataSource.sortDescriptors = [descFilename, descTime] 111 | 112 | XCTAssertFalse(dataSource.isEmpty()) 113 | XCTAssertEqual(dataSource.count(), 5) 114 | XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName1.swift") 115 | XCTAssertEqual(dataSource.measure(index: 0)!.time, 2) 116 | XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName1.swift") 117 | XCTAssertEqual(dataSource.measure(index: 1)!.time, 10) 118 | XCTAssertEqual(dataSource.measure(index: 2)!.filename, "FileName2.swift") 119 | XCTAssertEqual(dataSource.measure(index: 3)!.filename, "FileName3.swift") 120 | XCTAssertEqual(dataSource.measure(index: 3)!.time, 0) 121 | XCTAssertEqual(dataSource.measure(index: 4)!.filename, "FileName3.swift") 122 | XCTAssertEqual(dataSource.measure(index: 4)!.time, 8) 123 | } 124 | 125 | func testSortTimeAscending_FilenameDescending() { 126 | let dataSource = ViewControllerDataSource() 127 | dataSource.resetSourceData(newSourceData: measArray) 128 | let descTime = NSSortDescriptor(key: "time", ascending: true) 129 | let descFilename = NSSortDescriptor(key: "filename", ascending: false) 130 | dataSource.sortDescriptors = [descTime, descFilename] 131 | 132 | XCTAssertFalse(dataSource.isEmpty()) 133 | XCTAssertEqual(dataSource.count(), 5) 134 | XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") 135 | XCTAssertEqual(dataSource.measure(index: 0)!.time, 0) 136 | XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName2.swift") 137 | XCTAssertEqual(dataSource.measure(index: 1)!.time, 2) 138 | XCTAssertEqual(dataSource.measure(index: 2)!.filename, "FileName1.swift") 139 | XCTAssertEqual(dataSource.measure(index: 2)!.time, 2) 140 | XCTAssertEqual(dataSource.measure(index: 3)!.filename, "FileName3.swift") 141 | XCTAssertEqual(dataSource.measure(index: 3)!.time, 8) 142 | XCTAssertEqual(dataSource.measure(index: 4)!.filename, "FileName1.swift") 143 | XCTAssertEqual(dataSource.measure(index: 4)!.time, 10) 144 | } 145 | 146 | func testSortTimeAscending_Filter3() { 147 | let dataSource = ViewControllerDataSource() 148 | dataSource.resetSourceData(newSourceData: measArray) 149 | let descTime = NSSortDescriptor(key: "time", ascending: true) 150 | dataSource.sortDescriptors = [descTime] 151 | dataSource.filter = "3" 152 | 153 | XCTAssertFalse(dataSource.isEmpty()) 154 | XCTAssertEqual(dataSource.count(), 2) 155 | XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") 156 | XCTAssertEqual(dataSource.measure(index: 0)!.time, 0) 157 | XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName3.swift") 158 | XCTAssertEqual(dataSource.measure(index: 1)!.time, 8) 159 | } 160 | 161 | func testFilter3_Aggregate() { 162 | let dataSource = ViewControllerDataSource() 163 | dataSource.resetSourceData(newSourceData: measArray) 164 | dataSource.filter = "3" 165 | dataSource.aggregateByFile = true 166 | 167 | XCTAssertFalse(dataSource.isEmpty()) 168 | XCTAssertEqual(dataSource.count(), 1) 169 | XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") 170 | } 171 | 172 | func testSortFilenameDescending_FilterCanceled_Aggregate() { 173 | let dataSource = ViewControllerDataSource() 174 | dataSource.resetSourceData(newSourceData: measArray) 175 | let descFilename = NSSortDescriptor(key: "filename", ascending: false) 176 | dataSource.sortDescriptors = [descFilename] 177 | dataSource.filter = "2" 178 | dataSource.aggregateByFile = true 179 | dataSource.filter = "" 180 | 181 | XCTAssertFalse(dataSource.isEmpty()) 182 | XCTAssertEqual(dataSource.count(), 3) 183 | XCTAssertEqual(dataSource.measure(index: 0)!.filename, "FileName3.swift") 184 | XCTAssertEqual(dataSource.measure(index: 1)!.filename, "FileName2.swift") 185 | XCTAssertEqual(dataSource.measure(index: 2)!.filename, "FileName1.swift") 186 | 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /GZIP/NSData+GZIP.h: -------------------------------------------------------------------------------- 1 | // 2 | // GZIP.h 3 | // 4 | // Version 1.1.1 5 | // 6 | // Created by Nick Lockwood on 03/06/2012. 7 | // Copyright (C) 2012 Charcoal Design 8 | // 9 | // Distributed under the permissive zlib License 10 | // Get the latest version from here: 11 | // 12 | // https://github.com/nicklockwood/GZIP 13 | // 14 | // This software is provided 'as-is', without any express or implied 15 | // warranty. In no event will the authors be held liable for any damages 16 | // arising from the use of this software. 17 | // 18 | // Permission is granted to anyone to use this software for any purpose, 19 | // including commercial applications, and to alter it and redistribute it 20 | // freely, subject to the following restrictions: 21 | // 22 | // 1. The origin of this software must not be misrepresented; you must not 23 | // claim that you wrote the original software. If you use this software 24 | // in a product, an acknowledgment in the product documentation would be 25 | // appreciated but is not required. 26 | // 27 | // 2. Altered source versions must be plainly marked as such, and must not be 28 | // misrepresented as being the original software. 29 | // 30 | // 3. This notice may not be removed or altered from any source distribution. 31 | // 32 | 33 | 34 | #import 35 | 36 | @interface NSData (GZIP) 37 | 38 | - (nullable NSData *)gzippedDataWithCompressionLevel:(float)level; 39 | - (nullable NSData *)gzippedData; 40 | - (nullable NSData *)gunzippedData; 41 | - (BOOL)isGzippedData; 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /GZIP/NSData+GZIP.m: -------------------------------------------------------------------------------- 1 | // 2 | // GZIP.m 3 | // 4 | // Version 1.1.1 5 | // 6 | // Created by Nick Lockwood on 03/06/2012. 7 | // Copyright (C) 2012 Charcoal Design 8 | // 9 | // Distributed under the permissive zlib License 10 | // Get the latest version from here: 11 | // 12 | // https://github.com/nicklockwood/GZIP 13 | // 14 | // This software is provided 'as-is', without any express or implied 15 | // warranty. In no event will the authors be held liable for any damages 16 | // arising from the use of this software. 17 | // 18 | // Permission is granted to anyone to use this software for any purpose, 19 | // including commercial applications, and to alter it and redistribute it 20 | // freely, subject to the following restrictions: 21 | // 22 | // 1. The origin of this software must not be misrepresented; you must not 23 | // claim that you wrote the original software. If you use this software 24 | // in a product, an acknowledgment in the product documentation would be 25 | // appreciated but is not required. 26 | // 27 | // 2. Altered source versions must be plainly marked as such, and must not be 28 | // misrepresented as being the original software. 29 | // 30 | // 3. This notice may not be removed or altered from any source distribution. 31 | // 32 | 33 | 34 | #import "NSData+GZIP.h" 35 | #import 36 | #import 37 | 38 | 39 | #pragma clang diagnostic ignored "-Wcast-qual" 40 | 41 | 42 | @implementation NSData (GZIP) 43 | 44 | static void *libzOpen() 45 | { 46 | static void *libz; 47 | static dispatch_once_t onceToken; 48 | dispatch_once(&onceToken, ^{ 49 | libz = dlopen("/usr/lib/libz.dylib", RTLD_LAZY); 50 | }); 51 | return libz; 52 | } 53 | 54 | - (NSData *)gzippedDataWithCompressionLevel:(float)level 55 | { 56 | if (self.length == 0 || [self isGzippedData]) 57 | { 58 | return self; 59 | } 60 | 61 | void *libz = libzOpen(); 62 | int (*deflateInit2_)(z_streamp, int, int, int, int, int, const char *, int) = 63 | (int (*)(z_streamp, int, int, int, int, int, const char *, int))dlsym(libz, "deflateInit2_"); 64 | int (*deflate)(z_streamp, int) = (int (*)(z_streamp, int))dlsym(libz, "deflate"); 65 | int (*deflateEnd)(z_streamp) = (int (*)(z_streamp))dlsym(libz, "deflateEnd"); 66 | 67 | z_stream stream; 68 | stream.zalloc = Z_NULL; 69 | stream.zfree = Z_NULL; 70 | stream.opaque = Z_NULL; 71 | stream.avail_in = (uint)self.length; 72 | stream.next_in = (Bytef *)(void *)self.bytes; 73 | stream.total_out = 0; 74 | stream.avail_out = 0; 75 | 76 | static const NSUInteger ChunkSize = 16384; 77 | 78 | NSMutableData *output = nil; 79 | int compression = (level < 0.0f)? Z_DEFAULT_COMPRESSION: (int)(roundf(level * 9)); 80 | if (deflateInit2(&stream, compression, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK) 81 | { 82 | output = [NSMutableData dataWithLength:ChunkSize]; 83 | while (stream.avail_out == 0) 84 | { 85 | if (stream.total_out >= output.length) 86 | { 87 | output.length += ChunkSize; 88 | } 89 | stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out; 90 | stream.avail_out = (uInt)(output.length - stream.total_out); 91 | deflate(&stream, Z_FINISH); 92 | } 93 | deflateEnd(&stream); 94 | output.length = stream.total_out; 95 | } 96 | 97 | return output; 98 | } 99 | 100 | - (NSData *)gzippedData 101 | { 102 | return [self gzippedDataWithCompressionLevel:-1.0f]; 103 | } 104 | 105 | - (NSData *)gunzippedData 106 | { 107 | if (self.length == 0 || ![self isGzippedData]) 108 | { 109 | return self; 110 | } 111 | 112 | void *libz = libzOpen(); 113 | int (*inflateInit2_)(z_streamp, int, const char *, int) = 114 | (int (*)(z_streamp, int, const char *, int))dlsym(libz, "inflateInit2_"); 115 | int (*inflate)(z_streamp, int) = (int (*)(z_streamp, int))dlsym(libz, "inflate"); 116 | int (*inflateEnd)(z_streamp) = (int (*)(z_streamp))dlsym(libz, "inflateEnd"); 117 | 118 | z_stream stream; 119 | stream.zalloc = Z_NULL; 120 | stream.zfree = Z_NULL; 121 | stream.avail_in = (uint)self.length; 122 | stream.next_in = (Bytef *)self.bytes; 123 | stream.total_out = 0; 124 | stream.avail_out = 0; 125 | 126 | NSMutableData *output = nil; 127 | if (inflateInit2(&stream, 47) == Z_OK) 128 | { 129 | int status = Z_OK; 130 | output = [NSMutableData dataWithCapacity:self.length * 2]; 131 | while (status == Z_OK) 132 | { 133 | if (stream.total_out >= output.length) 134 | { 135 | output.length += self.length / 2; 136 | } 137 | stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out; 138 | stream.avail_out = (uInt)(output.length - stream.total_out); 139 | status = inflate (&stream, Z_SYNC_FLUSH); 140 | } 141 | if (inflateEnd(&stream) == Z_OK) 142 | { 143 | if (status == Z_STREAM_END) 144 | { 145 | output.length = stream.total_out; 146 | } 147 | } 148 | } 149 | 150 | return output; 151 | } 152 | 153 | - (BOOL)isGzippedData 154 | { 155 | const UInt8 *bytes = (const UInt8 *)self.bytes; 156 | return (self.length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b); 157 | } 158 | 159 | @end 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Robert Gummesson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Build Time Analyzer for Xcode 2 | ====================== 3 | 4 | [![GitHub release](https://img.shields.io/github/release/RobertGummesson/BuildTimeAnalyzer-for-Xcode.svg)](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/releases/latest) 5 | [![Build Status](https://travis-ci.org/RobertGummesson/BuildTimeAnalyzer-for-Xcode.svg?branch=master)](https://travis-ci.org/RobertGummesson/BuildTimeAnalyzer-for-Xcode) 6 | [![Swift](https://img.shields.io/badge/Swift-4.1-orange.svg)](https://swift.org) 7 | [![Platform](https://img.shields.io/badge/platform-osx-blue.svg)](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode) 8 | [![MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 9 | 10 | ## Overview 11 | 12 | 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. 13 | 14 | ## Usage 15 | 16 | Open up the app and follow the instructions. 17 | 18 | ![screenshot.png](https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/master/Screenshots/screenshot.png) 19 | 20 | ## Installation 21 | 22 | Download the code and open it in Xcode, archive the project and export the build. Easy, right? 23 | 24 | ## Contributions 25 | 26 | If you encounter any issues or have ideas for improvement, I am open to code contributions. 27 | 28 | ## License 29 | 30 | Copyright (c) 2016-2018, Robert Gummesson 31 | All rights reserved. 32 | 33 | Redistribution and use in source and binary forms, with or without 34 | modification, are permitted provided that the following conditions are met: 35 | 36 | * Redistributions of source code must retain the above copyright notice, this 37 | list of conditions and the following disclaimer. 38 | 39 | * Redistributions in binary form must reproduce the above copyright notice, 40 | this list of conditions and the following disclaimer in the documentation 41 | and/or other materials provided with the distribution. 42 | 43 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 44 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 45 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 46 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 47 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 48 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 49 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 50 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 51 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 52 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 53 | -------------------------------------------------------------------------------- /Screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/18d281084787ceefbadf7c6cf29f60f7c86aa10f/Screenshots/screenshot.png --------------------------------------------------------------------------------