v0.1.1: perf timings + single-save tag writes + coalesced browser rebuild

Diagnosing the Ctrl+I save hang on the Debian machine (GUI thread blocks
past mutter's 5s check-alive):

- lintunes/perf.py: timed() context manager logging wall-clock ms at INFO
  on lintunes.perf — instruments tag/artwork saves, file moves,
  edit_track(s)_fields, the browser rebuild, smart recompute, and the
  debounced JSON flush, so the slow machine can tell us which stage
  eats the time (journalctl --user or a terminal run).
- tagging.write_tags: ONE parse + ONE save per edit. grouping/
  compilation/bpm ride the main save via registered Easy keys
  (GRP1/TCMP/TBPM on EasyID3, cpil on EasyMP4) instead of
  _write_extra_tags re-parsing and re-saving the audio file.
- LibraryView: browser rebuilds coalesce through a 0ms single-shot
  timer — an N-track Get Info edit rebuilds the genre/artist/album
  cascade once instead of N times over the whole library.

Moving the writes off the GUI thread is deliberately deferred until the
Debian timings say which stage dominates.
This commit is contained in:
2026-07-03 18:25:56 -04:00
parent b7089f865b
commit 8e0ba85086
7 changed files with 325 additions and 100 deletions
+18 -1
View File
@@ -8,6 +8,7 @@ from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from lintunes.gui.table_settings import TableSettingsMixin
from lintunes.gui.track_table import TrackTableView
from lintunes.perf import timed
_ARTICLE_RE = re.compile(r"^the\s+", re.IGNORECASE)
@@ -162,6 +163,13 @@ class LibraryView(TableSettingsMixin, QWidget):
manager.track_updated.connect(self.table.model_.refresh_track)
manager.track_fields_edited.connect(self._on_track_fields_edited)
# Coalesces browser rebuilds: a multi-track edit emits
# track_fields_edited once per track, and rebuilding the whole
# genre/artist/album cascade per signal is O(edited × library).
self._browser_rebuild_timer = QTimer(self)
self._browser_rebuild_timer.setSingleShot(True)
self._browser_rebuild_timer.setInterval(0)
self._browser_rebuild_timer.timeout.connect(self._rebuild_browser)
self.reload()
@@ -295,8 +303,17 @@ class LibraryView(TableSettingsMixin, QWidget):
rebuild the lists and re-filter so the renamed value sorts to its new
spot and clicking it still finds its tracks (the plain row repaint from
track_updated leaves the browser stale). Non-browser edits (name, bpm…)
are handled by the proxy's live re-sort, so they need nothing here."""
are handled by the proxy's live re-sort, so they need nothing here.
The rebuild is deferred to a 0 ms single-shot timer so an N-track edit
(which fires this once per track) rebuilds the browser once, not N
times."""
if _BROWSER_FIELDS.intersection(changed_fields):
self._browser_rebuild_timer.start()
def _rebuild_browser(self):
with timed("library browser rebuild (%d tracks)",
len(self._all_tracks)):
self._populate_genres()
self._populate_artists()
self._populate_albums()