commit fcf2241bd54e354a9280aab0b766f5f4132df3af
parent 45d598f0b3409015d32510a2ac64ecf9ca631754
Author: Michael Camilleri <[email protected]>
Date: Fri, 3 Jul 2026 14:21:18 +0900
Improve speed of diagnostics log sharing
Diffstat:
1 file changed, 32 insertions(+), 6 deletions(-)
diff --git a/Crossmate/Views/Settings/DiagnosticsView.swift b/Crossmate/Views/Settings/DiagnosticsView.swift
@@ -125,7 +125,7 @@ struct DiagnosticsView: View {
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
ShareLink(
- item: DiagnosticsShareFile(render: { diagnosticDump }),
+ item: DiagnosticsShareFile(snapshot: { diagnosticDump }),
preview: SharePreview(
"Crossmate Diagnostics",
image: Image(systemName: "doc.text")
@@ -224,7 +224,7 @@ struct DiagnosticsView: View {
return "\(version) (\(build))"
}
- private var diagnosticDump: String {
+ private var diagnosticDump: DiagnosticsDump {
var lines: [String] = []
lines.append("Version: \(versionText)")
lines.append("Account Status: \(accountStatusText)")
@@ -238,11 +238,33 @@ struct DiagnosticsView: View {
lines.append("Last Error Code: \(syncMonitor.lastErrorCode.map(String.init) ?? "None")")
lines.append("Last Error Description: \(syncMonitor.lastErrorDescription ?? "None")")
lines.append("Recent Event Count: \(eventLog.entries.count)")
+ return DiagnosticsDump(headerLines: lines, entries: eventLog.entries)
+ }
+}
+
+/// The dump's inputs, captured on the main actor so the expensive per-entry
+/// formatting can run off it. `EventLogEntry` is a Sendable value type, so
+/// taking the buffer is a cheap copy-on-write retain, not a data copy.
+private struct DiagnosticsDump: Sendable {
+ let headerLines: [String]
+ let entries: [EventLogEntry]
+
+ /// Formats the full dump. Runs nonisolated on purpose: tens of thousands
+ /// of entries make this the slow part of a share, and it sits between the
+ /// share tap and the sheet appearing — on the main actor it both stalls
+ /// the UI and queues behind it. The formatter is created locally so the
+ /// loop shares nothing across the isolation boundary.
+ func rendered() -> String {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "h:mm:ss a 'UTC'"
+ formatter.timeZone = TimeZone(secondsFromGMT: 0)
+
+ var lines = headerLines
lines.append("")
lines.append("Recent Events (UTC):")
- for entry in eventLog.entries {
+ for entry in entries {
lines.append(
- "\(TimestampFormatter.string(from: entry.timestamp, in: .utc)) [\(entry.level.uppercased())] \(entry.message)"
+ "\(formatter.string(from: entry.timestamp)) [\(entry.level.uppercased())] \(entry.message)"
)
}
return lines.joined(separator: "\n")
@@ -253,14 +275,18 @@ struct DiagnosticsView: View {
/// buffer spans a full day (tens of thousands of entries during a co-solve),
/// so building the joined text and writing it to disk eagerly — as the view
/// once did on every log event — is multi-MB of wasted work per second.
+/// Only the cheap input snapshot runs on the main actor; the formatting and
+/// disk write happen in the export's own background context, since ShareLink
+/// won't present the sheet until this closure returns.
private struct DiagnosticsShareFile: Transferable {
- let render: @MainActor () -> String
+ let snapshot: @MainActor () -> DiagnosticsDump
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(exportedContentType: .plainText) { file in
+ let dump = await file.snapshot()
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("crossmate-diagnostics.txt")
- try await file.render().write(to: url, atomically: true, encoding: .utf8)
+ try dump.rendered().write(to: url, atomically: true, encoding: .utf8)
return SentTransferredFile(url)
}
}