Improve performance for Logs
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user