Improve performance for Logs

This commit is contained in:
世界
2025-11-26 22:25:51 +08:00
parent f6d14dbb8a
commit 32f5bf1f3b
4 changed files with 96 additions and 19 deletions
+38 -7
View File
@@ -206,9 +206,25 @@ class LogCoordinator {
context.coordinator.cachedAttributedString = attributedString context.coordinator.cachedAttributedString = attributedString
textView.attributedText = attributedString textView.attributedText = attributedString
case let .incremental(from: startIndex): case let .incremental(from: startIndex):
let attributedString = buildAttributedString(logs: logs, monoFont: Self.monoFont, defaultColor: Self.defaultColor, searchText: searchText, baseAttributedString: context.coordinator.cachedAttributedString, startIndex: startIndex) // Build only the new logs, not the entire attributed string
context.coordinator.cachedAttributedString = attributedString let newLogsAttributedString = buildAttributedString(logs: logs, monoFont: Self.monoFont, defaultColor: Self.defaultColor, searchText: searchText, baseAttributedString: nil, startIndex: startIndex)
textView.attributedText = attributedString
// Append to existing text storage instead of replacing
let textStorage = textView.textStorage
// Add newline separator before appending if there's existing content
if textStorage.length > 0 {
let newline = NSAttributedString(string: "\n", attributes: [
.foregroundColor: Self.defaultColor,
.font: Self.monoFont,
])
textStorage.append(newline)
}
textStorage.append(newLogsAttributedString)
// Update cache to full content
context.coordinator.cachedAttributedString = NSAttributedString(attributedString: textStorage)
} }
} }
@@ -254,6 +270,7 @@ class LogCoordinator {
func updateNSView(_ scrollView: NSScrollView, context: Context) { func updateNSView(_ scrollView: NSScrollView, context: Context) {
guard let textView = scrollView.documentView as? NSTextView else { return } guard let textView = scrollView.documentView as? NSTextView else { return }
guard let textStorage = textView.textStorage else { return }
let lastCount = context.coordinator.lastLogsCount let lastCount = context.coordinator.lastLogsCount
let updateStrategy = context.coordinator.shouldUpdate(logs: logs, searchText: searchText) let updateStrategy = context.coordinator.shouldUpdate(logs: logs, searchText: searchText)
@@ -264,11 +281,25 @@ class LogCoordinator {
case .fullRebuild: case .fullRebuild:
let attributedText = buildAttributedString(logs: logs, monoFont: Self.monoFont, defaultColor: Self.defaultColor, searchText: searchText) let attributedText = buildAttributedString(logs: logs, monoFont: Self.monoFont, defaultColor: Self.defaultColor, searchText: searchText)
context.coordinator.cachedAttributedString = attributedText context.coordinator.cachedAttributedString = attributedText
textView.textStorage?.setAttributedString(attributedText) textStorage.setAttributedString(attributedText)
case let .incremental(from: startIndex): case let .incremental(from: startIndex):
let attributedText = buildAttributedString(logs: logs, monoFont: Self.monoFont, defaultColor: Self.defaultColor, searchText: searchText, baseAttributedString: context.coordinator.cachedAttributedString, startIndex: startIndex) // Build only the new logs, not the entire attributed string
context.coordinator.cachedAttributedString = attributedText let newLogsAttributedString = buildAttributedString(logs: logs, monoFont: Self.monoFont, defaultColor: Self.defaultColor, searchText: searchText, baseAttributedString: nil, startIndex: startIndex)
textView.textStorage?.setAttributedString(attributedText)
// Add newline separator before appending if there's existing content
if textStorage.length > 0 {
let newline = NSAttributedString(string: "\n", attributes: [
.foregroundColor: Self.defaultColor,
.font: Self.monoFont,
])
textStorage.append(newline)
}
// Append to existing text storage instead of replacing
textStorage.append(newLogsAttributedString)
// Update cache to full content
context.coordinator.cachedAttributedString = NSAttributedString(attributedString: textStorage)
} }
let shouldScroll = shouldAutoScroll && logs.count != lastCount let shouldScroll = shouldAutoScroll && logs.count != lastCount
+5 -5
View File
@@ -34,7 +34,7 @@ private struct LogViewContent: View {
previewContent previewContent
} else if viewModel.isEmpty { } else if viewModel.isEmpty {
emptyContent emptyContent
} else if viewModel.filteredLogs.isEmpty { } else if viewModel.visibleLogs.isEmpty {
emptyContent emptyContent
} else { } else {
logScrollView logScrollView
@@ -113,7 +113,7 @@ private struct LogViewContent: View {
ScrollViewReader { reader in ScrollViewReader { reader in
ScrollView { ScrollView {
LazyVStack(alignment: .leading, spacing: 8) { LazyVStack(alignment: .leading, spacing: 8) {
ForEach(viewModel.filteredLogs) { logEntry in ForEach(viewModel.visibleLogs) { logEntry in
Text(highlightedText(for: logEntry.message)) Text(highlightedText(for: logEntry.message))
.font(logFont) .font(logFont)
.focusable() .focusable()
@@ -127,7 +127,7 @@ private struct LogViewContent: View {
.onAppear { .onAppear {
scrollToLastEntry(reader) scrollToLastEntry(reader)
} }
.onChangeCompat(of: viewModel.filteredLogs.count) { _ in .onChangeCompat(of: viewModel.visibleLogs.count) { _ in
if !viewModel.isPaused { if !viewModel.isPaused {
scrollToLastEntry(reader) scrollToLastEntry(reader)
} }
@@ -135,7 +135,7 @@ private struct LogViewContent: View {
} }
#else #else
LogTextView( LogTextView(
logs: viewModel.filteredLogs, logs: viewModel.visibleLogs,
font: logFont, font: logFont,
shouldAutoScroll: !viewModel.isPaused, shouldAutoScroll: !viewModel.isPaused,
searchText: viewModel.searchText searchText: viewModel.searchText
@@ -166,7 +166,7 @@ private struct LogViewContent: View {
#if os(tvOS) #if os(tvOS)
private func scrollToLastEntry(_ reader: ScrollViewProxy) { private func scrollToLastEntry(_ reader: ScrollViewProxy) {
guard let lastEntry = viewModel.filteredLogs.last else { return } guard let lastEntry = viewModel.visibleLogs.last else { return }
withAnimation { withAnimation {
reader.scrollTo(lastEntry.id, anchor: .bottom) reader.scrollTo(lastEntry.id, anchor: .bottom)
} }
@@ -25,9 +25,21 @@ public class LogViewModel: ObservableObject {
private var lastEffectiveLevel: Int? private var lastEffectiveLevel: Int?
private var lastSearchText = "" private var lastSearchText = ""
// Performance optimization: limit visible logs to prevent UI lag
private static let maxVisibleLogs = 1000
public var isEmpty: Bool { commandClient.logList.isEmpty } public var isEmpty: Bool { commandClient.logList.isEmpty }
public var isConnected: Bool { commandClient.isConnected } public var isConnected: Bool { commandClient.isConnected }
// Only show last N logs for performance, but keep all in filteredLogs for export
public var visibleLogs: [LogEntry] {
if filteredLogs.count <= Self.maxVisibleLogs {
return filteredLogs
} else {
return Array(filteredLogs.suffix(Self.maxVisibleLogs))
}
}
public init(commandClient: CommandClient) { public init(commandClient: CommandClient) {
self.commandClient = commandClient self.commandClient = commandClient
+41 -7
View File
@@ -66,6 +66,11 @@ public class CommandClient: ObservableObject {
@Published public var connections: [LibboxConnection]? @Published public var connections: [LibboxConnection]?
public var rawConnections: LibboxConnections? public var rawConnections: LibboxConnections?
// Batch processing for logs
private var pendingLogs: [LogEntry] = []
private var logBatchTimer: DispatchWorkItem?
private let logBatchInterval: TimeInterval = 0.1 // 100ms batch window
public init(_ connectionTypes: [ConnectionType], logMaxLines: Int = 3000) { public init(_ connectionTypes: [ConnectionType], logMaxLines: Int = 3000) {
self.connectionTypes = connectionTypes self.connectionTypes = connectionTypes
self.logMaxLines = logMaxLines self.logMaxLines = logMaxLines
@@ -102,6 +107,20 @@ public class CommandClient: ObservableObject {
} }
} }
private func flushPendingLogs() {
guard !pendingLogs.isEmpty else { return }
// Batch append all pending logs
logList.append(contentsOf: pendingLogs)
pendingLogs.removeAll()
// Trim to max lines if needed
if logList.count > logMaxLines {
let removeCount = logList.count - logMaxLines
logList.removeFirst(removeCount)
}
}
public func filterConnectionsNow() { public func filterConnectionsNow() {
guard let message = rawConnections else { guard let message = rawConnections else {
return return
@@ -217,15 +236,30 @@ public class CommandClient: ObservableObject {
guard let messageList else { guard let messageList else {
return return
} }
// Collect new logs
var newLogs: [LogEntry] = []
while messageList.hasNext() {
let logEntry = messageList.next()!
newLogs.append(LogEntry(level: Int(logEntry.level), message: logEntry.message))
}
guard !newLogs.isEmpty else { return }
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
while messageList.hasNext() { // Add to pending batch
let logEntry = messageList.next()! commandClient.pendingLogs.append(contentsOf: newLogs)
commandClient.logList.append(LogEntry(level: Int(logEntry.level), message: logEntry.message))
} // Cancel existing timer
if commandClient.logList.count > commandClient.logMaxLines { commandClient.logBatchTimer?.cancel()
let removeCount = commandClient.logList.count - commandClient.logMaxLines
commandClient.logList.removeFirst(removeCount) // Schedule batch flush
let workItem = DispatchWorkItem { [weak commandClient] in
guard let commandClient else { return }
commandClient.flushPendingLogs()
} }
commandClient.logBatchTimer = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + commandClient.logBatchInterval, execute: workItem)
} }
} }