From 9973999ea3742717566be1f4daee6fdf4a2c238b 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/Process+Extension.swift | 4 +- .../GenIR/Extensions/String+Extension.swift | 7 + Sources/GenIR/XcodeLogParser.swift | 211 +++++++++--------- 3 files changed, 119 insertions(+), 103 deletions(-) diff --git a/Sources/GenIR/Extensions/Process+Extension.swift b/Sources/GenIR/Extensions/Process+Extension.swift index 425ec99..c1d2c8e 100644 --- a/Sources/GenIR/Extensions/Process+Extension.swift +++ b/Sources/GenIR/Extensions/Process+Extension.swift @@ -118,8 +118,8 @@ extension Process { try? stderrHandle.close() return .init( - stdout: String(decoding: stdout, as: UTF8.self), - stderr: String(decoding: stderr, as: UTF8.self), + stdout: String(data: stdout, encoding: .utf8), + stderr: String(data: stderr, encoding: .utf8), code: process.terminationStatus ) } 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..5da9d26 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) - - 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 } - - settings = lines[index.advanced(by: 1).. String? { + guard offset + 1 < log.endIndex else { return nil } - currentTarget = target - } + defer { offset += 1 } + return log[offset] + } - guard let currentTarget else { - continue + /// Parse build settings key-value pairs + private func parseBuildSettings() -> [String: String] { + var settings = [String: String]() + + // Build settings end with a blank line + while let line = consumeLine()?.trimmed(), !line.isEmpty { + let pair = line.split(separator: "=", maxSplits: 1).map { $0.trimmed() } + if pair.count < 2 { + settings[pair[0]] = "" + } else { + settings[pair[0]] = pair[1] } + } - guard - let compilerCommand = compilerCommand(from: line), - isPartOfCompilerCommand(lines, index) - else { - continue - } + return settings + } + + /// Parse the build description path from the provided line + /// - Parameter from: the line that should contain the build description path + private func buildDescriptionPath(from line: String) -> URL? { + guard line.hasPrefix("Build description path:"), let startIndex = line.firstIndex(of: ":") else { + return nil + } + + var cachePath = String(line[line.index(after: startIndex).. 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 + /// Parsecompiler commands from the current block + private func parseCompilerCommands() -> [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 } - if previousLine.starts(with: "CompileC") - || previousLine.starts(with: "SwiftDriver") - || previousLine.starts(with: "CompileSwiftSources") { - result = true - break + guard let compilerCommand = parseCompilerCommand(from: line) else { + continue } + + commands.append(compilerCommand) + } + + return commands + } + + /// 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 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..