From 95f59a6972ff2144b8deff8f617aeaf76e876c7a Mon Sep 17 00:00:00 2001 From: bmxav <5422569+bmxav@users.noreply.github.com> Date: Thu, 5 Sep 2024 20:16:33 -0400 Subject: [PATCH] XcodeLogParser performance improvements --- .../GenIR/Extensions/String+Extension.swift | 7 + Sources/GenIR/XcodeLogParser.swift | 217 +++++++++--------- 2 files changed, 120 insertions(+), 104 deletions(-) diff --git a/Sources/GenIR/Extensions/String+Extension.swift b/Sources/GenIR/Extensions/String+Extension.swift index 4f6df5b..bbd51b3 100644 --- a/Sources/GenIR/Extensions/String+Extension.swift +++ b/Sources/GenIR/Extensions/String+Extension.swift @@ -118,3 +118,10 @@ extension [String] { return nil } } + +extension StringProtocol { + /// Trims leading and trailing whitespace characters + func trimmed() -> String { + return trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/Sources/GenIR/XcodeLogParser.swift b/Sources/GenIR/XcodeLogParser.swift index 8b81dcb..dcdc64b 100644 --- a/Sources/GenIR/XcodeLogParser.swift +++ b/Sources/GenIR/XcodeLogParser.swift @@ -10,10 +10,11 @@ import LogHandlers /// An XcodeLogParser extracts targets and their compiler commands from a given Xcode build log class XcodeLogParser { - /// The Xcode build log contents private let log: [String] - /// Any CLI Settings found in the build log + /// The current line offset in the log + private var offset: Int = 0 + /// Any CLI settings found in the build log private(set) var settings: [String: String] = [:] /// The path to the Xcode build cache private(set) var buildCachePath: URL! @@ -33,9 +34,8 @@ class XcodeLogParser { } /// Start parsing the build log - /// - Parameter targets: The global list of targets func parse() throws { - parseBuildLog(log) + parseBuildLog() if targetCommands.isEmpty { logger.debug("Found no targets in log") @@ -67,106 +67,137 @@ class XcodeLogParser { } } - /// Parses an array representing the contents of an Xcode build log - /// - Parameters: - /// - lines: contents of the Xcode build log lines - // swiftlint:disable:next cyclomatic_complexity - private func parseBuildLog(_ lines: [String]) { - var currentTarget: String? + /// Parse the lines from the build log + func parseBuildLog() { var seenTargets = Set() - for (index, line) in lines.enumerated() { - let line = line.trimmingCharacters(in: .whitespacesAndNewlines) + while let line = consumeLine() { + if line.hasPrefix("Build description path: ") { + buildCachePath = buildDescriptionPath(from: line) + } else if line.hasPrefix("Build settings from command line:") { + settings = parseBuildSettings() + } else { + // Attempt to find a build task on this line that we are interested in. + let task = line.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: false)[0] + + switch task { + case "CompileC", "SwiftDriver", "CompileSwiftSources": + guard let target = target(from: line) else { + continue + } + + if seenTargets.insert(target).inserted { + logger.debug("Found target: \(target)") + } + + let compilerCommands = parseCompilerCommands() + + compilerCommands.forEach { + logger.debug("Found \($0.compiler.rawValue) compiler command for target: \(target)") + } + + targetCommands[target, default: []].append(contentsOf: compilerCommands) + default: + continue + } + } + } + } - if line.contains("Build settings from command line") { - // Every line until an empty line will contain a build setting from the CLI arguments - guard let nextEmptyLine = lines.nextIndex(of: "", after: index) else { continue } + /// Consume the next line from the log file and return it if we have not reached the end + private func consumeLine() -> String? { + guard offset + 1 < log.endIndex else { return nil } - settings = lines[index.advanced(by: 1).. URL? { + guard line.hasPrefix("Build description path:"), let startIndex = line.firstIndex(of: ":") else { + return nil + } - if let target = target(from: line), currentTarget != target { - if seenTargets.insert(target).inserted { - logger.debug("Found target: \(target)") - } + var cachePath = String(line[line.index(after: startIndex).. [CompilerCommand] { + var commands: [CompilerCommand] = [] + + while let line = consumeLine() { + // Assume we have reached the end of this build task's block when we encounter an unindented line. + guard line.hasPrefix(" ") else { + break } - guard - let compilerCommand = compilerCommand(from: line), - isPartOfCompilerCommand(lines, index) - else { + guard let compilerCommand = parseCompilerCommand(from: line) else { continue } - logger.debug("Found \(compilerCommand.compiler.rawValue) compiler command for target: \(currentTarget)") - - targetCommands[currentTarget, default: [CompilerCommand]()].append(compilerCommand) + commands.append(compilerCommand) } + + return commands } - /// Is the index provided part of a compiler command block - /// - Parameters: - /// - lines: all the lines in the build log - /// - index: the index of the line to search from - /// - Returns: true if it's determined that the index is part of compiler command block - private func isPartOfCompilerCommand(_ lines: [String], _ index: Int) -> Bool { - var result = false - var offset = lines.index(index, offsetBy: -2) - - // Check the line starts with either 'CompileC', 'SwiftDriver', or 'CompileSwiftSources' to ensure we only pick up compilation commands - while lines.indices.contains(offset) { - let previousLine = lines[offset].trimmingCharacters(in: .whitespacesAndNewlines) - offset -= 1 - - if previousLine.isEmpty { - // hit the top of the block, exit loop - break - } + /// Parses a `CompilerCommand` from the given line if one exists + /// - Parameter from: the line which may contain a compiler command + private func parseCompilerCommand(from line: String) -> CompilerCommand? { + var commandLine = line - if previousLine.starts(with: "CompileC") - || previousLine.starts(with: "SwiftDriver") - || previousLine.starts(with: "CompileSwiftSources") { - result = true - break - } + if let index = line.firstIndexWithEscapes(of: "/"), index != line.startIndex { + commandLine = String(line[index.. CompilerCommand? { - var stripped = line - if let index = stripped.firstIndexWithEscapes(of: "/"), index != stripped.startIndex { - stripped = String(stripped[index..