From 49247af11458c47ed2681771aa04e0a08396dbc2 Mon Sep 17 00:00:00 2001 From: Konstantin Iurrichev Date: Thu, 17 Oct 2024 20:47:17 +0300 Subject: [PATCH 01/19] Encode log data to ascii string using c string pointer Signed-off-by: Konstantin Iurrichev --- Sources/XCLogParser/loglocation/LogLoader.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Sources/XCLogParser/loglocation/LogLoader.swift b/Sources/XCLogParser/loglocation/LogLoader.swift index 4a39b88..3d8e946 100644 --- a/Sources/XCLogParser/loglocation/LogLoader.swift +++ b/Sources/XCLogParser/loglocation/LogLoader.swift @@ -26,7 +26,17 @@ public struct LogLoader { do { let data = try Data(contentsOf: url) let unzipped = try data.gunzipped() - guard let contents = String(data: unzipped, encoding: .ascii) else { + let string: String? = unzipped.withUnsafeBytes { pointer in + guard let charPointer = pointer + .assumingMemoryBound(to: CChar.self) + .baseAddress + else { + return nil + } + + return String(cString: charPointer, encoding: .ascii) + } + guard let contents = string else { throw LogError.readingFile(url.path) } return contents From c727646101ceaa44fa9e202d2451e1b98b8b834f Mon Sep 17 00:00:00 2001 From: Jose Ramos Date: Fri, 18 Oct 2024 16:24:19 +0100 Subject: [PATCH 02/19] Replace scanner with substrings Signed-off-by: Jose Ramos --- Package.resolved | 4 +- Sources/XCLogParser/lexer/Lexer.swift | 157 ++++++++---------- .../XCLogParser/lexer/Substring+Scanner.swift | 57 +++++++ 3 files changed, 129 insertions(+), 89 deletions(-) create mode 100644 Sources/XCLogParser/lexer/Substring+Scanner.swift diff --git a/Package.resolved b/Package.resolved index 900fb44..37e6802 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/1024jp/GzipSwift", "state": { "branch": null, - "revision": "ba0b6cb51cc6202f896e469b87d2889a46b10d1b", - "version": "5.1.1" + "revision": "7a7f17761c76a932662ab77028a4329f67d645a4", + "version": "5.2.0" } }, { diff --git a/Sources/XCLogParser/lexer/Lexer.swift b/Sources/XCLogParser/lexer/Lexer.swift index 03ad44b..9d3d506 100644 --- a/Sources/XCLogParser/lexer/Lexer.swift +++ b/Sources/XCLogParser/lexer/Lexer.swift @@ -23,7 +23,7 @@ public final class Lexer { static let SLFHeader = "SLF" - let typeDelimiters: CharacterSet + let typeDelimiters: Set let filePath: String var classNames = [String]() var userDirToRedact: String? { @@ -38,7 +38,7 @@ public final class Lexer { public init(filePath: String) { self.filePath = filePath - self.typeDelimiters = CharacterSet(charactersIn: TokenType.all()) + self.typeDelimiters = Set(TokenType.all()) self.redactor = LexRedactor() } @@ -52,44 +52,45 @@ public final class Lexer { public func tokenize(contents: String, redacted: Bool, withoutBuildSpecificInformation: Bool) throws -> [Token] { - let scanner = Scanner(string: contents) - guard scanSLFHeader(scanner: scanner) else { + var contentSequence = contents[...] + let contentsCount = contents.endIndex + + guard scanSLFHeader(contentSequence: &contentSequence) else { throw XCLogParserError.invalidLogHeader(filePath) } + var tokens = [Token]() - while !scanner.isAtEnd { - guard let logTokens = scanSLFType(scanner: scanner, + while contentSequence.startIndex < contentsCount { + + guard let logTokens = scanSLFType(contentSequence: &contentSequence, + content: contents, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation), - logTokens.isEmpty == false else { + logTokens.isEmpty == false else { print(tokens) - throw XCLogParserError.invalidLine(scanner.approximateLine) + throw XCLogParserError.invalidLine(contentSequence.makeApproximateLine(in: contents)) } tokens.append(contentsOf: logTokens) } return tokens } - private func scanSLFHeader(scanner: Scanner) -> Bool { - #if os(Linux) - var format: String? - #else - var format: NSString? - #endif - return scanner.scanString(Lexer.SLFHeader, into: &format) + private func scanSLFHeader(contentSequence: inout String.SubSequence) -> Bool { + return contentSequence.scan(prefix: Lexer.SLFHeader) } - private func scanSLFType(scanner: Scanner, redacted: Bool, withoutBuildSpecificInformation: Bool) -> [Token]? { + private func scanSLFType(contentSequence: inout String.SubSequence, + content: String, + redacted: Bool, + withoutBuildSpecificInformation: Bool) -> [Token]? { + let payload = self.scanPayload(contentSequence: &contentSequence) - guard let payload = scanPayload(scanner: scanner) else { - return nil - } - guard let tokenTypes = scanTypeDelimiter(scanner: scanner), tokenTypes.count > 0 else { + guard let tokenTypes = self.scanTypeDelimiter(contentSequence: &contentSequence, content: content), tokenTypes.count > 0 else { return nil } return tokenTypes.compactMap { tokenType -> Token? in - scanToken(scanner: scanner, + scanToken(contentSequence: &contentSequence, payload: payload, tokenType: tokenType, redacted: redacted, @@ -97,47 +98,35 @@ public final class Lexer { } } - private func scanPayload(scanner: Scanner) -> String? { - var payload: String = "" - #if os(Linux) - var char: String? - #else - var char: NSString? - #endif + private func scanPayload(contentSequence: inout String.SubSequence) -> String { let hexChars = "abcdef0123456789" - while scanner.scanCharacters(from: CharacterSet(charactersIn: hexChars), into: &char), - let char = char as String? { - payload.append(char) - } - return payload + let characterSet = Set(hexChars) + return contentSequence.scanCharacters(in: characterSet) ?? "" } - private func scanTypeDelimiter(scanner: Scanner) -> [TokenType]? { - #if os(Linux) - var delimiters: String? - #else - var delimiters: NSString? - #endif - if scanner.scanCharacters(from: typeDelimiters, into: &delimiters), let delimiters = delimiters { - let delimiters = String(delimiters) - if delimiters.count > 1 { - // if we found a string, we discard other type delimiters because there are part of the string - let tokenString = TokenType.string - if let char = delimiters.first, tokenString.rawValue == String(char) { - scanner.scanLocation -= delimiters.count - 1 - return [tokenString] - } - } - // sometimes we found one or more nil list (-) next to the type delimiter - // in that case we'll return the delimiter and one or more `Token.null` - return delimiters.compactMap { character -> TokenType? in - TokenType(rawValue: String(character)) + private func scanTypeDelimiter(contentSequence: inout String.SubSequence, + content: String) -> [TokenType]? { + guard let delimiters = contentSequence.scanCharacters(in: Set(self.typeDelimiters)) else { + return nil + } + + if delimiters.count > 1 { + // if we found a string, we discard other type delimiters because there are part of the string + let tokenString = TokenType.string + + if let char = delimiters.first, tokenString.rawValue == String(char) { + contentSequence.moveStartIndex(offset: -(delimiters.count - 1), originalString: content) + return [tokenString] } } - return nil + // sometimes we found one or more nil list (-) next to the type delimiter + // in that case we'll return the delimiter and one or more `Token.null` + return delimiters.compactMap { character -> TokenType? in + TokenType(rawValue: String(character)) + } } - private func scanToken(scanner: Scanner, + private func scanToken(contentSequence: inout String.SubSequence, payload: String, tokenType: TokenType, redacted: Bool, @@ -146,14 +135,14 @@ public final class Lexer { case .int: return handleIntTokenTypeCase(payload: payload) case .className: - return handleClassNameTokenTypeCase(scanner: scanner, + return handleClassNameTokenTypeCase(contentSequence: &contentSequence, payload: payload, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) case .classNameRef: return handleClassNameRefTokenTypeCase(payload: payload) case .string: - return handleStringTokenTypeCase(scanner: scanner, + return handleStringTokenTypeCase(contentSequence: &contentSequence, payload: payload, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) @@ -164,7 +153,7 @@ public final class Lexer { case .list: return handleListTokenTypeCase(payload: payload) case .json: - return handleJSONTokenTypeCase(scanner: scanner, + return handleJSONTokenTypeCase(contentSequence: &contentSequence, payload: payload, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) @@ -173,18 +162,17 @@ public final class Lexer { private func handleIntTokenTypeCase(payload: String) -> Token? { guard let value = UInt64(payload) else { - print("error parsing int") return nil } return .int(value) } - private func handleClassNameTokenTypeCase(scanner: Scanner, + private func handleClassNameTokenTypeCase(contentSequence: inout String.SubSequence, payload: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> Token? { guard let className = scanString(length: payload, - scanner: scanner, + contentSequence: &contentSequence, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) else { print("error parsing string") @@ -204,12 +192,12 @@ public final class Lexer { return .classNameRef(className) } - private func handleStringTokenTypeCase(scanner: Scanner, + private func handleStringTokenTypeCase(contentSequence: inout String.SubSequence, payload: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> Token? { guard let content = scanString(length: payload, - scanner: scanner, + contentSequence: &contentSequence, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) else { print("error parsing string") @@ -218,12 +206,12 @@ public final class Lexer { return .string(content) } - private func handleJSONTokenTypeCase(scanner: Scanner, + private func handleJSONTokenTypeCase(contentSequence: inout String.SubSequence, payload: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> Token? { guard let content = scanString(length: payload, - scanner: scanner, + contentSequence: &contentSequence, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) else { print("error parsing string") @@ -249,22 +237,15 @@ public final class Lexer { } private func scanString(length: String, - scanner: Scanner, + contentSequence: inout String.SubSequence, redacted: Bool, withoutBuildSpecificInformation: Bool) -> String? { - guard let value = Int(length) else { + guard let value = Int(length), let scannedResult = contentSequence.scan(count: value) else { print("error parsing string") return nil } - #if swift(>=5.0) - let start = String.Index(utf16Offset: scanner.scanLocation, in: scanner.string) - let end = String.Index(utf16Offset: scanner.scanLocation + value, in: scanner.string) - #else - let start = String.Index(encodedOffset: scanner.scanLocation) - let end = String.Index(encodedOffset: scanner.scanLocation + value) - #endif - scanner.scanLocation += value - var result = String(scanner.string[start.. 21 ? scanLocation + 21 : string.count - scanLocation - #if swift(>=5.0) - let start = String.Index(utf16Offset: scanLocation, in: self.string) - let end = String.Index(utf16Offset: endCount, in: self.string) - #else - let start = String.Index(encodedOffset: scanLocation) +private extension String.SubSequence { + func makeApproximateLine(in content: String) -> String { + let currentLocation = content.distance(from: content.startIndex, to: self.startIndex) + let contentSize = content.count + + let endCount = contentSize - currentLocation > 21 ? currentLocation + 21 : contentSize - currentLocation + let start = self.startIndex +#if swift(>=5.0) + let end = String.Index(utf16Offset: endCount, in: content) +#else let end = String.Index(encodedOffset: endCount) - #endif +#endif if end <= start { - return String(string[start.. Substring? { + let result = self.prefix(count) + guard result.count == count else { return nil } + + self.removeFirst(count) + return result + } + + mutating func scan(prefix: C) -> Bool where C: Collection, C.Element == Character { + guard self.starts(with: prefix) else { return false } + + self.removeFirst(prefix.count) + return true + } + + mutating func scanCharacters(in allowedCharacters: Set) -> String? { + var prefix: String = "" + + for character in self { + if allowedCharacters.contains(character) { + prefix.append(character) + } else { + break + } + } + self.removeFirst(prefix.count) + return prefix + } + + mutating func moveStartIndex(offset: Int, originalString: String) { + let newOriginalIndex = originalString.index(self.startIndex, offsetBy: offset) + self = originalString[newOriginalIndex...self.endIndex] + } +} From 100992bdf6fca7decd80702182e9ab67e5b6bd17 Mon Sep 17 00:00:00 2001 From: Jose Ramos Date: Fri, 18 Oct 2024 17:26:19 +0100 Subject: [PATCH 03/19] Use concrete type Signed-off-by: Jose Ramos --- Sources/XCLogParser/lexer/Lexer.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/XCLogParser/lexer/Lexer.swift b/Sources/XCLogParser/lexer/Lexer.swift index 9d3d506..ec06866 100644 --- a/Sources/XCLogParser/lexer/Lexer.swift +++ b/Sources/XCLogParser/lexer/Lexer.swift @@ -75,11 +75,11 @@ public final class Lexer { return tokens } - private func scanSLFHeader(contentSequence: inout String.SubSequence) -> Bool { + private func scanSLFHeader(contentSequence: inout Substring) -> Bool { return contentSequence.scan(prefix: Lexer.SLFHeader) } - private func scanSLFType(contentSequence: inout String.SubSequence, + private func scanSLFType(contentSequence: inout Substring, content: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> [Token]? { @@ -98,13 +98,13 @@ public final class Lexer { } } - private func scanPayload(contentSequence: inout String.SubSequence) -> String { + private func scanPayload(contentSequence: inout Substring) -> String { let hexChars = "abcdef0123456789" let characterSet = Set(hexChars) return contentSequence.scanCharacters(in: characterSet) ?? "" } - private func scanTypeDelimiter(contentSequence: inout String.SubSequence, + private func scanTypeDelimiter(contentSequence: inout Substring, content: String) -> [TokenType]? { guard let delimiters = contentSequence.scanCharacters(in: Set(self.typeDelimiters)) else { return nil @@ -167,7 +167,7 @@ public final class Lexer { return .int(value) } - private func handleClassNameTokenTypeCase(contentSequence: inout String.SubSequence, + private func handleClassNameTokenTypeCase(contentSequence: inout Substring, payload: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> Token? { @@ -192,7 +192,7 @@ public final class Lexer { return .classNameRef(className) } - private func handleStringTokenTypeCase(contentSequence: inout String.SubSequence, + private func handleStringTokenTypeCase(contentSequence: inout Substring, payload: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> Token? { @@ -206,7 +206,7 @@ public final class Lexer { return .string(content) } - private func handleJSONTokenTypeCase(contentSequence: inout String.SubSequence, + private func handleJSONTokenTypeCase(contentSequence: inout Substring, payload: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> Token? { @@ -237,7 +237,7 @@ public final class Lexer { } private func scanString(length: String, - contentSequence: inout String.SubSequence, + contentSequence: inout Substring, redacted: Bool, withoutBuildSpecificInformation: Bool) -> String? { guard let value = Int(length), let scannedResult = contentSequence.scan(count: value) else { @@ -266,7 +266,7 @@ public final class Lexer { } } -private extension String.SubSequence { +private extension Substring { func makeApproximateLine(in content: String) -> String { let currentLocation = content.distance(from: content.startIndex, to: self.startIndex) let contentSize = content.count From 963e043aa6b06e58ec30854923bc36ff97149716 Mon Sep 17 00:00:00 2001 From: Jose Ramos Date: Sun, 20 Oct 2024 14:35:14 +0100 Subject: [PATCH 04/19] Introduce scanner to improve performance Signed-off-by: Jose Ramos --- Sources/XCLogParser/lexer/Index+Offset.swift | 18 +++++ Sources/XCLogParser/lexer/Lexer.swift | 81 +++++++++---------- Sources/XCLogParser/lexer/Scanner.swift | 80 ++++++++++++++++++ .../XCLogParser/lexer/Substring+Scanner.swift | 57 ------------- 4 files changed, 137 insertions(+), 99 deletions(-) create mode 100644 Sources/XCLogParser/lexer/Index+Offset.swift create mode 100644 Sources/XCLogParser/lexer/Scanner.swift delete mode 100644 Sources/XCLogParser/lexer/Substring+Scanner.swift diff --git a/Sources/XCLogParser/lexer/Index+Offset.swift b/Sources/XCLogParser/lexer/Index+Offset.swift new file mode 100644 index 0000000..5782f3e --- /dev/null +++ b/Sources/XCLogParser/lexer/Index+Offset.swift @@ -0,0 +1,18 @@ +// +// File.swift +// +// +// Created by Jose Correia Miranda Ramos on 20/10/2024. +// + +import Foundation + +extension String.Index { + init(compilerSafeOffset offset: Int, in string: String) { +#if swift(>=5.0) + self = String.Index(utf16Offset: offset, in: string) +#else + self = String.Index(encodedOffset: offset) +#endif + } +} diff --git a/Sources/XCLogParser/lexer/Lexer.swift b/Sources/XCLogParser/lexer/Lexer.swift index ec06866..3341b84 100644 --- a/Sources/XCLogParser/lexer/Lexer.swift +++ b/Sources/XCLogParser/lexer/Lexer.swift @@ -52,45 +52,44 @@ public final class Lexer { public func tokenize(contents: String, redacted: Bool, withoutBuildSpecificInformation: Bool) throws -> [Token] { - var contentSequence = contents[...] - let contentsCount = contents.endIndex + let scanner = Scanner(string: contents) - guard scanSLFHeader(contentSequence: &contentSequence) else { + guard scanSLFHeader(scanner: scanner) else { throw XCLogParserError.invalidLogHeader(filePath) } var tokens = [Token]() - while contentSequence.startIndex < contentsCount { + while scanner.isAtEnd { - guard let logTokens = scanSLFType(contentSequence: &contentSequence, + guard let logTokens = scanSLFType(scanner: scanner, content: contents, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation), logTokens.isEmpty == false else { print(tokens) - throw XCLogParserError.invalidLine(contentSequence.makeApproximateLine(in: contents)) + throw XCLogParserError.invalidLine(scanner.makeApproximateLine) } tokens.append(contentsOf: logTokens) } return tokens } - private func scanSLFHeader(contentSequence: inout Substring) -> Bool { - return contentSequence.scan(prefix: Lexer.SLFHeader) + private func scanSLFHeader(scanner: Scanner) -> Bool { + return scanner.scan(prefix: Lexer.SLFHeader) } - private func scanSLFType(contentSequence: inout Substring, + private func scanSLFType(scanner: Scanner, content: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> [Token]? { - let payload = self.scanPayload(contentSequence: &contentSequence) + let payload = self.scanPayload(scanner: scanner) - guard let tokenTypes = self.scanTypeDelimiter(contentSequence: &contentSequence, content: content), tokenTypes.count > 0 else { + guard let tokenTypes = self.scanTypeDelimiter(scanner: scanner, content: content), tokenTypes.count > 0 else { return nil } return tokenTypes.compactMap { tokenType -> Token? in - scanToken(contentSequence: &contentSequence, + scanToken(scanner: scanner, payload: payload, tokenType: tokenType, redacted: redacted, @@ -98,15 +97,15 @@ public final class Lexer { } } - private func scanPayload(contentSequence: inout Substring) -> String { + private func scanPayload(scanner: Scanner) -> String { let hexChars = "abcdef0123456789" let characterSet = Set(hexChars) - return contentSequence.scanCharacters(in: characterSet) ?? "" + return scanner.scanCharacters(in: characterSet) ?? "" } - private func scanTypeDelimiter(contentSequence: inout Substring, + private func scanTypeDelimiter(scanner: Scanner, content: String) -> [TokenType]? { - guard let delimiters = contentSequence.scanCharacters(in: Set(self.typeDelimiters)) else { + guard let delimiters = scanner.scanCharacters(in: Set(self.typeDelimiters)) else { return nil } @@ -115,7 +114,7 @@ public final class Lexer { let tokenString = TokenType.string if let char = delimiters.first, tokenString.rawValue == String(char) { - contentSequence.moveStartIndex(offset: -(delimiters.count - 1), originalString: content) + scanner.moveStartIndex(offset: -(delimiters.count - 1), originalString: content) return [tokenString] } } @@ -126,7 +125,7 @@ public final class Lexer { } } - private func scanToken(contentSequence: inout String.SubSequence, + private func scanToken(scanner: Scanner, payload: String, tokenType: TokenType, redacted: Bool, @@ -135,14 +134,14 @@ public final class Lexer { case .int: return handleIntTokenTypeCase(payload: payload) case .className: - return handleClassNameTokenTypeCase(contentSequence: &contentSequence, + return handleClassNameTokenTypeCase(scanner: scanner, payload: payload, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) case .classNameRef: return handleClassNameRefTokenTypeCase(payload: payload) case .string: - return handleStringTokenTypeCase(contentSequence: &contentSequence, + return handleStringTokenTypeCase(scanner: scanner, payload: payload, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) @@ -153,7 +152,7 @@ public final class Lexer { case .list: return handleListTokenTypeCase(payload: payload) case .json: - return handleJSONTokenTypeCase(contentSequence: &contentSequence, + return handleJSONTokenTypeCase(scanner: scanner, payload: payload, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) @@ -167,12 +166,12 @@ public final class Lexer { return .int(value) } - private func handleClassNameTokenTypeCase(contentSequence: inout Substring, + private func handleClassNameTokenTypeCase(scanner: Scanner, payload: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> Token? { guard let className = scanString(length: payload, - contentSequence: &contentSequence, + scanner: scanner, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) else { print("error parsing string") @@ -192,12 +191,12 @@ public final class Lexer { return .classNameRef(className) } - private func handleStringTokenTypeCase(contentSequence: inout Substring, + private func handleStringTokenTypeCase(scanner: Scanner, payload: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> Token? { guard let content = scanString(length: payload, - contentSequence: &contentSequence, + scanner: scanner, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) else { print("error parsing string") @@ -206,12 +205,12 @@ public final class Lexer { return .string(content) } - private func handleJSONTokenTypeCase(contentSequence: inout Substring, + private func handleJSONTokenTypeCase(scanner: Scanner, payload: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> Token? { guard let content = scanString(length: payload, - contentSequence: &contentSequence, + scanner: scanner, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation) else { print("error parsing string") @@ -237,15 +236,16 @@ public final class Lexer { } private func scanString(length: String, - contentSequence: inout Substring, + scanner: Scanner, redacted: Bool, withoutBuildSpecificInformation: Bool) -> String? { - guard let value = Int(length), let scannedResult = contentSequence.scan(count: value) else { + let value = (length as NSString).integerValue + guard let scannedResult = scanner.scan(count: value) else { print("error parsing string") return nil } - var result = String(scannedResult) + var result = scannedResult if redacted { result = redactor.redactUserDir(string: result) } @@ -266,21 +266,18 @@ public final class Lexer { } } -private extension Substring { - func makeApproximateLine(in content: String) -> String { - let currentLocation = content.distance(from: content.startIndex, to: self.startIndex) - let contentSize = content.count +private extension Scanner { + var makeApproximateLine: String { + let currentLocation = self.offset + let contentSize = self.string.count + let start = String.Index(compilerSafeOffset: currentLocation, in: self.string) let endCount = contentSize - currentLocation > 21 ? currentLocation + 21 : contentSize - currentLocation - let start = self.startIndex -#if swift(>=5.0) - let end = String.Index(utf16Offset: endCount, in: content) -#else - let end = String.Index(encodedOffset: endCount) -#endif + let end = String.Index(compilerSafeOffset: endCount, in: self.string) + if end <= start { - return String(content[start.. String? { + let start = String.Index(compilerSafeOffset: self.offset, in: self.string) + let end = String.Index(compilerSafeOffset: self.offset + count, in: self.string) + + var result = self.string.substring(with: (start.. Bool { + guard self.string.starts(with: prefix) else { return false } + + self.offset = self.offset + prefix.count + return true + } + + func scanCharacters(in allowedCharacters: Set) -> String? { + var prefix: String = "" + var characterIndex = String.Index(compilerSafeOffset: self.offset, in: self.string) + + while characterIndex < self.stringEndIndex { + let character = self.string[characterIndex] + + guard allowedCharacters.contains(character) else { + break + } + + prefix.append(character) + self.offset = self.offset + 1 + characterIndex = String.Index(utf16Offset: self.offset, in: self.string) + } + + return prefix + } + + func moveStartIndex(offset: Int, originalString: String) { + self.offset = self.offset + offset + } +} diff --git a/Sources/XCLogParser/lexer/Substring+Scanner.swift b/Sources/XCLogParser/lexer/Substring+Scanner.swift deleted file mode 100644 index 80a35b3..0000000 --- a/Sources/XCLogParser/lexer/Substring+Scanner.swift +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2019 Spotify AB. -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import Foundation - -extension Substring { - - mutating func scan(count: Int) -> Substring? { - let result = self.prefix(count) - guard result.count == count else { return nil } - - self.removeFirst(count) - return result - } - - mutating func scan(prefix: C) -> Bool where C: Collection, C.Element == Character { - guard self.starts(with: prefix) else { return false } - - self.removeFirst(prefix.count) - return true - } - - mutating func scanCharacters(in allowedCharacters: Set) -> String? { - var prefix: String = "" - - for character in self { - if allowedCharacters.contains(character) { - prefix.append(character) - } else { - break - } - } - self.removeFirst(prefix.count) - return prefix - } - - mutating func moveStartIndex(offset: Int, originalString: String) { - let newOriginalIndex = originalString.index(self.startIndex, offsetBy: offset) - self = originalString[newOriginalIndex...self.endIndex] - } -} From f24bc46e864025e948d14d6f749ca21a8aba4278 Mon Sep 17 00:00:00 2001 From: Jose Ramos Date: Sun, 20 Oct 2024 15:23:40 +0100 Subject: [PATCH 05/19] Improve code styling Signed-off-by: Jose Ramos --- Package.resolved | 4 ++-- Sources/XCLogParser/lexer/Index+Offset.swift | 18 +++++++++++--- Sources/XCLogParser/lexer/Lexer.swift | 25 ++++++++------------ Sources/XCLogParser/lexer/Scanner.swift | 14 +++++------ 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/Package.resolved b/Package.resolved index 37e6802..900fb44 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/1024jp/GzipSwift", "state": { "branch": null, - "revision": "7a7f17761c76a932662ab77028a4329f67d645a4", - "version": "5.2.0" + "revision": "ba0b6cb51cc6202f896e469b87d2889a46b10d1b", + "version": "5.1.1" } }, { diff --git a/Sources/XCLogParser/lexer/Index+Offset.swift b/Sources/XCLogParser/lexer/Index+Offset.swift index 5782f3e..41daca8 100644 --- a/Sources/XCLogParser/lexer/Index+Offset.swift +++ b/Sources/XCLogParser/lexer/Index+Offset.swift @@ -1,9 +1,21 @@ +// Copyright (c) 2019 Spotify AB. // -// File.swift -// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at // -// Created by Jose Correia Miranda Ramos on 20/10/2024. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. import Foundation diff --git a/Sources/XCLogParser/lexer/Lexer.swift b/Sources/XCLogParser/lexer/Lexer.swift index 3341b84..b84cd40 100644 --- a/Sources/XCLogParser/lexer/Lexer.swift +++ b/Sources/XCLogParser/lexer/Lexer.swift @@ -59,15 +59,14 @@ public final class Lexer { } var tokens = [Token]() - while scanner.isAtEnd { + while !scanner.isAtEnd { guard let logTokens = scanSLFType(scanner: scanner, - content: contents, redacted: redacted, withoutBuildSpecificInformation: withoutBuildSpecificInformation), logTokens.isEmpty == false else { print(tokens) - throw XCLogParserError.invalidLine(scanner.makeApproximateLine) + throw XCLogParserError.invalidLine(scanner.approximateLine) } tokens.append(contentsOf: logTokens) } @@ -75,16 +74,15 @@ public final class Lexer { } private func scanSLFHeader(scanner: Scanner) -> Bool { - return scanner.scan(prefix: Lexer.SLFHeader) + return scanner.scan(string: Lexer.SLFHeader) } private func scanSLFType(scanner: Scanner, - content: String, redacted: Bool, withoutBuildSpecificInformation: Bool) -> [Token]? { let payload = self.scanPayload(scanner: scanner) - guard let tokenTypes = self.scanTypeDelimiter(scanner: scanner, content: content), tokenTypes.count > 0 else { + guard let tokenTypes = self.scanTypeDelimiter(scanner: scanner), tokenTypes.count > 0 else { return nil } @@ -100,21 +98,19 @@ public final class Lexer { private func scanPayload(scanner: Scanner) -> String { let hexChars = "abcdef0123456789" let characterSet = Set(hexChars) - return scanner.scanCharacters(in: characterSet) ?? "" + return scanner.scanCharacters(from: characterSet) ?? "" } - private func scanTypeDelimiter(scanner: Scanner, - content: String) -> [TokenType]? { - guard let delimiters = scanner.scanCharacters(in: Set(self.typeDelimiters)) else { + private func scanTypeDelimiter(scanner: Scanner) -> [TokenType]? { + guard let delimiters = scanner.scanCharacters(from: self.typeDelimiters) else { return nil } if delimiters.count > 1 { // if we found a string, we discard other type delimiters because there are part of the string let tokenString = TokenType.string - if let char = delimiters.first, tokenString.rawValue == String(char) { - scanner.moveStartIndex(offset: -(delimiters.count - 1), originalString: content) + scanner.moveOffset(by: -(delimiters.count - 1)) return [tokenString] } } @@ -239,8 +235,7 @@ public final class Lexer { scanner: Scanner, redacted: Bool, withoutBuildSpecificInformation: Bool) -> String? { - let value = (length as NSString).integerValue - guard let scannedResult = scanner.scan(count: value) else { + guard let value = Int(length), let scannedResult = scanner.scan(count: value) else { print("error parsing string") return nil } @@ -267,7 +262,7 @@ public final class Lexer { } private extension Scanner { - var makeApproximateLine: String { + var approximateLine: String { let currentLocation = self.offset let contentSize = self.string.count diff --git a/Sources/XCLogParser/lexer/Scanner.swift b/Sources/XCLogParser/lexer/Scanner.swift index f5a2a9d..fc78f1b 100644 --- a/Sources/XCLogParser/lexer/Scanner.swift +++ b/Sources/XCLogParser/lexer/Scanner.swift @@ -27,7 +27,7 @@ final class Scanner { private(set) lazy var stringEndIndex: String.Index = self.string.endIndex var isAtEnd: Bool { - String.Index(compilerSafeOffset: self.offset, in: self.string) < self.stringEndIndex + String.Index(compilerSafeOffset: self.offset, in: self.string) >= self.stringEndIndex } init(string: String) { @@ -48,14 +48,14 @@ final class Scanner { return String(result) } - func scan(prefix: String) -> Bool { - guard self.string.starts(with: prefix) else { return false } + func scan(string value: String) -> Bool { + guard self.string.starts(with: value) else { return false } - self.offset = self.offset + prefix.count + self.offset = self.offset + value.count return true } - func scanCharacters(in allowedCharacters: Set) -> String? { + func scanCharacters(from allowedCharacters: Set) -> String? { var prefix: String = "" var characterIndex = String.Index(compilerSafeOffset: self.offset, in: self.string) @@ -74,7 +74,7 @@ final class Scanner { return prefix } - func moveStartIndex(offset: Int, originalString: String) { - self.offset = self.offset + offset + func moveOffset(by value: Int) { + self.offset = self.offset + value } } From 740d31b0dd2ce012c6b73fb39c3b831f55970a21 Mon Sep 17 00:00:00 2001 From: Jose Ramos Date: Sun, 20 Oct 2024 16:25:45 +0100 Subject: [PATCH 06/19] Add missing log Signed-off-by: Jose Ramos --- Sources/XCLogParser/lexer/Lexer.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/XCLogParser/lexer/Lexer.swift b/Sources/XCLogParser/lexer/Lexer.swift index b84cd40..cbe5225 100644 --- a/Sources/XCLogParser/lexer/Lexer.swift +++ b/Sources/XCLogParser/lexer/Lexer.swift @@ -157,6 +157,7 @@ public final class Lexer { private func handleIntTokenTypeCase(payload: String) -> Token? { guard let value = UInt64(payload) else { + print("error parsing int") return nil } return .int(value) From 987ee78282cd00c5e5b8fad554091c2ebb1eda1b Mon Sep 17 00:00:00 2001 From: memoto Date: Wed, 18 Dec 2024 11:45:25 +0100 Subject: [PATCH 07/19] Fix linting Co-authored-by: Daeho Ro Signed-off-by: memoto --- Sources/XCLogParser/loglocation/LogLoader.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XCLogParser/loglocation/LogLoader.swift b/Sources/XCLogParser/loglocation/LogLoader.swift index 3d8e946..4465c57 100644 --- a/Sources/XCLogParser/loglocation/LogLoader.swift +++ b/Sources/XCLogParser/loglocation/LogLoader.swift @@ -29,7 +29,7 @@ public struct LogLoader { let string: String? = unzipped.withUnsafeBytes { pointer in guard let charPointer = pointer .assumingMemoryBound(to: CChar.self) - .baseAddress + .baseAddress else { return nil } From 78b330a67b4e3c916f5ad0c68e61ba4bb163cc2a Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Tue, 17 Dec 2024 08:07:46 -0500 Subject: [PATCH 08/19] fix: bump version to match with the 0.2.41 release Signed-off-by: Rui Chen --- Sources/XCLogParser/commands/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XCLogParser/commands/Version.swift b/Sources/XCLogParser/commands/Version.swift index 84c80cb..d1fe7c4 100644 --- a/Sources/XCLogParser/commands/Version.swift +++ b/Sources/XCLogParser/commands/Version.swift @@ -21,6 +21,6 @@ import Foundation public struct Version { - public static let current = "0.2.39" + public static let current = "0.2.41" } From e2622a5e7d063bde984e8749a4a35a20ede9aae8 Mon Sep 17 00:00:00 2001 From: jrcmramos-bumble <88049527+jrcmramos-bumble@users.noreply.github.com> Date: Thu, 8 May 2025 09:56:23 +0100 Subject: [PATCH 09/19] Change `result` as immutable Co-authored-by: Antoine van der Lee <4329185+AvdLee@users.noreply.github.com> Signed-off-by: jrcmramos-bumble <88049527+jrcmramos-bumble@users.noreply.github.com> --- Sources/XCLogParser/lexer/Scanner.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XCLogParser/lexer/Scanner.swift b/Sources/XCLogParser/lexer/Scanner.swift index fc78f1b..42e9ef5 100644 --- a/Sources/XCLogParser/lexer/Scanner.swift +++ b/Sources/XCLogParser/lexer/Scanner.swift @@ -39,7 +39,7 @@ final class Scanner { let start = String.Index(compilerSafeOffset: self.offset, in: self.string) let end = String.Index(compilerSafeOffset: self.offset + count, in: self.string) - var result = self.string.substring(with: (start.. Date: Thu, 8 May 2025 11:37:14 +0100 Subject: [PATCH 10/19] Use addition assignments in Scanner.swift Signed-off-by: Jose Ricardo Correia Miranda Ramos --- Sources/XCLogParser/lexer/Scanner.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/XCLogParser/lexer/Scanner.swift b/Sources/XCLogParser/lexer/Scanner.swift index 42e9ef5..bee0fe5 100644 --- a/Sources/XCLogParser/lexer/Scanner.swift +++ b/Sources/XCLogParser/lexer/Scanner.swift @@ -39,11 +39,11 @@ final class Scanner { let start = String.Index(compilerSafeOffset: self.offset, in: self.string) let end = String.Index(compilerSafeOffset: self.offset + count, in: self.string) - let result = self.string.substring(with: (start.. Bool { guard self.string.starts(with: value) else { return false } - self.offset = self.offset + value.count + self.offset += value.count return true } @@ -61,13 +61,13 @@ final class Scanner { while characterIndex < self.stringEndIndex { let character = self.string[characterIndex] - + guard allowedCharacters.contains(character) else { break } prefix.append(character) - self.offset = self.offset + 1 + self.offset += 1 characterIndex = String.Index(utf16Offset: self.offset, in: self.string) } @@ -75,6 +75,6 @@ final class Scanner { } func moveOffset(by value: Int) { - self.offset = self.offset + value + self.offset += value } } From 38225d865f21d4c2874bf55b79bda160f2f1d2e6 Mon Sep 17 00:00:00 2001 From: Pedro Date: Fri, 16 May 2025 19:01:14 +0200 Subject: [PATCH 11/19] Check with all the elements --- Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift b/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift index b16a7b8..189a1eb 100644 --- a/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift +++ b/Sources/XCLogParser/parser/IDEActivityLogSection+Parsing.swift @@ -42,8 +42,7 @@ extension IDEActivityLogSection { func groupedByTarget() -> IDEActivityLogSection { // The only way to know if the structure is flatten is to check the first elements // for the `(in target 'ABC' from project Project)` string - let firstElements = subSections.prefix(15) // we only analyze up to the first 15 subsections - let isFlatten = firstElements.contains { $0.getTargetFromCommand() != nil } + let isFlatten = subSections.contains { $0.getTargetFromCommand() != nil } if isFlatten { let mainTarget = "$MainTarget" let targetsDictionary = subSections.reduce( From 040ef7e2c8788a14de3cb0e886c817185ed2cb26 Mon Sep 17 00:00:00 2001 From: Yasura Dodo <11143310+yasuradodo@users.noreply.github.com> Date: Mon, 19 May 2025 19:14:21 +0200 Subject: [PATCH 12/19] Update Xcactivitylog Format.md Signed-off-by: Yasura Dodo <11143310+yasuradodo@users.noreply.github.com> --- docs/Xcactivitylog Format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Xcactivitylog Format.md b/docs/Xcactivitylog Format.md index dc6c31c..2869f64 100644 --- a/docs/Xcactivitylog Format.md +++ b/docs/Xcactivitylog Format.md @@ -155,4 +155,4 @@ Inside the logs you can find these classes: If you search for them, you will find that they belong to the IDEFoundation.framework. A private framework part of Xcode. You can class dump it to get the headers of those classes. Once you have the headers, you will have the name and type of the properties that belong to the class. Now, you can match them to the tokens you got from the log. Some of them are in the same order than in the headers, but for others it will be about trial and error. -In the project you can find those classes with their properties in the order in which they appear in the log in the file (IDEActivityModel.swift)[https://github.com/MobileNativeFoundation/XCLogParser/blob/master/Sources/XCLogParser/activityparser/IDEActivityModel.swift]. +In the project you can find those classes with their properties in the order in which they appear in the log in the file [IDEActivityModel.swift](../Sources/XCLogParser/activityparser/IDEActivityModel.swift). From a640c391cd1a8bd7643c24b71b8dd5eea7393bee Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 12 Jun 2025 11:48:38 +0200 Subject: [PATCH 13/19] Version 0.2.42 --- Sources/XCLogParser/commands/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XCLogParser/commands/Version.swift b/Sources/XCLogParser/commands/Version.swift index d1fe7c4..406acba 100644 --- a/Sources/XCLogParser/commands/Version.swift +++ b/Sources/XCLogParser/commands/Version.swift @@ -21,6 +21,6 @@ import Foundation public struct Version { - public static let current = "0.2.41" + public static let current = "0.2.42" } From b2d7f2ed75b4ddc45f70e9c0452fd9ff5edc8299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fo=C5=99t?= Date: Tue, 8 Jul 2025 09:26:10 +0200 Subject: [PATCH 14/19] Fix parsing failing due to new TaskBacktrace activity log attachment (#235) * Fix parsing failing due to new TaskBacktrace activity log attachment Signed-off-by: fortmarek * Align backtrace with swift-build Signed-off-by: fortmarek --------- Signed-off-by: fortmarek --- .../activityparser/ActivityParser.swift | 27 +++-- .../activityparser/IDEActivityModel.swift | 101 +++++++++++++++++- .../XCLogParser/logmanifest/LogManifest.swift | 2 +- .../ActivityParserTests.swift | 12 ++- 4 files changed, 132 insertions(+), 10 deletions(-) diff --git a/Sources/XCLogParser/activityparser/ActivityParser.swift b/Sources/XCLogParser/activityparser/ActivityParser.swift index 07d8f27..49a98e9 100644 --- a/Sources/XCLogParser/activityparser/ActivityParser.swift +++ b/Sources/XCLogParser/activityparser/ActivityParser.swift @@ -380,12 +380,27 @@ public class ActivityParser { } if className == "IDEFoundation.\(String(describing: IDEActivityLogSectionAttachment.self))" { - let jsonType = IDEActivityLogSectionAttachment.BuildOperationTaskMetrics.self - return try IDEActivityLogSectionAttachment(identifier: try parseAsString(token: iterator.next()), - majorVersion: try parseAsInt(token: iterator.next()), - minorVersion: try parseAsInt(token: iterator.next()), - metrics: try parseAsJson(token: iterator.next(), - type: jsonType)) + let identifier = try parseAsString(token: iterator.next()) + switch identifier.components(separatedBy: "ActivityLogSectionAttachment.").last { + case .some("TaskMetrics"): + let jsonType = IDEActivityLogSectionAttachment.BuildOperationTaskMetrics.self + return try IDEActivityLogSectionAttachment(identifier: identifier, + majorVersion: try parseAsInt(token: iterator.next()), + minorVersion: try parseAsInt(token: iterator.next()), + metrics: try parseAsJson(token: iterator.next(), + type: jsonType), + backtrace: nil) + case .some("TaskBacktrace"): + let jsonType = IDEActivityLogSectionAttachment.BuildOperationTaskBacktrace.self + return try IDEActivityLogSectionAttachment(identifier: identifier, + majorVersion: try parseAsInt(token: iterator.next()), + minorVersion: try parseAsInt(token: iterator.next()), + metrics: nil, + backtrace: try parseAsJson(token: iterator.next(), + type: jsonType)) + default: + throw XCLogParserError.parseError("Unexpected attachment identifier \(identifier)") + } } throw XCLogParserError.parseError("Unexpected className found parsing IDEConsoleItem \(className)") } diff --git a/Sources/XCLogParser/activityparser/IDEActivityModel.swift b/Sources/XCLogParser/activityparser/IDEActivityModel.swift index adeb83f..0826d60 100644 --- a/Sources/XCLogParser/activityparser/IDEActivityModel.swift +++ b/Sources/XCLogParser/activityparser/IDEActivityModel.swift @@ -658,16 +658,115 @@ public class IDEActivityLogSectionAttachment: Encodable { public let majorVersion: UInt64 public let minorVersion: UInt64 public let metrics: BuildOperationTaskMetrics? + public let backtrace: BuildOperationTaskBacktrace? public init( identifier: String, majorVersion: UInt64, minorVersion: UInt64, - metrics: BuildOperationTaskMetrics? + metrics: BuildOperationTaskMetrics?, + backtrace: BuildOperationTaskBacktrace? ) throws { self.identifier = identifier self.majorVersion = majorVersion self.minorVersion = minorVersion self.metrics = metrics + self.backtrace = backtrace + } + + public struct BuildOperationTaskBacktrace: Codable { + public let frames: [BuildOperationTaskBacktraceFrame] + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + self.frames = try container.decode([BuildOperationTaskBacktraceFrame].self) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(frames) + } + } + + public struct BuildOperationTaskBacktraceFrame: Codable { + public let category: BuildOperationTaskBacktraceCategory + public let description: String + } + + public enum BuildOperationTaskBacktraceCategory: String, Codable { + case ruleHadInvalidValue + case ruleSignatureChanged + case ruleNeverBuilt + case ruleInputRebuilt + case ruleForced + case dynamicTaskRegistration + case dynamicTaskRequest + case none + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: BuildOperationTaskBacktraceCategoryCodingKeys.self) + + if container.contains(.ruleHadInvalidValue) { + self = .ruleHadInvalidValue + } else if container.contains(.ruleSignatureChanged) { + self = .ruleSignatureChanged + } else if container.contains(.ruleNeverBuilt) { + self = .ruleNeverBuilt + } else if container.contains(.ruleInputRebuilt) { + self = .ruleInputRebuilt + } else if container.contains(.ruleForced) { + self = .ruleForced + } else if container.contains(.dynamicTaskRegistration) { + self = .dynamicTaskRegistration + } else if container.contains(.dynamicTaskRequest) { + self = .dynamicTaskRequest + } else if container.contains(.none) { + self = .none + } else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown task backtrace category" + ) + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: BuildOperationTaskBacktraceCategoryCodingKeys.self) + switch self { + case .ruleHadInvalidValue: + try container.encode(EmptyObject(), forKey: .ruleHadInvalidValue) + case .ruleSignatureChanged: + try container.encode(EmptyObject(), forKey: .ruleSignatureChanged) + case .ruleNeverBuilt: + try container.encode(EmptyObject(), forKey: .ruleNeverBuilt) + case .ruleInputRebuilt: + try container.encode(EmptyObject(), forKey: .ruleInputRebuilt) + case .ruleForced: + try container.encode(EmptyObject(), forKey: .ruleForced) + case .dynamicTaskRegistration: + try container.encode(EmptyObject(), forKey: .dynamicTaskRegistration) + case .dynamicTaskRequest: + try container.encode(EmptyObject(), forKey: .dynamicTaskRequest) + case .none: + try container.encode(EmptyObject(), forKey: .none) + } + } + } + + private enum BuildOperationTaskBacktraceCategoryCodingKeys: String, CodingKey { + case ruleHadInvalidValue + case ruleSignatureChanged + case ruleNeverBuilt + case ruleInputRebuilt + case ruleForced + case dynamicTaskRegistration + case dynamicTaskRequest + case none + } + + private struct EmptyObject: Codable { + // Empty struct for objects with no properties } } diff --git a/Sources/XCLogParser/logmanifest/LogManifest.swift b/Sources/XCLogParser/logmanifest/LogManifest.swift index 57773c6..eafff2e 100644 --- a/Sources/XCLogParser/logmanifest/LogManifest.swift +++ b/Sources/XCLogParser/logmanifest/LogManifest.swift @@ -25,7 +25,7 @@ public struct LogManifest { public init() {} - public func getWithLogOptions(_ logOptions: LogOptions) throws -> [LogManifestEntry] { + public func getWithLogOptions(_ logOptions: LogOptions) throws -> [LogManifestEntry] { let logFinder = LogFinder() let logManifestURL = try logFinder.findLogManifestWithLogOptions(logOptions) let logManifestDictionary = try getDictionaryFromURL(logManifestURL) diff --git a/Tests/XCLogParserTests/ActivityParserTests.swift b/Tests/XCLogParserTests/ActivityParserTests.swift index de1c787..b68245c 100644 --- a/Tests/XCLogParserTests/ActivityParserTests.swift +++ b/Tests/XCLogParserTests/ActivityParserTests.swift @@ -97,7 +97,13 @@ class ActivityParserTests: XCTestCase { Token.string("501796C4-6BE4-4F80-9F9D-3269617ECC17"), Token.string("localizedResultString"), Token.string("xcbuildSignature"), - Token.list(1), + Token.list(2), + Token.classNameRef("IDEFoundation.IDEActivityLogSectionAttachment"), + Token.string("com.apple.dt.ActivityLogSectionAttachment.TaskBacktrace"), + Token.int(1), + Token.int(0), + // swiftlint:disable:next line_length + Token.json(#"[{"description":"'Planning Swift module ConcurrencyExtras (arm64)' had never run","category":{"ruleNeverBuilt":{}},"identifier":{"storage":{"task":{"_0":[0,80,50,58,116,97,114,103,101,116,45,67,111,110,99,117,114,114,101,110,99,121,69,120,116,114,97,115,45,101,102,52,50,51,48,52,53,57,52,98,102,56,53,50,102,52,51,56,101,102,55,99,51,97,49,51,54,98,50,99,57,48,100,102,56,55,49,56,97,102,50,98,57,100,51,97,97,99,48,100,48,100,99,97,50,50,98,52,99,50,57,99,50,45,58,66,101,116,97,32,68,101,98,117,103,58,51,99,57,97,99,57,53,50,98,52,99,56,49,100,57,99,99,49,55,100,49,97,102,52,55,49,97,48,52,53,101,56]}}},"frameKind":{"genericTask":{}}}]"#), Token.classNameRef("IDEFoundation.IDEActivityLogSectionAttachment"), Token.string("com.apple.dt.ActivityLogSectionAttachment.TaskMetrics"), Token.int(1), @@ -339,7 +345,9 @@ class ActivityParserTests: XCTestCase { XCTAssertEqual("501796C4-6BE4-4F80-9F9D-3269617ECC17", logSection.uniqueIdentifier) XCTAssertEqual("localizedResultString", logSection.localizedResultString) XCTAssertEqual("xcbuildSignature", logSection.xcbuildSignature) - XCTAssertEqual(1, logSection.attachments.count) + XCTAssertEqual(2, logSection.attachments.count) + XCTAssertEqual(logSection.attachments[0].backtrace?.frames.first?.category, .ruleNeverBuilt) + XCTAssertEqual(logSection.attachments[1].metrics?.wcDuration, 1) XCTAssertEqual(0, logSection.unknown) } From e204aeffb95c907db93d19aede2062a11cdb5123 Mon Sep 17 00:00:00 2001 From: fortmarek Date: Tue, 8 Jul 2025 09:28:15 +0200 Subject: [PATCH 15/19] Version 0.2.43 Signed-off-by: fortmarek --- Sources/XCLogParser/commands/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XCLogParser/commands/Version.swift b/Sources/XCLogParser/commands/Version.swift index 406acba..f4dec17 100644 --- a/Sources/XCLogParser/commands/Version.swift +++ b/Sources/XCLogParser/commands/Version.swift @@ -21,6 +21,6 @@ import Foundation public struct Version { - public static let current = "0.2.42" + public static let current = "0.2.43" } From cf4df991cd095d516996908d8dd60c9b5b729252 Mon Sep 17 00:00:00 2001 From: Antoine van der Lee <4329185+AvdLee@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:22:01 +0200 Subject: [PATCH 16/19] Update Sources/XCLogParser/lexer/Scanner.swift Signed-off-by: Antoine van der Lee <4329185+AvdLee@users.noreply.github.com> --- Sources/XCLogParser/lexer/Scanner.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/XCLogParser/lexer/Scanner.swift b/Sources/XCLogParser/lexer/Scanner.swift index bee0fe5..99dce2f 100644 --- a/Sources/XCLogParser/lexer/Scanner.swift +++ b/Sources/XCLogParser/lexer/Scanner.swift @@ -37,8 +37,11 @@ final class Scanner { func scan(count: Int) -> String? { let start = String.Index(compilerSafeOffset: self.offset, in: self.string) - let end = String.Index(compilerSafeOffset: self.offset + count, in: self.string) + let endOffset = self.offset + count + guard endOffset <= self.string.utf16.count else { return nil } + + let end = String.Index(compilerSafeOffset: endOffset, in: self.string) let result = self.string[start.. Date: Tue, 16 Sep 2025 09:14:28 +0200 Subject: [PATCH 17/19] Fix parsing new BuildOperationMetrics (#237) Signed-off-by: fortmarek --- .../activityparser/ActivityParser.swift | 13 +++++++++++ .../activityparser/IDEActivityModel.swift | 22 +++++++++++++++++++ .../ActivityParserTests.swift | 14 +++++++++--- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Sources/XCLogParser/activityparser/ActivityParser.swift b/Sources/XCLogParser/activityparser/ActivityParser.swift index 49a98e9..8aec978 100644 --- a/Sources/XCLogParser/activityparser/ActivityParser.swift +++ b/Sources/XCLogParser/activityparser/ActivityParser.swift @@ -389,6 +389,7 @@ public class ActivityParser { minorVersion: try parseAsInt(token: iterator.next()), metrics: try parseAsJson(token: iterator.next(), type: jsonType), + buildOperationMetrics: nil, backtrace: nil) case .some("TaskBacktrace"): let jsonType = IDEActivityLogSectionAttachment.BuildOperationTaskBacktrace.self @@ -396,8 +397,20 @@ public class ActivityParser { majorVersion: try parseAsInt(token: iterator.next()), minorVersion: try parseAsInt(token: iterator.next()), metrics: nil, + buildOperationMetrics: nil, backtrace: try parseAsJson(token: iterator.next(), type: jsonType)) + case .some("BuildOperationMetrics"): + let jsonType = IDEActivityLogSectionAttachment.BuildOperationMetrics.self + return try IDEActivityLogSectionAttachment(identifier: identifier, + majorVersion: try parseAsInt(token: iterator.next()), + minorVersion: try parseAsInt(token: iterator.next()), + metrics: nil, + buildOperationMetrics: try parseAsJson( + token: iterator.next(), + type: jsonType + ), + backtrace: nil) default: throw XCLogParserError.parseError("Unexpected attachment identifier \(identifier)") } diff --git a/Sources/XCLogParser/activityparser/IDEActivityModel.swift b/Sources/XCLogParser/activityparser/IDEActivityModel.swift index 0826d60..312a04d 100644 --- a/Sources/XCLogParser/activityparser/IDEActivityModel.swift +++ b/Sources/XCLogParser/activityparser/IDEActivityModel.swift @@ -658,6 +658,7 @@ public class IDEActivityLogSectionAttachment: Encodable { public let majorVersion: UInt64 public let minorVersion: UInt64 public let metrics: BuildOperationTaskMetrics? + public let buildOperationMetrics: BuildOperationMetrics? public let backtrace: BuildOperationTaskBacktrace? public init( @@ -665,12 +666,14 @@ public class IDEActivityLogSectionAttachment: Encodable { majorVersion: UInt64, minorVersion: UInt64, metrics: BuildOperationTaskMetrics?, + buildOperationMetrics: BuildOperationMetrics?, backtrace: BuildOperationTaskBacktrace? ) throws { self.identifier = identifier self.majorVersion = majorVersion self.minorVersion = minorVersion self.metrics = metrics + self.buildOperationMetrics = buildOperationMetrics self.backtrace = backtrace } @@ -769,4 +772,23 @@ public class IDEActivityLogSectionAttachment: Encodable { private struct EmptyObject: Codable { // Empty struct for objects with no properties } + + public struct BuildOperationMetrics: Codable { + public let clangCacheHits: Int + public let clangCacheMisses: Int + public let swiftCacheHits: Int + public let swiftCacheMisses: Int + + public init( + clangCacheHits: Int, + clangCacheMisses: Int, + swiftCacheHits: Int, + swiftCacheMisses: Int + ) { + self.clangCacheHits = clangCacheHits + self.clangCacheMisses = clangCacheMisses + self.swiftCacheHits = swiftCacheHits + self.swiftCacheMisses = swiftCacheMisses + } + } } diff --git a/Tests/XCLogParserTests/ActivityParserTests.swift b/Tests/XCLogParserTests/ActivityParserTests.swift index b68245c..dbb2aa8 100644 --- a/Tests/XCLogParserTests/ActivityParserTests.swift +++ b/Tests/XCLogParserTests/ActivityParserTests.swift @@ -97,7 +97,7 @@ class ActivityParserTests: XCTestCase { Token.string("501796C4-6BE4-4F80-9F9D-3269617ECC17"), Token.string("localizedResultString"), Token.string("xcbuildSignature"), - Token.list(2), + Token.list(3), Token.classNameRef("IDEFoundation.IDEActivityLogSectionAttachment"), Token.string("com.apple.dt.ActivityLogSectionAttachment.TaskBacktrace"), Token.int(1), @@ -105,6 +105,12 @@ class ActivityParserTests: XCTestCase { // swiftlint:disable:next line_length Token.json(#"[{"description":"'Planning Swift module ConcurrencyExtras (arm64)' had never run","category":{"ruleNeverBuilt":{}},"identifier":{"storage":{"task":{"_0":[0,80,50,58,116,97,114,103,101,116,45,67,111,110,99,117,114,114,101,110,99,121,69,120,116,114,97,115,45,101,102,52,50,51,48,52,53,57,52,98,102,56,53,50,102,52,51,56,101,102,55,99,51,97,49,51,54,98,50,99,57,48,100,102,56,55,49,56,97,102,50,98,57,100,51,97,97,99,48,100,48,100,99,97,50,50,98,52,99,50,57,99,50,45,58,66,101,116,97,32,68,101,98,117,103,58,51,99,57,97,99,57,53,50,98,52,99,56,49,100,57,99,99,49,55,100,49,97,102,52,55,49,97,48,52,53,101,56]}}},"frameKind":{"genericTask":{}}}]"#), Token.classNameRef("IDEFoundation.IDEActivityLogSectionAttachment"), + Token.string("com.apple.dt.ActivityLogSectionAttachment.BuildOperationMetrics"), + Token.int(1), + Token.int(0), + // swiftlint:disable:next line_length + Token.json(#"{"clangCacheHits":0,"clangCacheMisses":2,"swiftCacheHits":0,"swiftCacheMisses":8}"#), + Token.classNameRef("IDEFoundation.IDEActivityLogSectionAttachment"), Token.string("com.apple.dt.ActivityLogSectionAttachment.TaskMetrics"), Token.int(1), Token.int(0), @@ -345,9 +351,11 @@ class ActivityParserTests: XCTestCase { XCTAssertEqual("501796C4-6BE4-4F80-9F9D-3269617ECC17", logSection.uniqueIdentifier) XCTAssertEqual("localizedResultString", logSection.localizedResultString) XCTAssertEqual("xcbuildSignature", logSection.xcbuildSignature) - XCTAssertEqual(2, logSection.attachments.count) + XCTAssertEqual(3, logSection.attachments.count) XCTAssertEqual(logSection.attachments[0].backtrace?.frames.first?.category, .ruleNeverBuilt) - XCTAssertEqual(logSection.attachments[1].metrics?.wcDuration, 1) + print(logSection.attachments) + XCTAssertEqual(logSection.attachments[1].buildOperationMetrics?.clangCacheMisses, 2) + XCTAssertEqual(logSection.attachments[2].metrics?.wcDuration, 1) XCTAssertEqual(0, logSection.unknown) } From 8bcbbfd4c598d1032c7242afb40ab64fd0357fbf Mon Sep 17 00:00:00 2001 From: Uriel Hernandez Date: Mon, 16 Dec 2024 16:36:50 -0600 Subject: [PATCH 18/19] Updating ascii parsing --- Sources/XCLogParser/loglocation/LogLoader.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/XCLogParser/loglocation/LogLoader.swift b/Sources/XCLogParser/loglocation/LogLoader.swift index 4465c57..c7bfcd3 100644 --- a/Sources/XCLogParser/loglocation/LogLoader.swift +++ b/Sources/XCLogParser/loglocation/LogLoader.swift @@ -36,6 +36,7 @@ public struct LogLoader { return String(cString: charPointer, encoding: .ascii) } + guard let contents = string else { throw LogError.readingFile(url.path) } From bc41bcde2ada3a90eba71372e3c0a6144801b0f9 Mon Sep 17 00:00:00 2001 From: Ernesto Flores Date: Thu, 23 Oct 2025 15:56:44 -0600 Subject: [PATCH 19/19] Remove empty line --- Sources/XCLogParser/loglocation/LogLoader.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/XCLogParser/loglocation/LogLoader.swift b/Sources/XCLogParser/loglocation/LogLoader.swift index c7bfcd3..4465c57 100644 --- a/Sources/XCLogParser/loglocation/LogLoader.swift +++ b/Sources/XCLogParser/loglocation/LogLoader.swift @@ -36,7 +36,6 @@ public struct LogLoader { return String(cString: charPointer, encoding: .ascii) } - guard let contents = string else { throw LogError.readingFile(url.path) }