From 99a9d08aaa0a11d6a694638f3a7cd91a5de357ec Mon Sep 17 00:00:00 2001 From: Ali Elsokary Date: Mon, 21 Sep 2026 18:42:12 +0300 Subject: [PATCH 1/2] fix: group untagged build sections under one target instead of many In flattened xcodebuild logs, sections without an (in target ...) tag were appended to the root and reported as targets. Xcode 26 and later emit tens of thousands of such sections (explicit module scans), so the HTML report grew to 1.4GB and the target list and Slowest targets chart were wrong. Group them under a single Build system target. Signed-off-by: Ali Elsokary --- .../IDEActivityLogSection+Parsing.swift | 19 ++---- .../GroupedByTargetTests.swift | 61 +++++++++++++++++++ Tests/XCLogParserTests/XCTestManifests.swift | 11 ++++ 3 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 Tests/XCLogParserTests/GroupedByTargetTests.swift diff --git a/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift b/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift index 189a1eb7..048a1b8a 100644 --- a/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift +++ b/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift @@ -44,27 +44,18 @@ extension IDEActivityLogSection { // for the `(in target 'ABC' from project Project)` string let isFlatten = subSections.contains { $0.getTargetFromCommand() != nil } if isFlatten { - let mainTarget = "$MainTarget" let targetsDictionary = subSections.reduce( [String: IDEActivityLogSection]()) { targets, subSection -> [String: IDEActivityLogSection] in - // some substeps belong to the root project, we use a fixed name for them - let targetName = subSection.getTargetFromCommand() ?? mainTarget + // sections without a target (build description, module scans) are grouped as one + let targetName = subSection.getTargetFromCommand() ?? Self.buildSystemTarget let target = getOrBuildTarget(targetName, in: targets, using: subSection) target.subSections.append(subSection) var updatedTargets = targets updatedTargets[targetName] = target return updatedTargets } - let nonMainTargets = targetsDictionary.filter { (key: String, _) -> Bool in - return key != mainTarget - } - var subSections = [IDEActivityLogSection]() - if let mainTarget = targetsDictionary[mainTarget] { - subSections.append(contentsOf: mainTarget.subSections) - } - subSections.append(contentsOf: nonMainTargets.values) let withTargets = self - withTargets.subSections = subSections.sorted { lhs, rhs -> Bool in + withTargets.subSections = targetsDictionary.values.sorted { lhs, rhs -> Bool in lhs.timeStartedRecording < rhs.timeStartedRecording } return withTargets @@ -136,10 +127,12 @@ extension IDEActivityLogSection { return buildTargetSection(name, with: section) } + static let buildSystemTarget = "Build system" + private func buildTargetSection(_ name: String, with section: IDEActivityLogSection) -> IDEActivityLogSection { return IDEActivityLogSection(sectionType: 2, domainType: section.domainType, - title: "Build target \(name)", + title: name == Self.buildSystemTarget ? name : "Build target \(name)", signature: name, timeStartedRecording: section.timeStartedRecording, timeStoppedRecording: section.timeStoppedRecording, diff --git a/Tests/XCLogParserTests/GroupedByTargetTests.swift b/Tests/XCLogParserTests/GroupedByTargetTests.swift new file mode 100644 index 00000000..250bf087 --- /dev/null +++ b/Tests/XCLogParserTests/GroupedByTargetTests.swift @@ -0,0 +1,61 @@ +import XCTest +@testable import XCLogParser + +class GroupedByTargetTests: XCTestCase { + + func testUntaggedSectionsAreGroupedUnderOneTarget() { + let main = makeSection(title: "Build", subSections: [ + makeSection(title: "Create build description", start: 0), + makeSection(title: "Compiling Clang module Foundation", start: 1), + makeSection(title: "CompileC a.o", command: "CompileC a.o (in target 'Foo' from project 'P')", start: 2), + makeSection(title: "Compiling Clang module UIKit", start: 3), + makeSection(title: "CompileC b.o", command: "CompileC b.o (in target 'Bar' from project 'P')", start: 4), + ]) + + let grouped = main.groupedByTarget() + + XCTAssertEqual(["Build system", "Build target Foo", "Build target Bar"], + grouped.subSections.map { $0.title }) + let buildSystem = grouped.subSections[0] + XCTAssertEqual(["Create build description", + "Compiling Clang module Foundation", + "Compiling Clang module UIKit"], + buildSystem.subSections.map { $0.title }) + } + + func testTargetsKeepTheirOrder() { + let main = makeSection(title: "Build", subSections: [ + makeSection(title: "Ld Bar", command: "Ld Bar (in target 'Bar' from project 'P')", start: 2), + makeSection(title: "CompileC a.o", command: "CompileC a.o (in target 'Foo' from project 'P')", start: 1), + ]) + + XCTAssertEqual(["Build target Foo", "Build target Bar"], + main.groupedByTarget().subSections.map { $0.title }) + } + + private func makeSection(title: String, + command: String = "", + start: Double = 0, + subSections: [IDEActivityLogSection] = []) -> IDEActivityLogSection { + return IDEActivityLogSection(sectionType: 1, + domainType: "", + title: title, + signature: "", + timeStartedRecording: start, + timeStoppedRecording: start, + subSections: subSections, + text: "", + messages: [], + wasCancelled: false, + isQuiet: false, + wasFetchedFromCache: false, + subtitle: "", + location: DVTDocumentLocation(documentURLString: "", timestamp: 0.0), + commandDetailDesc: command, + uniqueIdentifier: "", + localizedResultString: "", + xcbuildSignature: "", + attachments: [], + unknown: 0) + } +} diff --git a/Tests/XCLogParserTests/XCTestManifests.swift b/Tests/XCLogParserTests/XCTestManifests.swift index e18faedd..e59dc5a7 100644 --- a/Tests/XCLogParserTests/XCTestManifests.swift +++ b/Tests/XCLogParserTests/XCTestManifests.swift @@ -28,6 +28,16 @@ extension ChromeTracerOutputTests { ] } +extension GroupedByTargetTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__GroupedByTargetTests = [ + ("testTargetsKeepTheirOrder", testTargetsKeepTheirOrder), + ("testUntaggedSectionsAreGroupedUnderOneTarget", testUntaggedSectionsAreGroupedUnderOneTarget), + ] +} + extension IssuesReporterTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -146,6 +156,7 @@ public func __allTests() -> [XCTestCaseEntry] { return [ testCase(ActivityParserTests.__allTests__ActivityParserTests), testCase(ChromeTracerOutputTests.__allTests__ChromeTracerOutputTests), + testCase(GroupedByTargetTests.__allTests__GroupedByTargetTests), testCase(IssuesReporterTests.__allTests__IssuesReporterTests), testCase(LexRedactorTests.__allTests__LexRedactorTests), testCase(LexerTests.__allTests__LexerTests), From 7a1c622839db9d8a713204adc5fd79153285e64f Mon Sep 17 00:00:00 2001 From: Ali Elsokary Date: Mon, 21 Sep 2026 18:43:44 +0300 Subject: [PATCH 2/2] fix: show the C and Swift file counts on the HTML index page The index page only loads the main step and the targets, so counting detail steps there always gave 0. Write the counts from the reporter instead. Signed-off-by: Ali Elsokary --- Resources/js/app.js | 11 ++----- .../generated/HtmlReporterResources.swift | 11 ++----- .../XCLogParser/reporter/HtmlReporter.swift | 23 +++++++------- .../XCLogParserTests/HtmlReporterTests.swift | 30 +++++++++++++++++++ Tests/XCLogParserTests/XCTestManifests.swift | 10 +++++++ 5 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 Tests/XCLogParserTests/HtmlReporterTests.swift diff --git a/Resources/js/app.js b/Resources/js/app.js index d7c5bf2b..f46a6523 100644 --- a/Resources/js/app.js +++ b/Resources/js/app.js @@ -195,14 +195,9 @@ function loadMainData() { targets = buildData.filter(function (step) { return step.type === 'target' && step.fetchedFromCache === false; }); - cFiles = buildData.filter(function (step) { - return step.type === 'detail' && step.detailStepType === cCompilation - && step.fetchedFromCache === false; - }); - swiftFiles = buildData.filter(function (step) { - return step.type === 'detail' && step.detailStepType === swiftCompilation - && step.fetchedFromCache === false; - }); + // buildData only has the main step and the targets here, the counts come from top_files.js + cFiles = new Array(cFilesCount); + swiftFiles = new Array(swiftFilesCount); } function loadTargetData(target) { diff --git a/Sources/XCLogParser/generated/HtmlReporterResources.swift b/Sources/XCLogParser/generated/HtmlReporterResources.swift index 6610aed0..9849ee7f 100644 --- a/Sources/XCLogParser/generated/HtmlReporterResources.swift +++ b/Sources/XCLogParser/generated/HtmlReporterResources.swift @@ -287,14 +287,9 @@ function loadMainData() { targets = buildData.filter(function (step) { return step.type === 'target' && step.fetchedFromCache === false; }); - cFiles = buildData.filter(function (step) { - return step.type === 'detail' && step.detailStepType === cCompilation - && step.fetchedFromCache === false; - }); - swiftFiles = buildData.filter(function (step) { - return step.type === 'detail' && step.detailStepType === swiftCompilation - && step.fetchedFromCache === false; - }); + // buildData only has the main step and the targets here, the counts come from top_files.js + cFiles = new Array(cFilesCount); + swiftFiles = new Array(swiftFilesCount); } function loadTargetData(target) { diff --git a/Sources/XCLogParser/reporter/HtmlReporter.swift b/Sources/XCLogParser/reporter/HtmlReporter.swift index c3b59250..821ba872 100644 --- a/Sources/XCLogParser/reporter/HtmlReporter.swift +++ b/Sources/XCLogParser/reporter/HtmlReporter.swift @@ -243,17 +243,16 @@ public struct HtmlReporter: LogReporter { swiftTypeCheckTimes.sort { $0.durationMS > $1.durationMS } let topTypeChecks = Array(swiftTypeCheckTimes.prefix(Self.maxSwifTypeChecks)) - try write( - slowestSwiftFiles: Array(swiftSteps.prefix(Self.maxSlowestFiles)), - slowestCFiles: Array(cSteps.prefix(Self.maxSlowestFiles)), - swiftFunctionTimes: topFunctions, - swiftTypeCheckTimes: topTypeChecks, - toDir: buildDir) + try write(swiftSteps: swiftSteps, + cSteps: cSteps, + swiftFunctionTimes: topFunctions, + swiftTypeCheckTimes: topTypeChecks, + toDir: buildDir) } - private func write(slowestSwiftFiles: [BuildStep], - slowestCFiles: [BuildStep], + private func write(swiftSteps: [BuildStep], + cSteps: [BuildStep], swiftFunctionTimes: [SwiftFunctionTime], swiftTypeCheckTimes: [SwiftTypeCheck], toDir buildDir: String) throws { @@ -261,8 +260,8 @@ public struct HtmlReporter: LogReporter { encoder.outputFormatting = .prettyPrinted let functionsJson = try encoder.encode(swiftFunctionTimes) let typeChecksJson = try encoder.encode(swiftTypeCheckTimes) - let swiftJson = try encoder.encode(slowestSwiftFiles) - let cJson = try encoder.encode(slowestCFiles) + let swiftJson = try encoder.encode(Array(swiftSteps.prefix(Self.maxSlowestFiles))) + let cJson = try encoder.encode(Array(cSteps.prefix(Self.maxSlowestFiles))) guard let functionsString = String(data: functionsJson, encoding: .utf8), let typeChecksString = String(data: typeChecksJson, encoding: .utf8), let swiftString = String(data: swiftJson, encoding: .utf8), @@ -278,6 +277,10 @@ public struct HtmlReporter: LogReporter { const topSwiftFunctions = \(functionsString); const topSwifTypeChecks = \(typeChecksString); + + const swiftFilesCount = \(swiftSteps.count); + + const cFilesCount = \(cSteps.count); """.write(toFile: "\(buildDir)/js/top_files.js", atomically: true, encoding: .utf8) } } diff --git a/Tests/XCLogParserTests/HtmlReporterTests.swift b/Tests/XCLogParserTests/HtmlReporterTests.swift new file mode 100644 index 00000000..801da5fb --- /dev/null +++ b/Tests/XCLogParserTests/HtmlReporterTests.swift @@ -0,0 +1,30 @@ +import XCTest +@testable import XCLogParser + +class HtmlReporterTests: XCTestCase { + + func testIndexFileCountsComeFromAllTargets() throws { + let compile = { (title: String, type: DetailStepType) -> BuildStep in + makeFakeBuildStep(title: title, type: .detail, detailStepType: type, + startTimestamp: 0, fetchedFromCache: false) + } + let foo = makeFakeBuildStep(title: "Build target Foo", type: .target, detailStepType: .none, + startTimestamp: 0, fetchedFromCache: false) + .with(subSteps: [compile("Compile a.swift", .swiftCompilation), + compile("Compile b.swift", .swiftCompilation), + compile("Compile c.c", .cCompilation)]) + let bar = makeFakeBuildStep(title: "Build target Bar", type: .target, detailStepType: .none, + startTimestamp: 0, fetchedFromCache: false) + .with(subSteps: [compile("Compile d.swift", .swiftCompilation)]) + let build = makeFakeBuildStep(title: "Build", type: .main, detailStepType: .none, + startTimestamp: 0, fetchedFromCache: false) + .with(subSteps: [foo, bar]) + let dir = try TestUtils.createRandomTestDir() + + try HtmlReporter().report(build: build, output: FileOutput(path: dir.path), rootOutput: dir.path) + + let topFiles = try String(contentsOf: dir.appendingPathComponent("js/top_files.js")) + XCTAssertTrue(topFiles.contains("const swiftFilesCount = 3;")) + XCTAssertTrue(topFiles.contains("const cFilesCount = 1;")) + } +} diff --git a/Tests/XCLogParserTests/XCTestManifests.swift b/Tests/XCLogParserTests/XCTestManifests.swift index e59dc5a7..deaad404 100644 --- a/Tests/XCLogParserTests/XCTestManifests.swift +++ b/Tests/XCLogParserTests/XCTestManifests.swift @@ -38,6 +38,15 @@ extension GroupedByTargetTests { ] } +extension HtmlReporterTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__HtmlReporterTests = [ + ("testIndexFileCountsComeFromAllTargets", testIndexFileCountsComeFromAllTargets), + ] +} + extension IssuesReporterTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -157,6 +166,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(ActivityParserTests.__allTests__ActivityParserTests), testCase(ChromeTracerOutputTests.__allTests__ChromeTracerOutputTests), testCase(GroupedByTargetTests.__allTests__GroupedByTargetTests), + testCase(HtmlReporterTests.__allTests__HtmlReporterTests), testCase(IssuesReporterTests.__allTests__IssuesReporterTests), testCase(LexRedactorTests.__allTests__LexRedactorTests), testCase(LexerTests.__allTests__LexerTests),