mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-05 13:28:03 +03:00
Optimize unified logging performance
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -54,3 +54,4 @@ class LogEntry:
|
||||
categoryTranslatable: bool = False
|
||||
source: str = ''
|
||||
severity: str = ''
|
||||
sequence: int = 0
|
||||
|
||||
@@ -23,6 +23,7 @@ from Furious.Models.Logging import LogCategory, LogEntry
|
||||
|
||||
from PySide6 import QtCore
|
||||
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@@ -54,17 +55,28 @@ def formatLogEntry(entry: LogEntry) -> str:
|
||||
class LogManager(QtCore.QObject):
|
||||
"""Own the application-wide categorized log stream."""
|
||||
|
||||
DefaultMaximumEntries = 10_000
|
||||
|
||||
categoryRegistered = QtCore.Signal(object)
|
||||
entryAdded = QtCore.Signal(object)
|
||||
entriesCleared = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent=None, *, maximumEntries=DefaultMaximumEntries):
|
||||
"""Initialize the category registry and thread-safe entry collection."""
|
||||
super().__init__(parent)
|
||||
|
||||
if (
|
||||
isinstance(maximumEntries, bool)
|
||||
or not isinstance(maximumEntries, int)
|
||||
or maximumEntries <= 0
|
||||
):
|
||||
raise ValueError('maximumEntries must be a positive integer')
|
||||
|
||||
self._lock = threading.RLock()
|
||||
self._categories: dict[str, LogCategory] = {}
|
||||
self._entries: list[LogEntry] = []
|
||||
self._maximumEntries = maximumEntries
|
||||
self._entries: deque[LogEntry] = deque(maxlen=maximumEntries)
|
||||
self._sequence = 0
|
||||
|
||||
self.registerCategory(
|
||||
LogCategory(
|
||||
@@ -89,6 +101,11 @@ class LogManager(QtCore.QObject):
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def maximumEntries(self) -> int:
|
||||
"""Return the maximum number of structured entries retained in memory."""
|
||||
return self._maximumEntries
|
||||
|
||||
def registerCategory(self, category: LogCategory) -> LogCategory:
|
||||
"""Register a filterable category and publish it exactly once."""
|
||||
if not isinstance(category, LogCategory):
|
||||
@@ -161,6 +178,8 @@ class LogManager(QtCore.QObject):
|
||||
elif not isinstance(timestamp, datetime):
|
||||
raise TypeError('log timestamp must be a datetime')
|
||||
|
||||
self._sequence += 1
|
||||
|
||||
entry = LogEntry(
|
||||
message=str(message).rstrip('\r\n'),
|
||||
timestamp=timestamp,
|
||||
@@ -169,7 +188,9 @@ class LogManager(QtCore.QObject):
|
||||
categoryTranslatable=category.translatable,
|
||||
source=str(source) if source else '',
|
||||
severity=str(severity) if severity else '',
|
||||
sequence=self._sequence,
|
||||
)
|
||||
|
||||
self._entries.append(entry)
|
||||
|
||||
self.entryAdded.emit(entry)
|
||||
@@ -203,13 +224,22 @@ class LogManager(QtCore.QObject):
|
||||
|
||||
def entries(self, categoryId: Optional[str] = None) -> tuple[LogEntry, ...]:
|
||||
"""Return an immutable snapshot, optionally filtered by category."""
|
||||
return self.snapshot(categoryId)[1]
|
||||
|
||||
def snapshot(
|
||||
self,
|
||||
categoryId: Optional[str] = None,
|
||||
) -> tuple[int, tuple[LogEntry, ...]]:
|
||||
"""Return the current sequence and its immutable filtered entries."""
|
||||
with self._lock:
|
||||
if categoryId in (None, ALL_LOGS_FILTER):
|
||||
return tuple(self._entries)
|
||||
entries = tuple(self._entries)
|
||||
else:
|
||||
entries = tuple(
|
||||
entry for entry in self._entries if entry.categoryId == categoryId
|
||||
)
|
||||
|
||||
return tuple(
|
||||
entry for entry in self._entries if entry.categoryId == categoryId
|
||||
)
|
||||
return self._sequence, entries
|
||||
|
||||
def clear(
|
||||
self,
|
||||
@@ -238,14 +268,20 @@ class LogManager(QtCore.QObject):
|
||||
|
||||
if clearedCategoryIds is None:
|
||||
changed = bool(self._entries)
|
||||
|
||||
self._entries.clear()
|
||||
else:
|
||||
oldLength = len(self._entries)
|
||||
self._entries = [
|
||||
entry
|
||||
for entry in self._entries
|
||||
if entry.categoryId not in clearedCategoryIds
|
||||
]
|
||||
|
||||
self._entries = deque(
|
||||
(
|
||||
entry
|
||||
for entry in self._entries
|
||||
if entry.categoryId not in clearedCategoryIds
|
||||
),
|
||||
maxlen=self._maximumEntries,
|
||||
)
|
||||
|
||||
changed = len(self._entries) != oldLength
|
||||
|
||||
if changed:
|
||||
|
||||
@@ -143,6 +143,10 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
pointSizeSettingsName='LogViewerWidgetPointSize',
|
||||
)
|
||||
self.textBrowser.setLineWrapMode(DraculaTextBrowser.LineWrapMode.NoWrap)
|
||||
self.textBrowser.document().setMaximumBlockCount(manager.maximumEntries)
|
||||
|
||||
self._entriesDirty = True
|
||||
self._renderedSequence = 0
|
||||
|
||||
filterLayout = QHBoxLayout()
|
||||
filterLayout.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -247,7 +251,6 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
self._registerMenuShortcuts(menu)
|
||||
|
||||
self._populateFilters(self._preferredFilter)
|
||||
self._refreshEntries()
|
||||
|
||||
self.filterComboBox.currentIndexChanged.connect(self._filterChanged)
|
||||
self.manager.categoryRegistered.connect(self._categoryRegistered)
|
||||
@@ -301,15 +304,25 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
def _refreshEntries(self):
|
||||
"""Render the current immutable filtered-entry snapshot."""
|
||||
selectedCategoryId = self.filterComboBox.currentData()
|
||||
sequence, entries = self.manager.snapshot(selectedCategoryId)
|
||||
|
||||
content = '\n'.join(formatLogEntry(entry) for entry in entries)
|
||||
|
||||
content = '\n'.join(
|
||||
formatLogEntry(entry) for entry in self.manager.entries(selectedCategoryId)
|
||||
)
|
||||
self.textBrowser.setPlainText(content)
|
||||
self._renderedSequence = sequence
|
||||
|
||||
scrollbar = self.textBrowser.verticalScrollBar()
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
|
||||
self._entriesDirty = False
|
||||
|
||||
def _refreshEntriesIfVisible(self):
|
||||
"""Render pending entries only while this page is actually visible."""
|
||||
if self.isVisible():
|
||||
self._refreshEntries()
|
||||
else:
|
||||
self._entriesDirty = True
|
||||
|
||||
@QtCore.Slot(int)
|
||||
def _filterChanged(self, _index: int):
|
||||
"""Persist and apply the category selected by the user."""
|
||||
@@ -322,7 +335,7 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
|
||||
AppSettings.set('LogViewerSelectedCategory', categoryId)
|
||||
|
||||
self._refreshEntries()
|
||||
self._refreshEntriesIfVisible()
|
||||
|
||||
@QtCore.Slot(object)
|
||||
def _categoryRegistered(self, category):
|
||||
@@ -338,6 +351,20 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
@QtCore.Slot(object)
|
||||
def _entryAdded(self, entry):
|
||||
"""Append an entry when it matches the active filter."""
|
||||
if not self.isVisible():
|
||||
self._entriesDirty = True
|
||||
|
||||
return
|
||||
|
||||
if entry.sequence <= self._renderedSequence:
|
||||
return
|
||||
|
||||
if entry.sequence != self._renderedSequence + 1:
|
||||
self._refreshEntries()
|
||||
|
||||
return
|
||||
|
||||
self._renderedSequence = entry.sequence
|
||||
categoryId = self.filterComboBox.currentData()
|
||||
|
||||
if categoryId in (ALL_LOGS_FILTER, entry.categoryId):
|
||||
@@ -346,7 +373,14 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
@QtCore.Slot(object)
|
||||
def _entriesCleared(self, _categoryIds):
|
||||
"""Refresh the presentation after the underlying collection changes."""
|
||||
self._refreshEntries()
|
||||
self._refreshEntriesIfVisible()
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Render entries accumulated while the page was hidden."""
|
||||
super().showEvent(event)
|
||||
|
||||
if self._entriesDirty:
|
||||
self._refreshEntries()
|
||||
|
||||
def plainText(self) -> str:
|
||||
"""Return the plain text currently shown by the selected filter."""
|
||||
@@ -364,4 +398,4 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
self.pageTitleLabel.setText(_('Log'))
|
||||
self.filterLabel.setText(_('Log Type'))
|
||||
self._populateFilters(selectedCategoryId)
|
||||
self._refreshEntries()
|
||||
self._refreshEntriesIfVisible()
|
||||
|
||||
Reference in New Issue
Block a user