From 86f0b4bd20cdec86dfb5bdb7978d1f6ff4af111a Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 14 Aug 2026 22:41:54 +0800
Subject: [PATCH] [verified] feat: export performance diagnostics

---
 C1.source/Sources/AlignerCore/Diagnostics/PerformanceDiagnosticArchiveWriter.swift |  154 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 154 insertions(+), 0 deletions(-)

diff --git a/C1.source/Sources/AlignerCore/Diagnostics/PerformanceDiagnosticArchiveWriter.swift b/C1.source/Sources/AlignerCore/Diagnostics/PerformanceDiagnosticArchiveWriter.swift
new file mode 100644
index 0000000..31200ef
--- /dev/null
+++ b/C1.source/Sources/AlignerCore/Diagnostics/PerformanceDiagnosticArchiveWriter.swift
@@ -0,0 +1,154 @@
+import Foundation
+
+public enum PerformanceDiagnosticArchiveWriter {
+    public static func write(
+        to destinationURL: URL,
+        summaryJSON: Data,
+        eventLogURLs: [URL],
+        fileManager: FileManager = .default
+    ) throws -> URL {
+        guard !fileManager.fileExists(atPath: destinationURL.path) else {
+            throw ArchiveError.destinationAlreadyExists
+        }
+        let workingDirectory = fileManager.temporaryDirectory
+            .appendingPathComponent("Aligner-Diagnostics-\(UUID().uuidString)", isDirectory: true)
+        let contentsDirectory = workingDirectory
+            .appendingPathComponent("Aligner-Diagnostics", isDirectory: true)
+
+        try fileManager.createDirectory(at: contentsDirectory, withIntermediateDirectories: true)
+        defer { try? fileManager.removeItem(at: workingDirectory) }
+
+        try summaryJSON.write(
+            to: contentsDirectory.appendingPathComponent("summary.json"),
+            options: [.atomic]
+        )
+        try readmeText.write(
+            to: contentsDirectory.appendingPathComponent("README.txt"),
+            atomically: true,
+            encoding: .utf8
+        )
+
+        var copiedLogCount = 0
+        for (index, sourceURL) in eventLogURLs.enumerated() where fileManager.fileExists(atPath: sourceURL.path) {
+            let name = index == 0 ? "performance-events.log" : "performance-events-previous.log"
+            try sanitizedEventLogData(from: sourceURL).write(
+                to: contentsDirectory.appendingPathComponent(name),
+                options: [.atomic]
+            )
+            copiedLogCount += 1
+        }
+
+        if copiedLogCount == 0 {
+            try "No performance events have been recorded yet. Reproduce the issue, then export again.\n".write(
+                to: contentsDirectory.appendingPathComponent("performance-events.log"),
+                atomically: true,
+                encoding: .utf8
+            )
+        }
+
+        let temporaryArchiveURL = destinationURL
+            .deletingLastPathComponent()
+            .appendingPathComponent(".Aligner-Diagnostics-\(UUID().uuidString).zip")
+        defer { try? fileManager.removeItem(at: temporaryArchiveURL) }
+        try createArchive(
+            sourceDirectory: contentsDirectory,
+            destinationURL: temporaryArchiveURL
+        )
+        try fileManager.moveItem(at: temporaryArchiveURL, to: destinationURL)
+        return destinationURL
+    }
+
+    private static func createArchive(sourceDirectory: URL, destinationURL: URL) throws {
+        let process = Process()
+        process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
+        process.arguments = [
+            "-c",
+            "-k",
+            "--sequesterRsrc",
+            "--keepParent",
+            sourceDirectory.path,
+            destinationURL.path
+        ]
+        try process.run()
+        process.waitUntilExit()
+        guard process.terminationStatus == 0 else {
+            throw ArchiveError.dittoFailed(status: process.terminationStatus)
+        }
+    }
+
+    private static func sanitizedEventLogData(from sourceURL: URL) throws -> Data {
+        let source = try String(contentsOf: sourceURL, encoding: .utf8)
+        let sanitized = source
+            .split(whereSeparator: \.isNewline)
+            .compactMap(sanitizedEventLine)
+            .joined(separator: "\n")
+        return Data((sanitized.isEmpty ? "No exportable performance events were recorded.\n" : sanitized + "\n").utf8)
+    }
+
+    private static func sanitizedEventLine(_ line: Substring) -> String? {
+        let source = String(line)
+        guard let event = tokenValue(named: "event", in: source) else { return nil }
+
+        let allowedKeys = [
+            "ts",
+            "event",
+            "pid",
+            "runID",
+            "generation",
+            "durationMilliseconds",
+            "overlayOpenElapsedMilliseconds",
+            "startElapsedMilliseconds",
+            "snapshotStartElapsedMilliseconds",
+            "lastActivationDurationMilliseconds",
+            "appCount",
+            "windowCount",
+            "displayCount",
+            "spaceCount",
+            "screenshotEligibleCount",
+            "screenshotResolvedCount",
+            "screenshotPendingCount",
+            "screenshotNotRequestedCount",
+            "ranOnMainThread",
+            "result",
+            "reason"
+        ]
+        let values = allowedKeys.compactMap { key -> String? in
+            let value = key == "event" ? event : tokenValue(named: key, in: source)
+            return value.map { "\(key)=\($0)" }
+        }
+        return values.isEmpty ? nil : values.joined(separator: " ")
+    }
+
+    private static func tokenValue(named key: String, in line: String) -> String? {
+        let prefix = "\(key)="
+        guard let range = line.range(of: prefix) else { return nil }
+        let suffix = line[range.upperBound...]
+        guard let first = suffix.first, first != "\"" else { return nil }
+        let value = suffix.prefix { !$0.isWhitespace }
+        return value.isEmpty ? nil : String(value)
+    }
+
+    private static let readmeText = """
+    Aligner performance diagnostics
+
+    This archive contains local performance information for troubleshooting:
+    - summary.json: app version, system, display, permissions, preferences, recent Quick Switch timings, application names, and window counts.
+    - performance-events.log: recent performance events with a fixed allowlist of timing and count fields.
+
+    It does not include window titles, screenshots, or typed text.
+    """
+
+    private enum ArchiveError: LocalizedError {
+        case destinationAlreadyExists
+        case dittoFailed(status: Int32)
+
+        var errorDescription: String? {
+            switch self {
+            case .destinationAlreadyExists:
+                return "A diagnostic archive already exists at the selected location."
+            case .dittoFailed(let status):
+                return "Failed to create the diagnostic archive (ditto exit status \(status))."
+            }
+        }
+    }
+}

--
Gitblit v1.9.3