diff --git a/BuildTimeAnalyzer.xcodeproj/project.pbxproj b/BuildTimeAnalyzer.xcodeproj/project.pbxproj index 6e0d641..af7052c 100644 --- a/BuildTimeAnalyzer.xcodeproj/project.pbxproj +++ b/BuildTimeAnalyzer.xcodeproj/project.pbxproj @@ -7,75 +7,75 @@ objects = { /* Begin PBXBuildFile section */ - 2A14D30B1CDA4F380079BFF0 /* NSData+GZIP.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A69BFFB1CD513D700A554A3 /* NSData+GZIP.m */; }; - 2A1682C31CD4039300014B7F /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A1682C21CD4039300014B7F /* Cocoa.framework */; }; - 2A1682C51CD403FB00014B7F /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A1682C41CD403FB00014B7F /* AppKit.framework */; }; - 2A1682C71CD4040900014B7F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A1682C61CD4040900014B7F /* Foundation.framework */; }; - 2A1682CA1CD4042800014B7F /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A1682C91CD4042800014B7F /* CoreData.framework */; }; - 2A71846E1CDA483E00E8B7D3 /* NSNotificationCenter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A71846D1CDA483E00E8B7D3 /* NSNotificationCenter+Extensions.swift */; }; - 2A71846F1CDA48AB00E8B7D3 /* CMCompileMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1B1E7F1CD768C300ADD414 /* CMCompileMeasure.swift */; }; - 2A74ACC51CD5733B0057B819 /* CMResultWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2A74ACC41CD5733B0057B819 /* CMResultWindow.xib */; }; - 2A7DBBF51CD8E5D1009E9B49 /* CMCompileMeasureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7DBBF41CD8E5D1009E9B49 /* CMCompileMeasureTests.swift */; }; - 2A9BDFEB1CD95196000E5862 /* Icons.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A9BDFEA1CD95196000E5862 /* Icons.xcassets */; }; - 2ABB72011CDA4680001FDC32 /* CMBuildOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1854A31CD8BB74005498BF /* CMBuildOperation.swift */; }; - 2ABB72021CDA4680001FDC32 /* CMCompileMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1B1E7F1CD768C300ADD414 /* CMCompileMeasure.swift */; }; - 2ABB72031CDA4680001FDC32 /* CMPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1682C01CD3FBCC00014B7F /* CMPlugin.swift */; }; - 2ABB72041CDA4680001FDC32 /* CMLogProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A74ACC11CD571470057B819 /* CMLogProcessor.swift */; }; - 2ABB72051CDA4680001FDC32 /* CMProcessingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5A07241CD64DCA009F0ECE /* CMProcessingState.swift */; }; - 2ABB72061CDA4680001FDC32 /* CMResultWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A74ACCB1CD573CD0057B819 /* CMResultWindowController.swift */; }; - 2ABB72071CDA4680001FDC32 /* CMXcodeWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A69BFFD1CD5179200A554A3 /* CMXcodeWorkspace.swift */; }; - 2AE38C571CDA8222008B65AE /* CMRawMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE38C561CDA8222008B65AE /* CMRawMeasure.swift */; }; + 2A3164C81D21D73F00064045 /* CompileMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C01D21D73F00064045 /* CompileMeasure.swift */; }; + 2A3164C91D21D73F00064045 /* LogProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C11D21D73F00064045 /* LogProcessor.swift */; }; + 2A3164CB1D21D73F00064045 /* ProcessingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C31D21D73F00064045 /* ProcessingState.swift */; }; + 2A3164CC1D21D73F00064045 /* RawMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164C41D21D73F00064045 /* RawMeasure.swift */; }; + 2A3164D01D21D74A00064045 /* CompileMeasureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164CF1D21D74A00064045 /* CompileMeasureTests.swift */; }; + 2A3164D51D21D77500064045 /* NSData+GZIP.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164D41D21D77500064045 /* NSData+GZIP.m */; }; + 2A3164DA1D21D90100064045 /* NSData+GZIP.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A3164D41D21D77500064045 /* NSData+GZIP.m */; }; + 2A3698AA1D80A1AC002C5CDA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A3698A91D80A1AC002C5CDA /* Main.storyboard */; }; + 2A3698AC1D80A33B002C5CDA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3698AB1D80A33B002C5CDA /* ViewController.swift */; }; + 2A5404011D86D01700DBD44C /* BuildManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5404001D86D01700DBD44C /* BuildManager.swift */; }; + 2A5404031D86DE0C00DBD44C /* XcodeDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5404021D86DE0C00DBD44C /* XcodeDatabase.swift */; }; + 2A5404051D86F3C700DBD44C /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5404041D86F3C700DBD44C /* File.swift */; }; + 2A5B32C21D86C629000705E1 /* CacheFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5B32C11D86C629000705E1 /* CacheFile.swift */; }; + 2A9807DD1D7C71F900B9232C /* DirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9807DC1D7C71F900B9232C /* DirectoryMonitor.swift */; }; + 2A9807DF1D7C76FD00B9232C /* ProjectSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9807DE1D7C76FD00B9232C /* ProjectSelection.swift */; }; + 2ABFB6CE1D81F2DE00D060BF /* NSAlert+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ABFB6CD1D81F2DE00D060BF /* NSAlert+Extensions.swift */; }; + 2ACBFD0C1D8835E60009567E /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ACBFD0B1D8835E60009567E /* UserSettings.swift */; }; + 2AE775121D225D5D00D1A744 /* DerivedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE775111D225D5D00D1A744 /* DerivedDataManager.swift */; }; + 2AF821441D21D6B900D65186 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF821431D21D6B900D65186 /* AppDelegate.swift */; }; + 2AF821461D21D6B900D65186 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2AF821451D21D6B900D65186 /* Assets.xcassets */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 2A7DBBFA1CD8E705009E9B49 /* PBXContainerItemProxy */ = { + 2AF821501D21D6B900D65186 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 2A1682AB1CD3F73900014B7F /* Project object */; + containerPortal = 2AF821381D21D6B900D65186 /* Project object */; proxyType = 1; - remoteGlobalIDString = 2A1682B21CD3F73900014B7F; + remoteGlobalIDString = 2AF8213F1D21D6B900D65186; remoteInfo = BuildTimeAnalyzer; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 2A1682B31CD3F73900014B7F /* BuildTimeAnalyzer.xcplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BuildTimeAnalyzer.xcplugin; sourceTree = BUILT_PRODUCTS_DIR; }; - 2A1682B61CD3F73900014B7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 2A1682BD1CD3FB5D00014B7F /* BuildTimeAnalyzer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BuildTimeAnalyzer-Bridging-Header.h"; sourceTree = ""; }; - 2A1682C01CD3FBCC00014B7F /* CMPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CMPlugin.swift; sourceTree = ""; }; - 2A1682C21CD4039300014B7F /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; - 2A1682C41CD403FB00014B7F /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; - 2A1682C61CD4040900014B7F /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; - 2A1682C91CD4042800014B7F /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; - 2A1854A31CD8BB74005498BF /* CMBuildOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CMBuildOperation.swift; sourceTree = ""; }; - 2A1B1E7F1CD768C300ADD414 /* CMCompileMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CMCompileMeasure.swift; sourceTree = ""; }; - 2A5A07241CD64DCA009F0ECE /* CMProcessingState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CMProcessingState.swift; sourceTree = ""; }; - 2A69BFFA1CD513D700A554A3 /* NSData+GZIP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSData+GZIP.h"; path = "../GZIP/NSData+GZIP.h"; sourceTree = ""; }; - 2A69BFFB1CD513D700A554A3 /* NSData+GZIP.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSData+GZIP.m"; path = "../GZIP/NSData+GZIP.m"; sourceTree = ""; }; - 2A69BFFD1CD5179200A554A3 /* CMXcodeWorkspace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CMXcodeWorkspace.swift; sourceTree = ""; }; - 2A71846D1CDA483E00E8B7D3 /* NSNotificationCenter+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSNotificationCenter+Extensions.swift"; sourceTree = ""; }; - 2A74ACC11CD571470057B819 /* CMLogProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CMLogProcessor.swift; sourceTree = ""; }; - 2A74ACC41CD5733B0057B819 /* CMResultWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CMResultWindow.xib; sourceTree = ""; }; - 2A74ACCB1CD573CD0057B819 /* CMResultWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CMResultWindowController.swift; sourceTree = ""; }; - 2A7DBBF21CD8E5D0009E9B49 /* BuildTimeAnalyzer.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BuildTimeAnalyzer.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 2A7DBBF41CD8E5D1009E9B49 /* CMCompileMeasureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMCompileMeasureTests.swift; sourceTree = ""; }; - 2A7DBBF61CD8E5D1009E9B49 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = BuildTimeAnalyzerTests/Info.plist; sourceTree = ""; }; - 2A9BDFEA1CD95196000E5862 /* Icons.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Icons.xcassets; sourceTree = ""; }; - 2AE38C561CDA8222008B65AE /* CMRawMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CMRawMeasure.swift; sourceTree = ""; }; + 2A3164C01D21D73F00064045 /* CompileMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompileMeasure.swift; sourceTree = ""; }; + 2A3164C11D21D73F00064045 /* LogProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogProcessor.swift; sourceTree = ""; }; + 2A3164C31D21D73F00064045 /* ProcessingState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessingState.swift; sourceTree = ""; }; + 2A3164C41D21D73F00064045 /* RawMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawMeasure.swift; sourceTree = ""; }; + 2A3164CF1D21D74A00064045 /* CompileMeasureTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompileMeasureTests.swift; sourceTree = ""; }; + 2A3164D21D21D77500064045 /* BuildTimeAnalyzerTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BuildTimeAnalyzerTests-Bridging-Header.h"; sourceTree = SOURCE_ROOT; }; + 2A3164D31D21D77500064045 /* NSData+GZIP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSData+GZIP.h"; path = "GZIP/NSData+GZIP.h"; sourceTree = ""; }; + 2A3164D41D21D77500064045 /* NSData+GZIP.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSData+GZIP.m"; path = "GZIP/NSData+GZIP.m"; sourceTree = ""; }; + 2A3698A91D80A1AC002C5CDA /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; + 2A3698AB1D80A33B002C5CDA /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 2A5404001D86D01700DBD44C /* BuildManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildManager.swift; sourceTree = ""; }; + 2A5404021D86DE0C00DBD44C /* XcodeDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XcodeDatabase.swift; sourceTree = ""; }; + 2A5404041D86F3C700DBD44C /* File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; + 2A5B32C11D86C629000705E1 /* CacheFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheFile.swift; sourceTree = ""; }; + 2A9807DC1D7C71F900B9232C /* DirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryMonitor.swift; sourceTree = ""; }; + 2A9807DE1D7C76FD00B9232C /* ProjectSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectSelection.swift; sourceTree = ""; }; + 2ABFB6CD1D81F2DE00D060BF /* NSAlert+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAlert+Extensions.swift"; sourceTree = ""; }; + 2ACBFD0B1D8835E60009567E /* UserSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; + 2AE775111D225D5D00D1A744 /* DerivedDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DerivedDataManager.swift; sourceTree = ""; }; + 2AF821401D21D6B900D65186 /* BuildTimeAnalyzer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BuildTimeAnalyzer.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2AF821431D21D6B900D65186 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 2AF821451D21D6B900D65186 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = BuildTimeAnalyzer/Assets.xcassets; sourceTree = ""; }; + 2AF8214A1D21D6B900D65186 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2AF8214F1D21D6B900D65186 /* BuildTimeAnalyzerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BuildTimeAnalyzerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2AF821551D21D6B900D65186 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 2A1682B01CD3F73900014B7F /* Frameworks */ = { + 2AF8213D1D21D6B900D65186 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2A1682CA1CD4042800014B7F /* CoreData.framework in Frameworks */, - 2A1682C71CD4040900014B7F /* Foundation.framework in Frameworks */, - 2A1682C51CD403FB00014B7F /* AppKit.framework in Frameworks */, - 2A1682C31CD4039300014B7F /* Cocoa.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 2A7DBBEF1CD8E5D0009E9B49 /* Frameworks */ = { + 2AF8214C1D21D6B900D65186 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -85,111 +85,135 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 2A1682AA1CD3F73900014B7F = { + 2A3164D11D21D74F00064045 /* Supporting Files */ = { isa = PBXGroup; children = ( - 2A1682B51CD3F73900014B7F /* BuildTimeAnalyzer */, - 2A7DBBF31CD8E5D1009E9B49 /* BuildTimeAnalyzerTests */, - 2A1682C81CD4041200014B7F /* Frameworks */, - 2A74ACC31CD571A40057B819 /* GZIP */, - 2A1682B41CD3F73900014B7F /* Products */, + 2A3164D21D21D77500064045 /* BuildTimeAnalyzerTests-Bridging-Header.h */, + 2AF8214A1D21D6B900D65186 /* Info.plist */, ); + name = "Supporting Files"; sourceTree = ""; }; - 2A1682B41CD3F73900014B7F /* Products */ = { + 2A3164D61D21D77E00064045 /* GZIP */ = { isa = PBXGroup; children = ( - 2A1682B31CD3F73900014B7F /* BuildTimeAnalyzer.xcplugin */, - 2A7DBBF21CD8E5D0009E9B49 /* BuildTimeAnalyzer.xctest */, + 2A3164D31D21D77500064045 /* NSData+GZIP.h */, + 2A3164D41D21D77500064045 /* NSData+GZIP.m */, ); - name = Products; + name = GZIP; sourceTree = ""; }; - 2A1682B51CD3F73900014B7F /* BuildTimeAnalyzer */ = { + 2A3164D71D21D7A800064045 /* Supporting Files */ = { isa = PBXGroup; children = ( - 2A1854A31CD8BB74005498BF /* CMBuildOperation.swift */, - 2A1B1E7F1CD768C300ADD414 /* CMCompileMeasure.swift */, - 2A74ACC11CD571470057B819 /* CMLogProcessor.swift */, - 2A1682C01CD3FBCC00014B7F /* CMPlugin.swift */, - 2A5A07241CD64DCA009F0ECE /* CMProcessingState.swift */, - 2AE38C561CDA8222008B65AE /* CMRawMeasure.swift */, - 2A74ACC41CD5733B0057B819 /* CMResultWindow.xib */, - 2A74ACCB1CD573CD0057B819 /* CMResultWindowController.swift */, - 2A69BFFD1CD5179200A554A3 /* CMXcodeWorkspace.swift */, - 2A9BDFEA1CD95196000E5862 /* Icons.xcassets */, - 2A1682BC1CD3FAA800014B7F /* Supporting Files */, - 2AC835061CD664B200314A5A /* System Extensions */, + 2AF821551D21D6B900D65186 /* Info.plist */, ); - path = BuildTimeAnalyzer; + name = "Supporting Files"; sourceTree = ""; }; - 2A1682BC1CD3FAA800014B7F /* Supporting Files */ = { + 2ABFB6CF1D81F34300D060BF /* Extensions */ = { isa = PBXGroup; children = ( - 2A1682BD1CD3FB5D00014B7F /* BuildTimeAnalyzer-Bridging-Header.h */, - 2A1682B61CD3F73900014B7F /* Info.plist */, + 2ABFB6CD1D81F2DE00D060BF /* NSAlert+Extensions.swift */, ); - name = "Supporting Files"; + name = Extensions; sourceTree = ""; }; - 2A1682C81CD4041200014B7F /* Frameworks */ = { + 2ABFB6D01D81F35400D060BF /* Application */ = { isa = PBXGroup; children = ( - 2A1682C41CD403FB00014B7F /* AppKit.framework */, - 2A1682C91CD4042800014B7F /* CoreData.framework */, - 2A1682C61CD4040900014B7F /* Foundation.framework */, - 2A1682C21CD4039300014B7F /* Cocoa.framework */, + 2AF821431D21D6B900D65186 /* AppDelegate.swift */, + 2A3698A91D80A1AC002C5CDA /* Main.storyboard */, ); - name = Frameworks; + name = Application; sourceTree = ""; }; - 2A74ACC31CD571A40057B819 /* GZIP */ = { + 2ABFB6D11D81F37300D060BF /* Models */ = { isa = PBXGroup; children = ( - 2A69BFFA1CD513D700A554A3 /* NSData+GZIP.h */, - 2A69BFFB1CD513D700A554A3 /* NSData+GZIP.m */, + 2A5B32C11D86C629000705E1 /* CacheFile.swift */, + 2A3164C01D21D73F00064045 /* CompileMeasure.swift */, + 2A5404041D86F3C700DBD44C /* File.swift */, + 2A3164C31D21D73F00064045 /* ProcessingState.swift */, + 2A3164C41D21D73F00064045 /* RawMeasure.swift */, + 2A5404021D86DE0C00DBD44C /* XcodeDatabase.swift */, ); - name = GZIP; - path = BuildTimeAnalyzer; + name = Models; sourceTree = ""; }; - 2A7DBBF31CD8E5D1009E9B49 /* BuildTimeAnalyzerTests */ = { + 2ABFB6D21D81F81400D060BF /* ViewControllers */ = { isa = PBXGroup; children = ( - 2A7DBBF41CD8E5D1009E9B49 /* CMCompileMeasureTests.swift */, - 2A7DBBFD1CD91D8F009E9B49 /* Supporting Files */, + 2A3698AB1D80A33B002C5CDA /* ViewController.swift */, ); - path = BuildTimeAnalyzerTests; + name = ViewControllers; sourceTree = ""; }; - 2A7DBBFD1CD91D8F009E9B49 /* Supporting Files */ = { + 2ABFB6D31D81F82600D060BF /* Utilities */ = { isa = PBXGroup; children = ( - 2A7DBBF61CD8E5D1009E9B49 /* Info.plist */, + 2A5404001D86D01700DBD44C /* BuildManager.swift */, + 2AE775111D225D5D00D1A744 /* DerivedDataManager.swift */, + 2A9807DC1D7C71F900B9232C /* DirectoryMonitor.swift */, + 2A3164C11D21D73F00064045 /* LogProcessor.swift */, + 2A9807DE1D7C76FD00B9232C /* ProjectSelection.swift */, + 2ACBFD0B1D8835E60009567E /* UserSettings.swift */, ); - name = "Supporting Files"; - path = "BuildTimeAnalyzerTests/Supporting Files"; + name = Utilities; + sourceTree = ""; + }; + 2AF821371D21D6B900D65186 = { + isa = PBXGroup; + children = ( + 2AF821421D21D6B900D65186 /* BuildTimeAnalyzer */, + 2AF821451D21D6B900D65186 /* Assets.xcassets */, + 2AF821521D21D6B900D65186 /* BuildTimeAnalyzerTests */, + 2A3164D61D21D77E00064045 /* GZIP */, + 2AF821411D21D6B900D65186 /* Products */, + ); + sourceTree = ""; + }; + 2AF821411D21D6B900D65186 /* Products */ = { + isa = PBXGroup; + children = ( + 2AF821401D21D6B900D65186 /* BuildTimeAnalyzer.app */, + 2AF8214F1D21D6B900D65186 /* BuildTimeAnalyzerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 2AF821421D21D6B900D65186 /* BuildTimeAnalyzer */ = { + isa = PBXGroup; + children = ( + 2ABFB6D01D81F35400D060BF /* Application */, + 2ABFB6CF1D81F34300D060BF /* Extensions */, + 2ABFB6D11D81F37300D060BF /* Models */, + 2A3164D11D21D74F00064045 /* Supporting Files */, + 2ABFB6D31D81F82600D060BF /* Utilities */, + 2ABFB6D21D81F81400D060BF /* ViewControllers */, + ); + path = BuildTimeAnalyzer; sourceTree = ""; }; - 2AC835061CD664B200314A5A /* System Extensions */ = { + 2AF821521D21D6B900D65186 /* BuildTimeAnalyzerTests */ = { isa = PBXGroup; children = ( - 2A71846D1CDA483E00E8B7D3 /* NSNotificationCenter+Extensions.swift */, + 2A3164CF1D21D74A00064045 /* CompileMeasureTests.swift */, + 2A3164D71D21D7A800064045 /* Supporting Files */, ); - name = "System Extensions"; + path = BuildTimeAnalyzerTests; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 2A1682B21CD3F73900014B7F /* BuildTimeAnalyzer */ = { + 2AF8213F1D21D6B900D65186 /* BuildTimeAnalyzer */ = { isa = PBXNativeTarget; - buildConfigurationList = 2A1682B91CD3F73900014B7F /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzer" */; + buildConfigurationList = 2AF821581D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzer" */; buildPhases = ( - 2A1682AF1CD3F73900014B7F /* Sources */, - 2A1682B01CD3F73900014B7F /* Frameworks */, - 2A1682B11CD3F73900014B7F /* Resources */, + 2AF8213C1D21D6B900D65186 /* Sources */, + 2AF8213D1D21D6B900D65186 /* Frameworks */, + 2AF8213E1D21D6B900D65186 /* Resources */, ); buildRules = ( ); @@ -197,74 +221,78 @@ ); name = BuildTimeAnalyzer; productName = BuildTimeAnalyzer; - productReference = 2A1682B31CD3F73900014B7F /* BuildTimeAnalyzer.xcplugin */; - productType = "com.apple.product-type.bundle"; + productReference = 2AF821401D21D6B900D65186 /* BuildTimeAnalyzer.app */; + productType = "com.apple.product-type.application"; }; - 2A7DBBF11CD8E5D0009E9B49 /* BuildTimeAnalyzerTests */ = { + 2AF8214E1D21D6B900D65186 /* BuildTimeAnalyzerTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 2A7DBBF91CD8E5D1009E9B49 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzerTests" */; + buildConfigurationList = 2AF8215B1D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzerTests" */; buildPhases = ( - 2A7DBBEE1CD8E5D0009E9B49 /* Sources */, - 2A7DBBEF1CD8E5D0009E9B49 /* Frameworks */, - 2A7DBBF01CD8E5D0009E9B49 /* Resources */, + 2AF8214B1D21D6B900D65186 /* Sources */, + 2AF8214C1D21D6B900D65186 /* Frameworks */, + 2AF8214D1D21D6B900D65186 /* Resources */, ); buildRules = ( ); dependencies = ( - 2A7DBBFB1CD8E705009E9B49 /* PBXTargetDependency */, + 2AF821511D21D6B900D65186 /* PBXTargetDependency */, ); name = BuildTimeAnalyzerTests; productName = BuildTimeAnalyzerTests; - productReference = 2A7DBBF21CD8E5D0009E9B49 /* BuildTimeAnalyzer.xctest */; + productReference = 2AF8214F1D21D6B900D65186 /* BuildTimeAnalyzerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 2A1682AB1CD3F73900014B7F /* Project object */ = { + 2AF821381D21D6B900D65186 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0730; - ORGANIZATIONNAME = "Robert Gummesson"; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Cane Media Ltd"; TargetAttributes = { - 2A1682B21CD3F73900014B7F = { - CreatedOnToolsVersion = 7.3; + 2AF8213F1D21D6B900D65186 = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0800; }; - 2A7DBBF11CD8E5D0009E9B49 = { - CreatedOnToolsVersion = 7.3; + 2AF8214E1D21D6B900D65186 = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0800; + TestTargetID = 2AF8213F1D21D6B900D65186; }; }; }; - buildConfigurationList = 2A1682AE1CD3F73900014B7F /* Build configuration list for PBXProject "BuildTimeAnalyzer" */; + buildConfigurationList = 2AF8213B1D21D6B900D65186 /* Build configuration list for PBXProject "BuildTimeAnalyzer" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); - mainGroup = 2A1682AA1CD3F73900014B7F; - productRefGroup = 2A1682B41CD3F73900014B7F /* Products */; + mainGroup = 2AF821371D21D6B900D65186; + productRefGroup = 2AF821411D21D6B900D65186 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 2A1682B21CD3F73900014B7F /* BuildTimeAnalyzer */, - 2A7DBBF11CD8E5D0009E9B49 /* BuildTimeAnalyzerTests */, + 2AF8213F1D21D6B900D65186 /* BuildTimeAnalyzer */, + 2AF8214E1D21D6B900D65186 /* BuildTimeAnalyzerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 2A1682B11CD3F73900014B7F /* Resources */ = { + 2AF8213E1D21D6B900D65186 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2A9BDFEB1CD95196000E5862 /* Icons.xcassets in Resources */, - 2A74ACC51CD5733B0057B819 /* CMResultWindow.xib in Resources */, + 2A3698AA1D80A1AC002C5CDA /* Main.storyboard in Resources */, + 2AF821461D21D6B900D65186 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 2A7DBBF01CD8E5D0009E9B49 /* Resources */ = { + 2AF8214D1D21D6B900D65186 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -274,44 +302,50 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 2A1682AF1CD3F73900014B7F /* Sources */ = { + 2AF8213C1D21D6B900D65186 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2ABB72011CDA4680001FDC32 /* CMBuildOperation.swift in Sources */, - 2ABB72021CDA4680001FDC32 /* CMCompileMeasure.swift in Sources */, - 2ABB72031CDA4680001FDC32 /* CMPlugin.swift in Sources */, - 2ABB72041CDA4680001FDC32 /* CMLogProcessor.swift in Sources */, - 2A71846E1CDA483E00E8B7D3 /* NSNotificationCenter+Extensions.swift in Sources */, - 2ABB72051CDA4680001FDC32 /* CMProcessingState.swift in Sources */, - 2ABB72061CDA4680001FDC32 /* CMResultWindowController.swift in Sources */, - 2A14D30B1CDA4F380079BFF0 /* NSData+GZIP.m in Sources */, - 2ABB72071CDA4680001FDC32 /* CMXcodeWorkspace.swift in Sources */, - 2AE38C571CDA8222008B65AE /* CMRawMeasure.swift in Sources */, + 2A9807DD1D7C71F900B9232C /* DirectoryMonitor.swift in Sources */, + 2A3164DA1D21D90100064045 /* NSData+GZIP.m in Sources */, + 2A5B32C21D86C629000705E1 /* CacheFile.swift in Sources */, + 2A3164C91D21D73F00064045 /* LogProcessor.swift in Sources */, + 2A5404011D86D01700DBD44C /* BuildManager.swift in Sources */, + 2A5404051D86F3C700DBD44C /* File.swift in Sources */, + 2ABFB6CE1D81F2DE00D060BF /* NSAlert+Extensions.swift in Sources */, + 2A5404031D86DE0C00DBD44C /* XcodeDatabase.swift in Sources */, + 2A3164CB1D21D73F00064045 /* ProcessingState.swift in Sources */, + 2AF821441D21D6B900D65186 /* AppDelegate.swift in Sources */, + 2AE775121D225D5D00D1A744 /* DerivedDataManager.swift in Sources */, + 2A3698AC1D80A33B002C5CDA /* ViewController.swift in Sources */, + 2A3164CC1D21D73F00064045 /* RawMeasure.swift in Sources */, + 2ACBFD0C1D8835E60009567E /* UserSettings.swift in Sources */, + 2A3164C81D21D73F00064045 /* CompileMeasure.swift in Sources */, + 2A9807DF1D7C76FD00B9232C /* ProjectSelection.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 2A7DBBEE1CD8E5D0009E9B49 /* Sources */ = { + 2AF8214B1D21D6B900D65186 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2A71846F1CDA48AB00E8B7D3 /* CMCompileMeasure.swift in Sources */, - 2A7DBBF51CD8E5D1009E9B49 /* CMCompileMeasureTests.swift in Sources */, + 2A3164D51D21D77500064045 /* NSData+GZIP.m in Sources */, + 2A3164D01D21D74A00064045 /* CompileMeasureTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 2A7DBBFB1CD8E705009E9B49 /* PBXTargetDependency */ = { + 2AF821511D21D6B900D65186 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 2A1682B21CD3F73900014B7F /* BuildTimeAnalyzer */; - targetProxy = 2A7DBBFA1CD8E705009E9B49 /* PBXContainerItemProxy */; + target = 2AF8213F1D21D6B900D65186 /* BuildTimeAnalyzer */; + targetProxy = 2AF821501D21D6B900D65186 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 2A1682B71CD3F73900014B7F /* Debug */ = { + 2AF821561D21D6B900D65186 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -325,14 +359,15 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - DEPLOYMENT_LOCATION = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -349,16 +384,15 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Application Support/Developer/Shared/Xcode/Plug-ins"; - MACH_O_TYPE = mh_bundle; MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; - 2A1682B81CD3F73900014B7F /* Release */ = { + 2AF821571D21D6B900D65186 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -372,14 +406,15 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEPLOYMENT_LOCATION = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -390,110 +425,105 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Application Support/Developer/Shared/Xcode/Plug-ins"; - MACH_O_TYPE = mh_bundle; MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; }; name = Release; }; - 2A1682BA1CD3F73900014B7F /* Debug */ = { + 2AF821591D21D6B900D65186 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COMBINE_HIDPI_IMAGES = YES; - DSTROOT = "$(HOME)"; - EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; INFOPLIST_FILE = BuildTimeAnalyzer/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Application Support/Developer/Shared/Xcode/Plug-ins"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzer; - PRODUCT_NAME = BuildTimeAnalyzer; - SKIP_INSTALL = NO; - SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzer/BuildTimeAnalyzer-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - WRAPPER_EXTENSION = xcplugin; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; + SWIFT_VERSION = 3.0; }; name = Debug; }; - 2A1682BB1CD3F73900014B7F /* Release */ = { + 2AF8215A1D21D6B900D65186 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COMBINE_HIDPI_IMAGES = YES; - COPY_PHASE_STRIP = YES; - DSTROOT = "$(HOME)"; - EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; INFOPLIST_FILE = BuildTimeAnalyzer/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Application Support/Developer/Shared/Xcode/Plug-ins"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzer; - PRODUCT_NAME = BuildTimeAnalyzer; - SKIP_INSTALL = NO; - SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzer/BuildTimeAnalyzer-Bridging-Header.h"; - WRAPPER_EXTENSION = xcplugin; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; + SWIFT_VERSION = 3.0; }; name = Release; }; - 2A7DBBF71CD8E5D1009E9B49 /* Debug */ = { + 2AF8215C1D21D6B900D65186 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = BuildTimeAnalyzerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzerTests; - PRODUCT_NAME = BuildTimeAnalyzer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BuildTimeAnalyzer.app/Contents/MacOS/BuildTimeAnalyzer"; }; name = Debug; }; - 2A7DBBF81CD8E5D1009E9B49 /* Release */ = { + 2AF8215D1D21D6B900D65186 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = BuildTimeAnalyzerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = uk.co.canemedia.BuildTimeAnalyzerTests; - PRODUCT_NAME = BuildTimeAnalyzer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "BuildTimeAnalyzerTests-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BuildTimeAnalyzer.app/Contents/MacOS/BuildTimeAnalyzer"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 2A1682AE1CD3F73900014B7F /* Build configuration list for PBXProject "BuildTimeAnalyzer" */ = { + 2AF8213B1D21D6B900D65186 /* Build configuration list for PBXProject "BuildTimeAnalyzer" */ = { isa = XCConfigurationList; buildConfigurations = ( - 2A1682B71CD3F73900014B7F /* Debug */, - 2A1682B81CD3F73900014B7F /* Release */, + 2AF821561D21D6B900D65186 /* Debug */, + 2AF821571D21D6B900D65186 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 2A1682B91CD3F73900014B7F /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzer" */ = { + 2AF821581D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzer" */ = { isa = XCConfigurationList; buildConfigurations = ( - 2A1682BA1CD3F73900014B7F /* Debug */, - 2A1682BB1CD3F73900014B7F /* Release */, + 2AF821591D21D6B900D65186 /* Debug */, + 2AF8215A1D21D6B900D65186 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 2A7DBBF91CD8E5D1009E9B49 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzerTests" */ = { + 2AF8215B1D21D6B900D65186 /* Build configuration list for PBXNativeTarget "BuildTimeAnalyzerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 2A7DBBF71CD8E5D1009E9B49 /* Debug */, - 2A7DBBF81CD8E5D1009E9B49 /* Release */, + 2AF8215C1D21D6B900D65186 /* Debug */, + 2AF8215D1D21D6B900D65186 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; - rootObject = 2A1682AB1CD3F73900014B7F /* Project object */; + rootObject = 2AF821381D21D6B900D65186 /* Project object */; } diff --git a/BuildTimeAnalyzer.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/BuildTimeAnalyzer.xcodeproj/project.xcworkspace/contents.xcworkspacedata index b48bd57..3637b15 100644 --- a/BuildTimeAnalyzer.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/BuildTimeAnalyzer.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:BuildTimeAnalyzer.xcodeproj"> diff --git a/BuildTimeAnalyzer/AppDelegate.swift b/BuildTimeAnalyzer/AppDelegate.swift new file mode 100644 index 0000000..8970713 --- /dev/null +++ b/BuildTimeAnalyzer/AppDelegate.swift @@ -0,0 +1,58 @@ +// +// AppDelegate.swift +// BuildTimeAnalyzer +// + +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + + @IBOutlet weak var projectSelectionMenuItem: NSMenuItem! + @IBOutlet weak var buildTimesMenuItem: NSMenuItem! + @IBOutlet weak var alwaysInFrontMenuItem: NSMenuItem! + + func applicationDidFinishLaunching(_ notification: Notification) { + alwaysInFrontMenuItem.state = UserSettings.windowShouldBeTopMost ? NSOnState : NSOffState + } + + var viewController: ViewController? { + return NSApplication.shared().mainWindow?.contentViewController as? ViewController + } + + func configureMenuItems(showBuildTimesMenuItem: Bool) { + projectSelectionMenuItem.isEnabled = !showBuildTimesMenuItem + buildTimesMenuItem.isEnabled = showBuildTimesMenuItem + } + + // MARK: Actions + + @IBAction func navigateToProjectSelection(_ sender: NSMenuItem) { + configureMenuItems(showBuildTimesMenuItem: true) + + viewController?.cancelProcessing() + viewController?.showInstructions(true) + } + + @IBAction func navigateToBuildTimes(_ sender: NSMenuItem) { + configureMenuItems(showBuildTimesMenuItem: false) + viewController?.showInstructions(false) + } + + @IBAction func visitGitHubPage(_ sender: AnyObject) { + let path = "https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode" + if let url = URL(string: path) { + NSWorkspace.shared().open(url) + } + } + + @IBAction func toggleAlwaysInFront(_ sender: NSMenuItem) { + let alwaysInFront = sender.state == NSOffState + + sender.state = alwaysInFront ? NSOnState : NSOffState + UserSettings.windowShouldBeTopMost = alwaysInFront + + viewController?.makeWindowTopMost(topMost: alwaysInFront) + } +} + diff --git a/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/Contents.json b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..97d3851 --- /dev/null +++ b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "logo16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "logo32-1.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "logo32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "logo64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "logo128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "logo256-1.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "logo256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "logo512-1.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "logo512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "logo512@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo128.png b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo128.png new file mode 100644 index 0000000..7fa299c Binary files /dev/null and b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo128.png differ diff --git a/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo16.png b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo16.png new file mode 100644 index 0000000..833e886 Binary files /dev/null and b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo16.png differ diff --git a/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo256-1.png b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo256-1.png new file mode 100644 index 0000000..29834a2 Binary files /dev/null and b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo256-1.png differ diff --git a/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo256.png b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo256.png new file mode 100644 index 0000000..29834a2 Binary files /dev/null and b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo256.png differ diff --git a/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo32-1.png b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo32-1.png new file mode 100644 index 0000000..07b69cd Binary files /dev/null and b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo32-1.png differ diff --git a/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo32.png b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo32.png new file mode 100644 index 0000000..07b69cd Binary files /dev/null and b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo32.png differ diff --git a/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512-1.png b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512-1.png new file mode 100644 index 0000000..d934bb9 Binary files /dev/null and b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512-1.png differ diff --git a/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512.png b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512.png new file mode 100644 index 0000000..d934bb9 Binary files /dev/null and b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512.png differ diff --git a/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512@2x.png b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512@2x.png new file mode 100644 index 0000000..bca7380 Binary files /dev/null and b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo512@2x.png differ diff --git a/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo64.png b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo64.png new file mode 100644 index 0000000..dfe37c6 Binary files /dev/null and b/BuildTimeAnalyzer/Assets.xcassets/AppIcon.appiconset/logo64.png differ diff --git a/BuildTimeAnalyzer/Icons.xcassets/Contents.json b/BuildTimeAnalyzer/Assets.xcassets/Contents.json similarity index 100% rename from BuildTimeAnalyzer/Icons.xcassets/Contents.json rename to BuildTimeAnalyzer/Assets.xcassets/Contents.json diff --git a/BuildTimeAnalyzer/Icons.xcassets/ScreenShot.imageset/Contents.json b/BuildTimeAnalyzer/Assets.xcassets/ScreenShot.imageset/Contents.json similarity index 100% rename from BuildTimeAnalyzer/Icons.xcassets/ScreenShot.imageset/Contents.json rename to BuildTimeAnalyzer/Assets.xcassets/ScreenShot.imageset/Contents.json diff --git a/BuildTimeAnalyzer/Icons.xcassets/ScreenShot.imageset/Screen Shot.png b/BuildTimeAnalyzer/Assets.xcassets/ScreenShot.imageset/Screen Shot.png similarity index 100% rename from BuildTimeAnalyzer/Icons.xcassets/ScreenShot.imageset/Screen Shot.png rename to BuildTimeAnalyzer/Assets.xcassets/ScreenShot.imageset/Screen Shot.png diff --git a/BuildTimeAnalyzer/BuildManager.swift b/BuildTimeAnalyzer/BuildManager.swift new file mode 100644 index 0000000..8b8470f --- /dev/null +++ b/BuildTimeAnalyzer/BuildManager.swift @@ -0,0 +1,77 @@ +// +// BuildManager.swift +// BuildTimeAnalyzer +// + +import Cocoa + +protocol BuildManagerDelegate: class { + func derivedDataDidChange() + func buildManager(_ buildManager: BuildManager, shouldParseLogWithDatabase database: XcodeDatabase) +} + +class BuildManager: NSObject { + + weak var delegate: BuildManagerDelegate? + + private let derivedDataDirectoryMonitor = DirectoryMonitor(isDerivedData: true) + private let logFolderDirectoryMonitor = DirectoryMonitor(isDerivedData: false) + + private var currentDataBase: XcodeDatabase? + + override func awakeFromNib() { + super.awakeFromNib() + + derivedDataDirectoryMonitor.delegate = self + logFolderDirectoryMonitor.delegate = self + + startMonitoring() + } + + func startMonitoring() { + stopMonitoring() + derivedDataDirectoryMonitor.startMonitoring(path: UserSettings.derivedDataLocation) + } + + func stopMonitoring() { + derivedDataDirectoryMonitor.stopMonitoring() + } + + func database(forFolder URL: URL) -> XcodeDatabase? { + let databaseURL = URL.appendingPathComponent("Cache.db") + return XcodeDatabase(fromPath: databaseURL.path) + } + + func processDerivedData() { + guard let mostRecent = DerivedDataManager.derivedData().first else { return } + + let logFolder = mostRecent.url.appendingPathComponent("Logs/Build").path + guard logFolderDirectoryMonitor.path != logFolder else { return } + + logFolderDirectoryMonitor.stopMonitoring() + logFolderDirectoryMonitor.startMonitoring(path: logFolder) + } + + func processLogFolder(with url: URL) { + guard let activeDatabase = database(forFolder: url), + activeDatabase.isBuildType, + activeDatabase != currentDataBase else { return } + + currentDataBase = activeDatabase + delegate?.buildManager(self, shouldParseLogWithDatabase: activeDatabase) + } +} + +extension BuildManager: DirectoryMonitorDelegate { + func directoryMonitorDidObserveChange(_ directoryMonitor: DirectoryMonitor, isDerivedData: Bool) { + if isDerivedData { + delegate?.derivedDataDidChange() + processDerivedData() + } else if let path = directoryMonitor.path { + // TODO: If we don't dispatch, it seems it fires off too soon + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.processLogFolder(with: URL(fileURLWithPath: path)) + } + } + } +} diff --git a/BuildTimeAnalyzer/CMBuildOperation.swift b/BuildTimeAnalyzer/CMBuildOperation.swift deleted file mode 100644 index 69b22fb..0000000 --- a/BuildTimeAnalyzer/CMBuildOperation.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// CMBuildOperation.swift -// BuildTimeAnalyzer -// -// Created by Robert Gummesson on 03/05/2016. -// Copyright © 2016 Robert Gummesson. All rights reserved. -// - -import Foundation - -enum CMBuildResult : Int { - case success = 1 - case failed = 2 - case cancelled = 3 -} - -struct CMBuildOperation { - - var actionName: String - var productName: String - var duration: Double - var result: CMBuildResult - var startTime: NSDate - - var endTime: NSDate { - // We will be looking for log files created after this date - // Let's subtract a second to be on the safe side - return startTime.dateByAddingTimeInterval(duration - 1) - } -} \ No newline at end of file diff --git a/BuildTimeAnalyzer/CMLogProcessor.swift b/BuildTimeAnalyzer/CMLogProcessor.swift deleted file mode 100644 index 14c7999..0000000 --- a/BuildTimeAnalyzer/CMLogProcessor.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// CMLogProcessor.swift -// BuildTimeAnalyzer -// -// Created by Robert Gummesson on 01/05/2016. -// Copyright © 2016 Robert Gummesson. All rights reserved. -// - -typealias CMUpdateClosure = (result: [CMCompileMeasure], didComplete: Bool) -> () - -protocol CMLogProcessorProtocol: class { - var rawMeasures: [String: CMRawMeasure] { get set } - var updateHandler: CMUpdateClosure? { get set } - var workspace: CMXcodeWorkSpace? { get set } - var shouldCancel: Bool { get set } - - func processingDidStart() - func processingDidFinish() -} - -extension CMLogProcessorProtocol { - func process(productName: String, buildCompletionDate: NSDate?, updateHandler: CMUpdateClosure?) { - workspace = CMXcodeWorkSpace(productName: productName, buildCompletionDate: buildCompletionDate) - workspace?.logTextForProduct() { [weak self] (text) in - guard let text = text else { - updateHandler?(result: [], didComplete: true) - return - } - - self?.updateHandler = updateHandler - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { - self?.process(text: text) - } - } - } - - // MARK: Private methods - - private func process(text text: String) { - let characterSet = NSCharacterSet(charactersInString:"\r\"") - var remainingRange = text.startIndex.. 10 }) - if filteredResults.count < 20 { - filteredResults = rawMeasures.values.filter({ $0.time > 0.1 }) - } - - let sortedResults = filteredResults.sort({ $0.time > $1.time }) - updateHandler?(result: processResult(sortedResults), didComplete: didComplete) - - if didComplete { - rawMeasures.removeAll() - } - } - - private func processResult(unprocessedResult: [CMRawMeasure]) -> [CMCompileMeasure] { - var result: [CMCompileMeasure] = [] - for entry in unprocessedResult { - let code = entry.text.characters.split("\t").map(String.init) - if code.count >= 2, let measure = CMCompileMeasure(time: entry.time, rawPath: code[0], code: trimPrefixes(code[1]), references: entry.references) { - result.append(measure) - } - } - return result - } - - private func trimPrefixes(code: String) -> String { - var code = code - ["@objc ", "final ", "@IBAction "].forEach { (prefix) in - if code.hasPrefix(prefix) { - code = code.substringFromIndex(code.startIndex.advancedBy(prefix.characters.count)) - } - } - return code - } -} - -class CMLogProcessor: NSObject, CMLogProcessorProtocol { - - var rawMeasures: [String: CMRawMeasure] = [:] - var updateHandler: CMUpdateClosure? - var workspace: CMXcodeWorkSpace? - var shouldCancel = false - var timer: NSTimer? - - func processingDidStart() { - dispatch_async(dispatch_get_main_queue()) { - self.timer = NSTimer.scheduledTimerWithTimeInterval(1.5, target: self, selector: #selector(self.timerCallback(_:)), userInfo: nil, repeats: true) - } - } - - func processingDidFinish() { - dispatch_async(dispatch_get_main_queue()) { - self.timer?.invalidate() - self.timer = nil - self.shouldCancel = false - self.updateResults(true) - } - } - - func timerCallback(timer: NSTimer) { - updateResults(false) - } -} \ No newline at end of file diff --git a/BuildTimeAnalyzer/CMPlugin.swift b/BuildTimeAnalyzer/CMPlugin.swift deleted file mode 100644 index 4ad85ea..0000000 --- a/BuildTimeAnalyzer/CMPlugin.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// CMPlugin.swift -// CMPlugin -// -// Created by Robert Gummesson on 29/04/2016. -// Copyright © 2016 Robert Gummesson. All rights reserved. -// - -import AppKit - -class CMPlugin: NSObject { - - static var sharedPlugin: CMPlugin? - var windowController: CMResultWindowController? - - var applicationDidFinishLaunchingObserver: AnyObject? - var startStateDescription = "" - - class func pluginDidLoad(bundle: NSBundle) { - let appName = NSBundle.mainBundle().infoDictionary?["CFBundleName"] as? String - if appName == "Xcode" && sharedPlugin == nil { - sharedPlugin = CMPlugin() - } - } - - override init() { - super.init() - - applicationDidFinishLaunchingObserver = NSNotificationCenter.addObserverForName(NSApplicationDidFinishLaunchingNotification, usingBlock: { [unowned self] _ in - self.createMenuItem() - }) - } - - deinit { - NSNotificationCenter.removeObserver(applicationDidFinishLaunchingObserver, name: NSApplicationDidFinishLaunchingNotification) - } - - func createMenuItem() { - guard let submenu = NSApp.mainMenu?.itemWithTitle("View")?.submenu else { return } - - let title = NSLocalizedString("Build Time Analyzer", comment: "") - let menuItem = NSMenuItem(title: title, action: #selector(showWindow), keyEquivalent: "") - menuItem.target = self - menuItem.keyEquivalent = "b" - menuItem.keyEquivalentModifierMask = Int(NSEventModifierFlags.ShiftKeyMask.rawValue | NSEventModifierFlags.ControlKeyMask.rawValue) - submenu.addItem(menuItem) - } - - func showWindow() { - guard windowController == nil else { - windowController?.resultWindow.close() - return - } - - windowController = CMResultWindowController(windowNibName: "CMResultWindow") - windowController?.show() - - if let window = windowController?.resultWindow { - NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(windowWillClose(_:)), name: NSWindowWillCloseNotification, object: window) - } - } - - func windowWillClose(notification: NSNotification) { - NSNotificationCenter.defaultCenter().removeObserver(self, name: NSWindowWillCloseNotification, object: notification.object as? NSWindow) - windowController = nil - } -} \ No newline at end of file diff --git a/BuildTimeAnalyzer/CMProcessingState.swift b/BuildTimeAnalyzer/CMProcessingState.swift deleted file mode 100644 index 6bcbff7..0000000 --- a/BuildTimeAnalyzer/CMProcessingState.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// CMProcessingState.swift -// BuildTimeAnalyzer -// -// Created by Robert Gummesson on 01/05/2016. -// Copyright © 2016 Robert Gummesson. All rights reserved. -// - -enum CMProcessingState { - case processing - case waiting(shouldIndicate: Bool) - case completed(stateName: String) - - static let cancelledString = "Cancelled" - static let completedString = "Completed" - static let failedString = "No valid logs found" - static let processingString = "Processing log..." - static let waitingForBuildString = "Waiting..." - static let buildString = "Building..." -} - -extension CMProcessingState : Equatable {} - -func ==(lhs: CMProcessingState, rhs: CMProcessingState) -> Bool { - switch (lhs, rhs) { - case (let .waiting(shouldIndicate1), let .waiting(shouldIndicate2)): - return shouldIndicate1 == shouldIndicate2 - - case (let .completed(stateName1), let .completed(stateName2)): - return stateName1 == stateName2 - - case (.processing, .processing): - return true - - default: - return false - } -} \ No newline at end of file diff --git a/BuildTimeAnalyzer/CMResultWindow.xib b/BuildTimeAnalyzer/CMResultWindow.xib deleted file mode 100755 index 79389ff..0000000 --- a/BuildTimeAnalyzer/CMResultWindow.xib +++ /dev/null @@ -1,400 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/BuildTimeAnalyzer/CMResultWindowController.swift b/BuildTimeAnalyzer/CMResultWindowController.swift deleted file mode 100755 index 7e21ec8..0000000 --- a/BuildTimeAnalyzer/CMResultWindowController.swift +++ /dev/null @@ -1,249 +0,0 @@ -// -// CMResultWindowController.swift -// BuildTimeAnalyzer -// -// Created by Robert Gummesson on 01/05/2016. -// Copyright © 2016 Robert Gummesson. All rights reserved. -// - -import Cocoa - -class CMResultWindowController: NSWindowController { - - let IDEBuildOperationWillStartNotification = "IDEBuildOperationWillStartNotification" - let IDEBuildOperationDidGenerateOutputFilesNotification = "IDEBuildOperationDidGenerateOutputFilesNotification" - - @IBOutlet weak var tableView: NSTableView! - @IBOutlet weak var instructionsView: NSView! - @IBOutlet weak var statusTextField: NSTextField! - @IBOutlet weak var progressIndicator: NSProgressIndicator! - @IBOutlet weak var resultWindow: NSWindow! - @IBOutlet weak var tableViewContainerView: NSScrollView! - @IBOutlet weak var buildDurationTextField: NSTextField! - @IBOutlet weak var cancelButton: NSButton! - @IBOutlet weak var searchField: NSSearchField! - @IBOutlet weak var perFileCheckbox: NSButton! - - var dataSource: [CMCompileMeasure] = [] - - var perFunctionTimes: [CMCompileMeasure] = [] - var perFileTimes: [CMCompileMeasure] = [] - var filteredData: [CMCompileMeasure]? = nil - - var processor: CMLogProcessor = CMLogProcessor() - - var buildOperationWillStartObserver: AnyObject? - var buildOperationDidGenerateOutputFilesObserver: AnyObject? - - var processingState: CMProcessingState = .completed(stateName: CMProcessingState.completedString) { - didSet { - updateViewForState() - } - } - - override func windowDidLoad() { - super.windowDidLoad() - statusTextField.stringValue = CMProcessingState.waitingForBuildString - } - - deinit { - removeObservers() - } - - func show() { - showWindow(self) - addObservers() - - // Get currentProduct needs to be run before resultWindow.makeMainWindow() - if let currentProduct = CMXcodeWorkSpace.currentProductName() { - processLog(currentProduct) - } - - if let window = resultWindow { - window.makeMainWindow() - window.level = Int(CGWindowLevelKey.OverlayWindowLevelKey.rawValue) - updateViewForState() - } - } - - func processLog(productName: String, buildCompletionDate: NSDate? = nil) { - guard processingState != .processing else { return } - processingState = .processing - - dataSource.removeAll() - searchField.stringValue = "" - tableView.reloadData() - - processor.process(productName, buildCompletionDate: buildCompletionDate, updateHandler: { [weak self] (result, didComplete) in - guard let strongSelf = self else { return } - - strongSelf.dataSource = result - strongSelf.perFunctionTimes = result - strongSelf.perFileTimes = strongSelf.aggregateTimesByFile(strongSelf.perFunctionTimes) - strongSelf.tableView.reloadData() - - if didComplete { - let stateName = strongSelf.dataSource.isEmpty ? CMProcessingState.failedString : CMProcessingState.completedString - strongSelf.processingState = .completed(stateName: stateName) - } - }) - } - - /* - * Aggregates all function times by file - */ - func aggregateTimesByFile(functionTimes: [CMCompileMeasure]) -> [CMCompileMeasure] { - - var fileTimes = [String: CMCompileMeasure]() - - for measure in functionTimes { - - if var fileMeasure = fileTimes[measure.path] { - // File exists, increment time - fileMeasure.time += measure.time - - fileTimes[measure.path] = fileMeasure - } else { - let newFileMeasure = CMCompileMeasure(rawPath: measure.path, time: measure.time) - - fileTimes[measure.path] = newFileMeasure - } - } - - // Sort by time - let sortedFiles = Array(fileTimes.values).sort({ $0.time > $1.time }) - - return sortedFiles - } - - func updateViewForState() { - switch processingState { - case .processing: - progressIndicator.hidden = false - progressIndicator.startAnimation(self) - statusTextField.stringValue = CMProcessingState.processingString - showInstructions(false) - cancelButton.hidden = false - - case .completed(let stateName): - progressIndicator.stopAnimation(self) - statusTextField.stringValue = stateName - showInstructions(stateName == CMProcessingState.failedString) - progressIndicator.hidden = true - cancelButton.hidden = true - - case .waiting(let shouldIndicate): - if shouldIndicate { - progressIndicator.startAnimation(self) - statusTextField.stringValue = CMProcessingState.buildString - showInstructions(false) - } else { - progressIndicator.stopAnimation(self) - statusTextField.stringValue = CMProcessingState.waitingForBuildString - } - progressIndicator.hidden = !shouldIndicate - cancelButton.hidden = true - } - searchField.hidden = !cancelButton.hidden - } - - func showInstructions(show: Bool) { - instructionsView.hidden = !show - progressIndicator.hidden = show - tableViewContainerView.hidden = show - } - - // MARK: Actions - - @IBAction func perFileCheckboxClicked(sender: AnyObject) { - if perFileCheckbox.state == 0 { - self.dataSource = self.perFunctionTimes - } else { - self.dataSource = self.perFileTimes - } - self.tableView.reloadData() - } - - @IBAction func clipboardButtonClicked(sender: AnyObject) { - NSPasteboard.generalPasteboard().clearContents() - NSPasteboard.generalPasteboard().writeObjects(["-Xfrontend -debug-time-function-bodies"]) - } - - @IBAction func cancelButtonClicked(sender: AnyObject) { - processor.shouldCancel = true - } - // MARK: Observers - - func addObservers() { - buildOperationWillStartObserver = NSNotificationCenter.addObserverForName(IDEBuildOperationWillStartNotification, usingBlock: { [weak self] (note) in - if let stateDescription = note.object?.valueForKeyPath("_buildStatus._stateDescription") as? String { - self?.processingState = .waiting(shouldIndicate: stateDescription == "Build") - } - }) - - buildOperationDidGenerateOutputFilesObserver = NSNotificationCenter.addObserverForName(IDEBuildOperationDidGenerateOutputFilesNotification, usingBlock: { [weak self] (note) in - guard let buildOperation = CMXcodeWorkSpace.buildOperation(fromData: note.object) else { return } - let result = buildOperation.result - let action = buildOperation.actionName - - guard (action == "Build" || action == "Compile") && (result == .success || result == .failed || result == .cancelled) else { - self?.processingState = .waiting(shouldIndicate: false) - return - } - - self?.buildDurationTextField.stringValue = String(format: "%.0fs", round(buildOperation.duration)) - self?.processLog(buildOperation.productName, buildCompletionDate: buildOperation.endTime) - }) - } - - func removeObservers() { - NSNotificationCenter.removeObserver(buildOperationWillStartObserver, name: IDEBuildOperationWillStartNotification) - NSNotificationCenter.removeObserver(buildOperationDidGenerateOutputFilesObserver, name: IDEBuildOperationWillStartNotification) - } - - override func controlTextDidChange(obj: NSNotification) { - guard let field = obj.object as? NSSearchField where field == searchField else { return } - - filteredData = field.stringValue.isEmpty ? nil : dataSource.filter{ textContains($0.code) || textContains($0.filename) } - tableView.reloadData() - } - - func textContains(text: String) -> Bool { - return text.lowercaseString.containsString(searchField.stringValue.lowercaseString) - } -} - -extension CMResultWindowController: NSTableViewDataSource { - func numberOfRowsInTableView(tableView: NSTableView) -> Int { - return filteredData?.count ?? dataSource.count - } -} - -extension CMResultWindowController: NSTableViewDelegate { - - func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? { - guard let tableColumn = tableColumn, columnIndex = tableView.tableColumns.indexOf(tableColumn) else { return nil } - - let result = tableView.makeViewWithIdentifier("Cell\(columnIndex)", owner: self) as? NSTableCellView - result?.textField?.stringValue = filteredData?[row][columnIndex] ?? dataSource[row][columnIndex] - - return result - } - - func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool { - let item = filteredData?[row] ?? dataSource[row] - processor.workspace?.openFile(atPath: item.path, andLineNumber: item.location, focusLostHandler: { [weak self] in - self?.resultWindow.makeKeyWindow() - }) - return true - } -} - -extension CMResultWindowController: NSWindowDelegate { - - func windowWillClose(notification: NSNotification) { - processor.shouldCancel = true - processingState = .completed(stateName: CMProcessingState.cancelledString) - removeObservers() - } -} diff --git a/BuildTimeAnalyzer/CMXcodeWorkspace.swift b/BuildTimeAnalyzer/CMXcodeWorkspace.swift deleted file mode 100644 index 1b729ee..0000000 --- a/BuildTimeAnalyzer/CMXcodeWorkspace.swift +++ /dev/null @@ -1,295 +0,0 @@ -// -// CMXcodeWorkspace.swift -// BuildTimeAnalyzer -// -// Created by Robert Gummesson on 30/04/2016. -// Copyright © 2016 Robert Gummesson. All rights reserved. -// - -import Cocoa - -protocol CMXcodeWorkspaceProtocol { - var retryAttempts: Int { get } - var productName: String { get set } - var buildCompletionDate: NSDate? { get set } - var focusLostHandler: (() -> ())? { get set } - - init(productName: String, buildCompletionDate: NSDate?) - func willOpenDocument(atLineNumber lineNumber: Int, usingTextView textView: NSTextView?) -} - -extension CMXcodeWorkspaceProtocol { - - func logTextForProduct(attemptIndex: Int = 0, completionHandler: ((text: String?) -> ())) { - guard let buildFolderPath = buildFolderPath(), - let buildFolderURL = NSURL(string: buildFolderPath), - let filenames = filesAtURL(buildFolderURL) else { - completionHandler(text: nil) - return - } - - var keyFilename: String? - var creationDates: [String: NSDate] = [:] - parseFiles(filenames, buildFolderURL: buildFolderURL, keyFilename: &keyFilename, creationDates: &creationDates) - - var lastLogPath: String? - var lastLogCreationDate: NSDate? - if let keyFilename = keyFilename, filename = buildFolderURL.URLByAppendingPathComponent(keyFilename).path { - lastLogPath = filename - lastLogCreationDate = creationDates[keyFilename] - } - validatePath(lastLogPath, creationDate: lastLogCreationDate, attemptIndex: attemptIndex, completionHandler: completionHandler) - } - - func productWorkspace() -> AnyObject? { - guard let windowControllers = Self.workspaceWindowControllers() else { return nil } - guard let keyWindow = windowControllers.filter({ ($0.valueForKeyPath("_workspace.name") as? String) == productName }).first else { - return locateWorkspace(fromWindowControllers: windowControllers) - } - return keyWindow.valueForKey("_workspace") - } - - func buildFolderFromWorkspace(workspace: AnyObject?) -> NSURL? { - return (workspace?.valueForKeyPath("_workspaceArena.logFolderPath.fileURL") as? NSURL)?.URLByAppendingPathComponent("Build") - } - - func buildFolderPath() -> String? { - return buildFolderFromWorkspace(productWorkspace())?.path - } - - mutating func openFile(atPath path: String, andLineNumber lineNumber: Int, focusLostHandler: () -> ()) { - if let textView = Self.editorForFilepath(path) { - willOpenDocument(atLineNumber: lineNumber, usingTextView: textView) - } else { - self.focusLostHandler = focusLostHandler - willOpenDocument(atLineNumber: lineNumber, usingTextView: nil) - NSApp.delegate?.application?(NSApp, openFile: path) - } - } - - // MARK: Public static methods - - static func buildOperation(fromData data: AnyObject?) -> CMBuildOperation? { - guard let actionName = data?.valueForKeyPath("_buildOperationDescription._actionName") as? String, - var productName = data?.valueForKeyPath("_buildOperationDescription._objectToBuildName") as? String, - let duration = data?.valueForKey("duration") as? Double, - let intResult = data?.valueForKey("_result") as? Int, - let cmResult = CMBuildResult(rawValue: intResult), - let startTime = data?.valueForKey("_startTime") as? NSDate else { - return nil - } - - if actionName == "Compile", let product = currentProductName() { - productName = product - } - - return CMBuildOperation(actionName: actionName, productName: productName, duration: duration, result: cmResult, startTime: startTime) - } - - static func currentProductName() -> String? { - guard let workSpaceControllers = workspaceWindowControllers() else { return nil } - - for controller in workSpaceControllers { - guard - let window = controller.valueForKey("window") as? NSWindow where window.keyWindow, - let workspace = controller.valueForKey("_workspace"), projectName = workspace.valueForKey("name") as? String - else { - continue - } - return projectName - } - return nil - } - - // MARK: Private static methods - - private static func editorForFilepath(filepath: String) -> NSTextView? { - guard let workSpaceControllers = workspaceWindowControllers() else { return nil } - - for controller in workSpaceControllers { - guard - let editor = controller.valueForKeyPath("editorArea.lastActiveEditorContext.editor"), - let IDESourceCodeEditor = NSClassFromString("IDESourceCodeEditor") where editor.isKindOfClass(IDESourceCodeEditor), - let url = editor.valueForKeyPath("sourceCodeDocument.fileURL") as? NSURL, - let path = url.path where path == filepath, - let textView = editor.valueForKey("_textView") - else { - continue - } - return textView as? NSTextView - } - return nil - } - - private static func workspaceWindowControllers() -> [AnyObject]? { - guard let windowController = NSClassFromString("IDEWorkspaceWindowController") else { return nil } - return windowController.valueForKey("workspaceWindowControllers") as? [AnyObject] - } - - // MARK: Private methods - - private func locateWorkspace(fromWindowControllers windowControllers: [AnyObject]) -> AnyObject? { - if windowControllers.count == 1 { - return windowControllers.first?.valueForKey("_workspace") - } else { - for controller in windowControllers { - if let workspace = controller.valueForKey("_workspace"), - let logFolderURL = buildFolderFromWorkspace(workspace), - let files = filesAtURL(logFolderURL), - let filename = files.filter({ $0.hasSuffix(".db") }).first, - let path = logFolderURL.URLByAppendingPathComponent(filename).path, - let schemeName = lastSchemeName(fromPath: path) where schemeName == productName { - return workspace - } - } - } - return nil - } - - private func filesAtURL(url: NSURL) -> [String]? { - guard let path = url.path, enumerator = NSFileManager.defaultManager().enumeratorAtPath(path) else { return nil } - - var result: [String] = [] - for file in enumerator { - if let filename = file as? String { - result.append(filename) - } - } - return result - } - - private func validatePath(path: String?, creationDate: NSDate?, attemptIndex: Int, completionHandler: ((text: String?) -> ())) { - if let path = path, creationDate = creationDate where buildCompletionDate == nil || buildCompletionDate?.compare(creationDate) == .OrderedAscending || buildCompletionDate?.compare(creationDate) == .OrderedSame { - completionHandler(text: contentsOfFile(path)) - return - } else if buildCompletionDate != nil { - let attemptIndex = attemptIndex + 1 - if attemptIndex < retryAttempts { - let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC)) - dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { - self.logTextForProduct(attemptIndex, completionHandler: completionHandler) - } - return - } - } - completionHandler(text: nil) - } - - private func parseFiles(filenames: [String], buildFolderURL: NSURL, inout keyFilename: String?, inout creationDates: [String: NSDate]) { - let activityLogExtension = ".xcactivitylog" - for filename in filenames { - if filename.hasSuffix(".db"), - let path = buildFolderURL.URLByAppendingPathComponent(filename).path, - let lastBuild = lastBuildKey(fromPath: path) { - keyFilename = "\(lastBuild)\(activityLogExtension)" - } else if filename.hasSuffix(activityLogExtension), - let path = buildFolderURL.URLByAppendingPathComponent(filename).path, - let creationDate = creationDateForFile(path) { - creationDates[filename] = creationDate - } - } - } - - private func lastBuildKey(fromPath path: String) -> String? { - return lastDatabaseEntry(fromPath: path, usingFunction: { (key, value) -> String? in - if let title = value["title"] as? String where title.hasPrefix("Build ") || title.hasPrefix("Compile "){ - return key - } - return nil - }) - } - - private func lastSchemeName(fromPath path: String) -> String? { - return lastDatabaseEntry(fromPath: path, usingFunction: { (key, value) -> String? in - return value["schemeIdentifier-schemeName"] as? String - }) - } - - private func lastDatabaseEntry(fromPath path: String, usingFunction f: (key: String, value: [String : AnyObject]) -> String?) -> String? { - guard let data = NSDictionary(contentsOfFile: path)?["logs"] as? [String: AnyObject], - let key = sortKeys(usingData: data).last?.key, - let value = data[key] as? [String : AnyObject] else { return nil } - - return f(key: key, value: value) - } - - private func sortKeys(usingData data: [String: AnyObject]) -> [(UInt, key: String)] { - var sortedKeys: [(UInt, key: String)] = [] - for key in data.keys { - if let value = data[key] as? [String: AnyObject], - let timeStoppedRecording = value["timeStoppedRecording"] as? UInt { - sortedKeys.append((timeStoppedRecording, key)) - } - } - return sortedKeys.sort{ $0.0 < $1.0 } - } - - private func contentsOfFile(path: String) -> String? { - if let data = NSData(contentsOfFile: path)?.gunzippedData() { - return String(data: data, encoding: NSUTF8StringEncoding) - } - return nil - } - - private func creationDateForFile(path: String) -> NSDate? { - let fileAttributes = try? NSFileManager.defaultManager().attributesOfItemAtPath(path) - return fileAttributes?[NSFileCreationDate] as? NSDate - } -} - -class CMXcodeWorkSpace: NSObject, CMXcodeWorkspaceProtocol { - - let notificationName = "IDESourceCodeEditorDidFinishSetup" - - var retryAttempts = 10 - var productName: String - var buildCompletionDate: NSDate? - var lineNumber = 0 - var focusLostHandler: (() -> ())? - - required init(productName: String, buildCompletionDate: NSDate?) { - self.productName = productName - self.buildCompletionDate = buildCompletionDate - } - - deinit { - NSNotificationCenter.defaultCenter().removeObserver(self, name: notificationName, object: nil) - } - - func willOpenDocument(atLineNumber lineNumber: Int, usingTextView textView: NSTextView?) { - self.lineNumber = lineNumber - if let textView = textView { - adjustSelection(forTextView: textView) - } else { - NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(sourceCodeEditorDidFinishSetup(_:)), name: notificationName, object: nil) - } - } - - func sourceCodeEditorDidFinishSetup(notification: NSNotification) { - NSNotificationCenter.defaultCenter().removeObserver(self, name: notificationName, object: nil) - - dispatch_async(dispatch_get_main_queue()) { - self.adjustSelection(forTextView: notification.object?.valueForKeyPath("_textView") as? NSTextView) - self.focusLostHandler?() - self.focusLostHandler = nil - } - } - - func adjustSelection(forTextView textView: NSTextView?) { - guard let textView = textView, text = textView.textStorage?.string else { return } - - let subSequences = text.characters.split("\n", allowEmptySlices: true) - let lineCount = subSequences.count > lineNumber ? lineNumber : subSequences.count - - var characterCount = 0 - subSequences.dropLast(subSequences.count - lineCount).forEach({ (subSequence) in - characterCount += String(subSequence).characters.count - }) - - let range = NSMakeRange(characterCount + lineNumber - 1, 0) - if range.location < text.characters.count { - textView.selectedRange = range - textView.scrollRangeToVisible(range) - } - } -} diff --git a/BuildTimeAnalyzer/CacheFile.swift b/BuildTimeAnalyzer/CacheFile.swift new file mode 100644 index 0000000..99f0e8e --- /dev/null +++ b/BuildTimeAnalyzer/CacheFile.swift @@ -0,0 +1,12 @@ +// +// CacheFile.swift +// BuildTimeAnalyzer +// + +import Foundation + +struct CacheFile { + let name: String + let path: String + let modificationDate: Date +} diff --git a/BuildTimeAnalyzer/CMCompileMeasure.swift b/BuildTimeAnalyzer/CompileMeasure.swift similarity index 63% rename from BuildTimeAnalyzer/CMCompileMeasure.swift rename to BuildTimeAnalyzer/CompileMeasure.swift index 19c8746..3cee53e 100755 --- a/BuildTimeAnalyzer/CMCompileMeasure.swift +++ b/BuildTimeAnalyzer/CompileMeasure.swift @@ -1,14 +1,11 @@ // -// CMCompileMeasure.swift +// CompileMeasure.swift // BuildTimeAnalyzer // -// Created by Robert Gummesson on 02/05/2016. -// Copyright © 2016 Robert Gummesson. All rights reserved. -// import Foundation -struct CMCompileMeasure { +struct CompileMeasure { var time: Double var path: String @@ -31,17 +28,17 @@ struct CMCompileMeasure { } var timeString: String { - return "\(time)ms" + return String(format: "%.1fms", time) } init?(time: Double, rawPath: String, code: String, references: Int) { - let untrimmedFilename = rawPath.characters.split("/").map(String.init).last + let untrimmedFilename = rawPath.characters.split(separator: "/").map(String.init).last - guard let filepath = rawPath.characters.split(":").map(String.init).first else { return nil } - guard let filename = untrimmedFilename?.characters.split(":").map(String.init).first else { return nil } + guard let filepath = rawPath.characters.split(separator: ":").map(String.init).first else { return nil } + guard let filename = untrimmedFilename?.characters.split(separator: ":").map(String.init).first else { return nil } - let locationString = String(rawPath.substringFromIndex(filepath.endIndex).characters.dropFirst()) - let locations = locationString.characters.split(":").flatMap{ Int(String.init($0)) } + let locationString = String(rawPath.substring(from: filepath.endIndex).characters.dropFirst()) + let locations = locationString.characters.split(separator: ":").flatMap{ Int(String.init($0)) } guard locations.count == 2 else { return nil } self.time = time @@ -53,10 +50,10 @@ struct CMCompileMeasure { } init?(rawPath: String, time: Double) { - let untrimmedFilename = rawPath.characters.split("/").map(String.init).last + let untrimmedFilename = rawPath.characters.split(separator: "/").map(String.init).last - guard let filepath = rawPath.characters.split(":").map(String.init).first else { return nil } - guard let filename = untrimmedFilename?.characters.split(":").map(String.init).first else { return nil } + guard let filepath = rawPath.characters.split(separator: ":").map(String.init).first else { return nil } + guard let filename = untrimmedFilename?.characters.split(separator: ":").map(String.init).first else { return nil } self.time = time self.code = "" @@ -81,4 +78,4 @@ struct CMCompileMeasure { } } } -} \ No newline at end of file +} diff --git a/BuildTimeAnalyzer/DerivedDataManager.swift b/BuildTimeAnalyzer/DerivedDataManager.swift new file mode 100644 index 0000000..c6b6fd1 --- /dev/null +++ b/BuildTimeAnalyzer/DerivedDataManager.swift @@ -0,0 +1,72 @@ +// +// DerivedDataManager.swift +// BuildTimeAnalyzer +// + +import Foundation + +class DerivedDataManager { + + static func derivedData() -> [File] { + let url = URL(fileURLWithPath: UserSettings.derivedDataLocation) + + let folders = DerivedDataManager.listFolders(at: url) + let fileManager = FileManager.default + + return folders.flatMap{ (url) -> File? in + if url.lastPathComponent != "ModuleCache", + let properties = try? fileManager.attributesOfItem(atPath: url.path), + let modificationDate = properties[FileAttributeKey.modificationDate] as? Date { + return File(date: modificationDate, url: url) + } + return nil + }.sorted{ $0.date > $1.date } + } + + static func listFolders(at url: URL) -> [URL] { + let fileManager = FileManager.default + let keys = [URLResourceKey.nameKey, URLResourceKey.isDirectoryKey] + let options: FileManager.DirectoryEnumerationOptions = [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants] + + guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, options: options, errorHandler: nil) else { return [] } + + return enumerator.map{ $0 as! URL } + } + + static func listCacheFiles() -> [CacheFile] { + let files = cacheFiles(at: URL(fileURLWithPath: UserSettings.derivedDataLocation)) + let earliestDate = Date().addingTimeInterval(-24 * 60 * 60) + return filterFiles(files, byEarliestDate: earliestDate) + } + + static private func cacheFiles(at url: URL) -> [CacheFile] { + let fileManager = FileManager.default + let keys = [URLResourceKey.nameKey, URLResourceKey.isDirectoryKey] + let options: FileManager.DirectoryEnumerationOptions = [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants] + + guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, options: options, errorHandler: nil) else { return [] } + + var result: [CacheFile] = [] + for case let fileURL as URL in enumerator { + let name = fileURL.lastPathComponent + let cachePath = fileURL.appendingPathComponent("Logs/Build/Cache.db").path + + if let properties = try? fileManager.attributesOfItem(atPath: cachePath), + let modificationDate = properties[FileAttributeKey.modificationDate] as? Date { + result.append(CacheFile(name: name, path: cachePath, modificationDate: modificationDate)) + } + } + return result + } + + static private func filterFiles(_ files: [CacheFile], byEarliestDate date: Date) -> [CacheFile] { + guard files.count > 0 else { return [] } + + let sortedFiles = files.sorted(by: { $0.modificationDate > $1.modificationDate }) + let recentFiles = sortedFiles.filter({ $0.modificationDate > date }) + if recentFiles.count == 0 { + return [sortedFiles[0]] + } + return recentFiles + } +} diff --git a/BuildTimeAnalyzer/DirectoryMonitor.swift b/BuildTimeAnalyzer/DirectoryMonitor.swift new file mode 100644 index 0000000..b39d5fe --- /dev/null +++ b/BuildTimeAnalyzer/DirectoryMonitor.swift @@ -0,0 +1,80 @@ +// +// DirectoryMonitor.swift +// BuildTimeAnalyzer +// + +import Foundation + +protocol DirectoryMonitorDelegate: class { + func directoryMonitorDidObserveChange(_ directoryMonitor: DirectoryMonitor, isDerivedData: Bool) +} + +class DirectoryMonitor { + var dispatchQueue: DispatchQueue + + weak var delegate: DirectoryMonitorDelegate? + + var fileDescriptor: Int32 = -1 + var dispatchSource: DispatchSourceFileSystemObject? + var isDerivedData: Bool + var path: String? + var timer: Timer? + var lastDerivedDataDate = Date() + var isMonitoringDates = false + + init(isDerivedData: Bool) { + self.isDerivedData = isDerivedData + + let suffix = isDerivedData ? "deriveddata" : "logfolder" + dispatchQueue = DispatchQueue(label: "uk.co.canemedia.directorymonitor.\(suffix)", attributes: .concurrent) + } + + func startMonitoring(path: String) { + self.path = path + + guard dispatchSource == nil && fileDescriptor == -1 else { return } + + fileDescriptor = open(path, O_EVTONLY) + guard fileDescriptor != -1 else { return } + + dispatchSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .all, queue: dispatchQueue) + dispatchSource?.setEventHandler { + DispatchQueue.main.async { + self.delegate?.directoryMonitorDidObserveChange(self, isDerivedData: self.isDerivedData) + } + } + dispatchSource?.setCancelHandler { + close(self.fileDescriptor) + + self.fileDescriptor = -1 + self.dispatchSource = nil + self.path = nil + } + dispatchSource?.resume() + + if isDerivedData && !isMonitoringDates { + isMonitoringDates = true + monitorModificationDates() + } + } + + func stopMonitoring() { + dispatchSource?.cancel() + path = nil + } + + func monitorModificationDates() { + if let date = DerivedDataManager.derivedData().first?.date, date > lastDerivedDataDate { + lastDerivedDataDate = date + self.delegate?.directoryMonitorDidObserveChange(self, isDerivedData: self.isDerivedData) + } + + if path != nil { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.monitorModificationDates() + } + } else { + isMonitoringDates = false + } + } +} diff --git a/BuildTimeAnalyzer/File.swift b/BuildTimeAnalyzer/File.swift new file mode 100644 index 0000000..9b494ba --- /dev/null +++ b/BuildTimeAnalyzer/File.swift @@ -0,0 +1,11 @@ +// +// File.swift +// BuildTimeAnalyzer +// + +import Foundation + +struct File { + let date: Date + let url: URL +} diff --git a/BuildTimeAnalyzer/Info.plist b/BuildTimeAnalyzer/Info.plist index d6396a2..7e46668 100644 --- a/BuildTimeAnalyzer/Info.plist +++ b/BuildTimeAnalyzer/Info.plist @@ -6,6 +6,8 @@ en CFBundleExecutable $(EXECUTABLE_NAME) + CFBundleIconFile + CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion @@ -13,29 +15,20 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - BNDL + APPL CFBundleShortVersionString - 1.0 + 1.0.0 CFBundleSignature ???? CFBundleVersion 1 - DVTPlugInCompatibilityUUIDs - - ACA8656B-FEA8-4B6D-8E4A-93F4C95C362C - F41BD31E-2683-44B8-AE7F-5F09E919790E - + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright - Copyright © 2016 Cane Media. All rights reserved. + Copyright © 2016 Cane Media Ltd. All rights reserved. + NSMainStoryboardFile + Main NSPrincipalClass - CompileTime - LSApplicationCategoryType - - XC4Compatible - - XCGCReady - - XCPluginHasUI - + NSApplication diff --git a/BuildTimeAnalyzer/LogProcessor.swift b/BuildTimeAnalyzer/LogProcessor.swift new file mode 100644 index 0000000..b274bcd --- /dev/null +++ b/BuildTimeAnalyzer/LogProcessor.swift @@ -0,0 +1,131 @@ +// +// LogProcessor.swift +// BuildTimeAnalyzer +// + +import Foundation + +typealias CMUpdateClosure = (_ result: [CompileMeasure], _ didComplete: Bool, _ didCancel: Bool) -> () + +protocol LogProcessorProtocol: class { + var rawMeasures: [String: RawMeasure] { get set } + var updateHandler: CMUpdateClosure? { get set } + var shouldCancel: Bool { get set } + + func processingDidStart() + func processingDidFinish() +} + +extension LogProcessorProtocol { + func processDatabase(database: XcodeDatabase, updateHandler: CMUpdateClosure?) { + guard let text = database.processLog() else { + updateHandler?([], true, false) + return + } + + self.updateHandler = updateHandler + DispatchQueue.global().async { + self.process(text: text) + } + } + + // MARK: Private methods + + private func process(text: String) { + let characterSet = CharacterSet(charactersIn:"\r\"") + var remainingRange = text.startIndex.. 10 } + if filteredResults.count < 20 { + filteredResults = rawMeasures.values.filter{ $0.time > 0.1 } + } + + let sortedResults = filteredResults.sorted(by: { $0.time > $1.time }) + updateHandler?(processResult(sortedResults), completed, didCancel) + + if completed { + rawMeasures.removeAll() + } + } + + private func processResult(_ unprocessedResult: [RawMeasure]) -> [CompileMeasure] { + var result: [CompileMeasure] = [] + for entry in unprocessedResult { + let code = entry.text.characters.split(separator: "\t").map(String.init) + if code.count >= 2, let measure = CompileMeasure(time: entry.time, rawPath: code[0], code: trimPrefixes(code[1]), references: entry.references) { + result.append(measure) + } + } + return result + } + + private func trimPrefixes(_ code: String) -> String { + var code = code + ["@objc ", "final ", "@IBAction "].forEach { (prefix) in + if code.hasPrefix(prefix) { + code = code.substring(from: code.index(code.startIndex, offsetBy: prefix.characters.count)) + } + } + return code + } +} + +class LogProcessor: NSObject, LogProcessorProtocol { + + var rawMeasures: [String: RawMeasure] = [:] + var updateHandler: CMUpdateClosure? + var shouldCancel = false + var timer: Timer? + + func processingDidStart() { + DispatchQueue.main.async { + self.timer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(self.timerCallback(_:)), userInfo: nil, repeats: true) + } + } + + func processingDidFinish() { + DispatchQueue.main.async { + self.timer?.invalidate() + self.timer = nil + let didCancel = self.shouldCancel + self.shouldCancel = false + self.updateResults(didComplete: true, didCancel: didCancel) + } + } + + func timerCallback(_ timer: Timer) { + updateResults(didComplete: false, didCancel: false) + } +} diff --git a/BuildTimeAnalyzer/Main.storyboard b/BuildTimeAnalyzer/Main.storyboard new file mode 100644 index 0000000..54717f3 --- /dev/null +++ b/BuildTimeAnalyzer/Main.storyboard @@ -0,0 +1,755 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BuildTimeAnalyzer/NSAlert+Extensions.swift b/BuildTimeAnalyzer/NSAlert+Extensions.swift new file mode 100644 index 0000000..e8bd2d2 --- /dev/null +++ b/BuildTimeAnalyzer/NSAlert+Extensions.swift @@ -0,0 +1,17 @@ +// +// NSAlert+Extensions.swift +// BuildTimeAnalyzer +// + +import Cocoa + +extension NSAlert { + static func show(withMessage message: String, andInformativeText informativeText: String = "") { + let alert = NSAlert() + alert.messageText = message + alert.informativeText = informativeText + alert.alertStyle = .warning + alert.addButton(withTitle: "OK") + alert.runModal() + } +} diff --git a/BuildTimeAnalyzer/ProcessingState.swift b/BuildTimeAnalyzer/ProcessingState.swift new file mode 100644 index 0000000..71fbb4e --- /dev/null +++ b/BuildTimeAnalyzer/ProcessingState.swift @@ -0,0 +1,31 @@ +// +// ProcessingState.swift +// BuildTimeAnalyzer +// + +enum ProcessingState { + case processing + case waiting + case completed(didSucceed: Bool, stateName: String) + + static let cancelledString = "Cancelled" + static let completedString = "Completed" + static let failedString = "No valid logs found" + static let processingString = "Processing log..." + static let waitingForBuildString = "Waiting..." +} + +extension ProcessingState : Equatable {} + +func ==(lhs: ProcessingState, rhs: ProcessingState) -> Bool { + switch (lhs, rhs) { + case (let .completed(didSucceed1, _), let .completed(didSucceed2, _)): + return didSucceed1 == didSucceed2 + + case (.processing, .processing), (.waiting, .waiting): + return true + + default: + return false + } +} diff --git a/BuildTimeAnalyzer/ProjectSelection.swift b/BuildTimeAnalyzer/ProjectSelection.swift new file mode 100644 index 0000000..3942750 --- /dev/null +++ b/BuildTimeAnalyzer/ProjectSelection.swift @@ -0,0 +1,71 @@ +// +// ProjectSelection.swift +// BuildTimeAnalyzer +// + +import Cocoa + +protocol ProjectSelectionDelegate: class { + func didSelectProject(with database: XcodeDatabase) +} + +class ProjectSelection: NSObject { + + @IBOutlet weak var tableView: NSTableView! + weak var delegate: ProjectSelectionDelegate? + + fileprivate var dataSource: [XcodeDatabase] = [] + + static fileprivate let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .medium + return dateFormatter + }() + + func listFolders() { + dataSource = DerivedDataManager.derivedData().flatMap{ + XcodeDatabase(fromPath: $0.url.appendingPathComponent("Logs/Build/Cache.db").path) + }.sorted(by: { $0.modificationDate > $1.modificationDate }) + + tableView.reloadData() + } + + // MARK: Actions + + @IBAction func didSelectCell(_ sender: NSTableView) { + delegate?.didSelectProject(with: dataSource[sender.selectedRow]) + } +} + +// MARK: NSTableViewDataSource + +extension ProjectSelection: NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + return dataSource.count + } +} + +// MARK: NSTableViewDelegate + +extension ProjectSelection: NSTableViewDelegate { + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + guard let tableColumn = tableColumn, let columnIndex = tableView.tableColumns.index(of: tableColumn) else { return nil } + + let cellView = tableView.make(withIdentifier: "Cell\(columnIndex)", owner: self) as? NSTableCellView + + let source = dataSource[row] + var value = "" + + switch columnIndex { + case 0: + value = source.schemeName + default: + value = ProjectSelection.dateFormatter.string(from: source.modificationDate) + } + cellView?.textField?.stringValue = value + + return cellView + } +} diff --git a/BuildTimeAnalyzer/CMRawMeasure.swift b/BuildTimeAnalyzer/RawMeasure.swift similarity index 59% rename from BuildTimeAnalyzer/CMRawMeasure.swift rename to BuildTimeAnalyzer/RawMeasure.swift index d59f375..cad4f82 100644 --- a/BuildTimeAnalyzer/CMRawMeasure.swift +++ b/BuildTimeAnalyzer/RawMeasure.swift @@ -1,14 +1,11 @@ // -// CMRawMeasure.swift +// RawMeasure.swift // BuildTimeAnalyzer // -// Created by Robert Gummesson on 04/05/2016. -// Copyright © 2016 Robert Gummesson. All rights reserved. -// import Foundation -struct CMRawMeasure { +struct RawMeasure { var time: Double var text: String var references: Int @@ -22,15 +19,15 @@ struct CMRawMeasure { // MARK: Equatable -extension CMRawMeasure: Equatable {} +extension RawMeasure: Equatable {} -func ==(lhs: CMRawMeasure, rhs: CMRawMeasure) -> Bool { +func ==(lhs: RawMeasure, rhs: RawMeasure) -> Bool { return lhs.time == rhs.time && lhs.text == rhs.text } // MARK: Hashable -extension CMRawMeasure: Hashable { +extension RawMeasure: Hashable { var hashValue: Int { return time.hashValue ^ text.hashValue } diff --git a/BuildTimeAnalyzer/UserSettings.swift b/BuildTimeAnalyzer/UserSettings.swift new file mode 100644 index 0000000..5f0a4f4 --- /dev/null +++ b/BuildTimeAnalyzer/UserSettings.swift @@ -0,0 +1,46 @@ +// +// UserCache.swift +// BuildTimeAnalyzer +// + +import Foundation + +class UserSettings { + + static private let derivedDataLocationKey = "derivedDataLocationKey" + static private let windowLevelIsNormalKey = "windowLevelIsNormalKey" + + static private var _derivedDataLocation: String? + static private var _windowLevelIsNormal: Bool? + + static var derivedDataLocation: String { + get { + if _derivedDataLocation == nil { + _derivedDataLocation = UserDefaults.standard.string(forKey: derivedDataLocationKey) + } + if _derivedDataLocation == nil, let libraryFolder = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first { + _derivedDataLocation = "\(libraryFolder)/Developer/Xcode/DerivedData" + } + return _derivedDataLocation ?? "" + } + set { + _derivedDataLocation = newValue + UserDefaults.standard.set(newValue, forKey: derivedDataLocationKey) + UserDefaults.standard.synchronize() + } + } + + static var windowShouldBeTopMost: Bool { + get { + if _windowLevelIsNormal == nil { + _windowLevelIsNormal = UserDefaults.standard.bool(forKey: windowLevelIsNormalKey) + } + return !(_windowLevelIsNormal ?? true) + } + set { + _windowLevelIsNormal = !newValue + UserDefaults.standard.set(_windowLevelIsNormal, forKey: windowLevelIsNormalKey) + UserDefaults.standard.synchronize() + } + } +} diff --git a/BuildTimeAnalyzer/ViewController.swift b/BuildTimeAnalyzer/ViewController.swift new file mode 100644 index 0000000..f5d1b57 --- /dev/null +++ b/BuildTimeAnalyzer/ViewController.swift @@ -0,0 +1,303 @@ +// +// ViewController.swift +// BuildTimeAnalyzer +// + +import Cocoa + +class ViewController: NSViewController { + + @IBOutlet var buildManager: BuildManager! + @IBOutlet weak var cancelButton: NSButton! + @IBOutlet weak var compileTimeTextField: NSTextField! + @IBOutlet weak var derivedDataTextField: NSTextField! + @IBOutlet weak var instructionsView: NSView! + @IBOutlet weak var leftButton: NSButton! + @IBOutlet weak var perFileButton: NSButton! + @IBOutlet weak var progressIndicator: NSProgressIndicator! + @IBOutlet weak var projectSelection: ProjectSelection! + @IBOutlet weak var searchField: NSSearchField! + @IBOutlet weak var statusLabel: NSTextField! + @IBOutlet weak var statusTextField: NSTextField! + @IBOutlet weak var tableView: NSTableView! + @IBOutlet weak var tableViewContainerView: NSScrollView! + + fileprivate var dataSource: [CompileMeasure] = [] + fileprivate var filteredData: [CompileMeasure]? + + private var currentKey: String? + private var nextDatabase: XcodeDatabase? + + private var processor = LogProcessor() + private var perFunctionTimes: [CompileMeasure] = [] + private var perFileTimes: [CompileMeasure] = [] + + var processingState: ProcessingState = .waiting { + didSet { + updateViewForState() + } + } + + // MARK: Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureLayout() + + buildManager.delegate = self + projectSelection.delegate = self + projectSelection.listFolders() + + NotificationCenter.default.addObserver(self, selector: #selector(windowWillClose(notification:)), name: .NSWindowWillClose, object: nil) + } + + func windowWillClose(notification: NSNotification) { + guard let object = notification.object, !(object is NSPanel) else { return } + NotificationCenter.default.removeObserver(self) + + processor.shouldCancel = true + NSApp.terminate(self) + } + + // MARK: Layout + + func configureLayout() { + updateTotalLabel(with: 0) + updateViewForState() + showInstructions(true) + + derivedDataTextField.stringValue = UserSettings.derivedDataLocation + makeWindowTopMost(topMost: UserSettings.windowShouldBeTopMost) + } + + func showInstructions(_ show: Bool) { + instructionsView.isHidden = !show + + let views: [NSView] = [compileTimeTextField, leftButton, perFileButton, searchField, statusLabel, statusTextField, tableViewContainerView] + views.forEach{ $0.isHidden = show } + + if show && processingState == .processing { + processor.shouldCancel = true + cancelButton.isHidden = true + progressIndicator.isHidden = true + } + } + + func aggregateTimesByFile(_ functionTimes: [CompileMeasure]) -> [CompileMeasure] { + var fileTimes: [String: CompileMeasure] = [:] + + for measure in functionTimes { + if var fileMeasure = fileTimes[measure.path] { + fileMeasure.time += measure.time + fileTimes[measure.path] = fileMeasure + } else { + let newFileMeasure = CompileMeasure(rawPath: measure.path, time: measure.time) + fileTimes[measure.path] = newFileMeasure + } + } + return Array(fileTimes.values).sorted{ $0.time > $1.time } + } + + func updateViewForState() { + switch processingState { + case .processing: + showInstructions(false) + progressIndicator.isHidden = false + progressIndicator.startAnimation(self) + statusTextField.stringValue = ProcessingState.processingString + cancelButton.isHidden = false + + case .completed(_, let stateName): + progressIndicator.isHidden = true + progressIndicator.stopAnimation(self) + statusTextField.stringValue = stateName + cancelButton.isHidden = true + + case .waiting(): + progressIndicator.isHidden = true + progressIndicator.stopAnimation(self) + statusTextField.stringValue = ProcessingState.waitingForBuildString + cancelButton.isHidden = true + } + + if instructionsView.isHidden { + searchField.isHidden = !cancelButton.isHidden + } + } + + func makeWindowTopMost(topMost: Bool) { + if let window = NSApplication.shared().windows.first { + let level: CGWindowLevelKey = topMost ? .floatingWindow : .normalWindow + window.level = Int(CGWindowLevelForKey(level)) + } + } + + // MARK: Actions + + @IBAction func perFileCheckboxClicked(_ sender: NSButton) { + dataSource = sender.state == 0 ? perFunctionTimes : perFileTimes + tableView.reloadData() + } + + @IBAction func clipboardButtonClicked(_ sender: AnyObject) { + NSPasteboard.general().clearContents() + NSPasteboard.general().writeObjects(["-Xfrontend -debug-time-function-bodies" as NSPasteboardWriting]) + } + + @IBAction func cancelButtonClicked(_ sender: AnyObject) { + processor.shouldCancel = true + } + + @IBAction func leftButtonClicked(_ sender: NSButton) { + configureMenuItems(showBuildTimesMenuItem: true) + + cancelProcessing() + showInstructions(true) + projectSelection.listFolders() + } + + override func controlTextDidChange(_ obj: Notification) { + if let field = obj.object as? NSSearchField, field == searchField { + filteredData = field.stringValue.isEmpty ? nil : dataSource.filter{ textContains($0.code) || textContains($0.filename) } + tableView.reloadData() + } else if let field = obj.object as? NSTextField, field == derivedDataTextField { + buildManager.stopMonitoring() + UserSettings.derivedDataLocation = field.stringValue + + projectSelection.listFolders() + buildManager.startMonitoring() + } + } + + // MARK: Utilities + + func cancelProcessing() { + guard processingState == .processing else { return } + + processor.shouldCancel = true + cancelButton.isHidden = true + } + + func configureMenuItems(showBuildTimesMenuItem: Bool) { + if let appDelegate = NSApp.delegate as? AppDelegate { + appDelegate.configureMenuItems(showBuildTimesMenuItem: showBuildTimesMenuItem) + } + } + + func processLog(with database: XcodeDatabase) { + guard processingState != .processing else { + if let currentKey = currentKey, currentKey != database.key { + nextDatabase = database + processor.shouldCancel = true + } + return + } + + configureMenuItems(showBuildTimesMenuItem: false) + + processingState = .processing + currentKey = database.key + + updateTotalLabel(with: database.buildTime) + + processor.processDatabase(database: database) { [weak self] (result, didComplete, didCancel) in + self?.handleProcessorUpdate(result: result, didComplete: didComplete, didCancel: didCancel) + } + } + + func handleProcessorUpdate(result: [CompileMeasure], didComplete: Bool, didCancel: Bool) { + dataSource = result + perFunctionTimes = result + perFileTimes = aggregateTimesByFile(perFunctionTimes) + tableView.reloadData() + + if didComplete { + completeProcessorUpdate(didCancel: didCancel) + } + } + + func completeProcessorUpdate(didCancel: Bool) { + let didSucceed = !dataSource.isEmpty + + var stateName = ProcessingState.failedString + if didCancel { + stateName = ProcessingState.cancelledString + } else if didSucceed { + stateName = ProcessingState.completedString + } + + processingState = .completed(didSucceed: didSucceed, stateName: stateName) + currentKey = nil + + if let nextDatabase = nextDatabase { + self.nextDatabase = nil + processLog(with: nextDatabase) + } + + if !didSucceed { + let text = "Ensure the Swift compiler flags has been added." + NSAlert.show(withMessage: ProcessingState.failedString, andInformativeText: text) + + showInstructions(true) + configureMenuItems(showBuildTimesMenuItem: true) + } + } + + func updateTotalLabel(with buildTime: Int) { + let text = "Build duration: " + (buildTime < 60 ? "\(buildTime)s" : "\(buildTime / 60)m \(buildTime % 60)s") + compileTimeTextField.stringValue = text + } + + func textContains(_ text: String) -> Bool { + return text.lowercased().contains(searchField.stringValue.lowercased()) + } +} + +// MARK: NSTableViewDataSource + +extension ViewController: NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + return filteredData?.count ?? dataSource.count + } + + func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { + let item = filteredData?[row] ?? dataSource[row] + NSWorkspace.shared().openFile(item.path) + + return true + } +} + +// MARK: NSTableViewDelegate + +extension ViewController: NSTableViewDelegate { + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + guard let tableColumn = tableColumn, let columnIndex = tableView.tableColumns.index(of: tableColumn) else { return nil } + + let result = tableView.make(withIdentifier: "Cell\(columnIndex)", owner: self) as? NSTableCellView + result?.textField?.stringValue = filteredData?[row][columnIndex] ?? dataSource[row][columnIndex] + + return result + } +} + +// MARK: BuildManagerDelegate + +extension ViewController: BuildManagerDelegate { + func buildManager(_ buildManager: BuildManager, shouldParseLogWithDatabase database: XcodeDatabase) { + processLog(with: database) + } + + func derivedDataDidChange() { + projectSelection.listFolders() + } +} + +// MARK: ProjectSelectionDelegate + +extension ViewController: ProjectSelectionDelegate { + func didSelectProject(with database: XcodeDatabase) { + processLog(with: database) + } +} diff --git a/BuildTimeAnalyzer/XcodeDatabase.swift b/BuildTimeAnalyzer/XcodeDatabase.swift new file mode 100644 index 0000000..4e83a11 --- /dev/null +++ b/BuildTimeAnalyzer/XcodeDatabase.swift @@ -0,0 +1,83 @@ +// +// XcodeDatabase.swift +// BuildTimeAnalyzer +// + +import Foundation + +struct XcodeDatabase { + var path: String + var modificationDate: Date + + var key: String + var schemeName: String + var title: String + var timeStartedRecording: Int + var timeStoppedRecording: Int + + var isBuildType: Bool { + return title.hasPrefix("Build ") || title.hasPrefix("Compile ") + } + + var url: URL { + return URL(fileURLWithPath: path) + } + + var logUrl: URL { + return folderPath.appendingPathComponent("\(key).xcactivitylog") + } + + var folderPath: URL { + return url.deletingLastPathComponent() + } + + var buildTime: Int { + return timeStoppedRecording - timeStartedRecording + } + + init?(fromPath path: String) { + guard let data = NSDictionary(contentsOfFile: path)?["logs"] as? [String: AnyObject], + let key = XcodeDatabase.sortKeys(usingData: data).last?.key, + let value = data[key] as? [String : AnyObject], + let schemeName = value["schemeIdentifier-schemeName"] as? String, + let title = value["title"] as? String, + let timeStartedRecording = value["timeStartedRecording"] as? Int, + let timeStoppedRecording = value["timeStoppedRecording"] as? Int, + let fileAttributes = try? FileManager.default.attributesOfItem(atPath: path), + let modificationDate = fileAttributes[FileAttributeKey.modificationDate] as? Date + else { return nil } + + self.modificationDate = modificationDate + self.path = path + self.key = key + self.schemeName = schemeName + self.title = title + self.timeStartedRecording = timeStartedRecording + self.timeStoppedRecording = timeStoppedRecording + } + + func processLog() -> String? { + if let rawData = try? Data(contentsOf: URL(fileURLWithPath: logUrl.path)), + let data = (rawData as NSData).gunzipped() { + return String(data: data, encoding: String.Encoding.utf8) + } + return nil + } + + static private func sortKeys(usingData data: [String: AnyObject]) -> [(Int, key: String)] { + var sortedKeys: [(Int, key: String)] = [] + for key in data.keys { + if let value = data[key] as? [String: AnyObject], + let timeStoppedRecording = value["timeStoppedRecording"] as? Int { + sortedKeys.append((timeStoppedRecording, key)) + } + } + return sortedKeys.sorted{ $0.0 < $1.0 } + } +} + +extension XcodeDatabase : Equatable {} + +func ==(lhs: XcodeDatabase, rhs: XcodeDatabase) -> Bool { + return lhs.path == rhs.path && lhs.modificationDate == rhs.modificationDate +} diff --git a/BuildTimeAnalyzerTests-Bridging-Header.h b/BuildTimeAnalyzerTests-Bridging-Header.h new file mode 100644 index 0000000..299d190 --- /dev/null +++ b/BuildTimeAnalyzerTests-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "NSData+GZIP.h" \ No newline at end of file diff --git a/BuildTimeAnalyzerTests/CMCompileMeasureTests.swift b/BuildTimeAnalyzerTests/CompileMeasureTests.swift similarity index 80% rename from BuildTimeAnalyzerTests/CMCompileMeasureTests.swift rename to BuildTimeAnalyzerTests/CompileMeasureTests.swift index 240c700..77d5700 100644 --- a/BuildTimeAnalyzerTests/CMCompileMeasureTests.swift +++ b/BuildTimeAnalyzerTests/CompileMeasureTests.swift @@ -1,13 +1,12 @@ // -// CMCompileMeasureTests.swift +// CompileMeasureTests.swift // CMBuildTimeAnalyzerTests // -// Created by Robert Gummesson on 02/05/2016. -// Copyright © 2016 Robert Gummesson. All rights reserved. -// import XCTest +@testable import BuildTimeAnalyzer + class BuildTimeAnalyzerTests: XCTestCase { func testInit() { @@ -24,7 +23,7 @@ class BuildTimeAnalyzerTests: XCTestCase { let references = 2 // When - let resultOptional = CMCompileMeasure(time: time, rawPath: rawPath, code: code, references: references) + let resultOptional = CompileMeasure(time: time, rawPath: rawPath, code: code, references: references) // Then XCTAssertNotNil(resultOptional) diff --git a/README.md b/README.md index deec013..72a6a18 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,19 @@ Build Time Analyzer for Xcode ## Overview -Build Time Analyzer is an Xcode plugin 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. +Build Time Analyzer is an OS X 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. Since Xcode 8 came out, the analyzer is a standalone app rather than an Xcode plug-in. ## Usage -Open the analyzer window by tapping `Shift+Ctrl+B` or `View` > `Build Time Analyzer`. From there just follow the instructions. +Open up the app and follow the instructions. -![screenshot.png](https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/master/Screenshots/screenshot2.png) +![screenshot.png](https://raw.githubusercontent.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/master/Screenshots/screenshot.png) ## Installation -**Xcode 7** +Download the code and open it in Xcode, archive the project and export the build. Easy, right? -To install the plug-in, build the project and restart Xcode. The plug-in will automatically be installed in `~/Library/Application Support/Developer/Shared/Xcode/Plug-ins`. To uninstall, just remove the plugin from there (and restart Xcode). - -**Xcode 8** - -With Xcode 8 and above, plug-ins are no longer supported. I am working around this by [turning the project into a standalone app](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/issues/38). This will unfortunately limit the functionality. - -There is still some work and Q.A to be done but if you'd like to access it now, you can [give it a try from here](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/tree/Standalone-for-Xcode-8). Consider it a bit experimental. If you rather wait, the branch should make it to master soon. +If you are using on Xcode 7, instead use the [Xcode plug-in](https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode/releases/tag/v0.0.0-Plugin-for-Xcode7). ## Contributions diff --git a/Screenshots/screenshot.png b/Screenshots/screenshot.png index 10b2441..79f4053 100644 Binary files a/Screenshots/screenshot.png and b/Screenshots/screenshot.png differ diff --git a/Screenshots/screenshot2.png b/Screenshots/screenshot2.png deleted file mode 100644 index 4201b4d..0000000 Binary files a/Screenshots/screenshot2.png and /dev/null differ