v0.7.0: startup speed — the 21k-track library stops freezing
Three reported symptoms, one shape: every operation was whole-library, whole-file, on the Qt main thread. Measured on the real library (21,482 tracks, 506 playlists, 15 MB library.json). The track table sorted through a QSortFilterProxyModel, which asks data() for a value on every comparison — 580k Python round trips, 8.5 s per table load, paid again on every reload. TrackTableModel now keeps _tracks in canonical (playlist) order plus an _order index list and sorts a key list computed once per track. Nothing used the proxy's filtering. Sorting by # is the identity order, so "source row" still means "playlist position" for drag-reorder. 8.5 s -> 0.13 s; MainWindow() 18.1 s -> 0.79 s. Smart rules compile to closures once per evaluate() instead of being re-dispatched per track — the operator lookup, casefolding the query and parsing the rule's own date constants all leave the 21k-iteration loop (_match_date was re-parsing its own constant 21,482 times per rule). Verified identical membership against the old evaluator on all 20 real smart playlists. 2.54 s -> 0.72 s, and it now runs after the first paint. Also: _reconcile_track skips two to_dict() round trips per unchanged track (MERGEABLE_FIELDS in the resolver is the single source of truth for what a merge touches); no git subprocess on the startup path (Updater.enabled is probed lazily on the background thread, version-button tooltip deferred); and .resolved/ is pruned to the newest 10 snapshots — it had reached 1.7 GB across 73 snapshots inside the Syncthing share. Time to interactive window ~15 s -> 3.2 s. The mid-session freeze when Syncthing delivers a change — a full reload + recompute + table rebuild with the window already on screen, which is what GNOME was offering to force-quit — ~16 s -> 3.8 s. The merge rework itself (playlist ordering, per-machine play journal, quieting the dialog) is queued in TASKS.md as Round 36. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,51 @@
|
||||
## Done
|
||||
|
||||
### Round 35 (2026-08-19) — Startup speed: the 21k-track library stops freezing (v0.7.0)
|
||||
|
||||
Reported as three separate complaints — "startup takes a minute or two and GNOME
|
||||
offers to force quit", a playlist insert that jumped to the end, and the pile of
|
||||
merge dialogs — which turned out to share one shape: every operation was
|
||||
whole-library, whole-file, on the Qt main thread. Measured against the real
|
||||
library (21,482 tracks, 506 playlists, 15 MB `library.json`).
|
||||
|
||||
- [x] **The track table sorts itself.** `TrackTableView` ran its model through a
|
||||
`QSortFilterProxyModel`, which asks `data()` for a value on *every
|
||||
comparison* — 580k Python round trips, **8.5 s per table load**, paid again
|
||||
on every reload. The model now keeps `_tracks` in canonical (playlist)
|
||||
order plus an `_order` index list, and sorts a key list computed once per
|
||||
track. Nothing used the proxy's filtering (the column browser filters by
|
||||
handing `set_tracks` a shorter list). Sorting by `#` is the identity order,
|
||||
so "source row" still means "playlist position" for drag-reorder.
|
||||
**8.5 s → 0.13 s.** `MainWindow()` **18.1 s → 0.79 s.**
|
||||
- [x] **Smart rules compile to closures once per `evaluate()`** instead of being
|
||||
re-dispatched per track: the operator lookup, casefolding the query, and
|
||||
parsing the rule's own date constants all leave the 21k-iteration loop
|
||||
(`_match_date` was re-parsing its own constant 21,482 times per rule).
|
||||
Verified against the previous implementation on all 20 real smart
|
||||
playlists: **identical membership, 0 mismatches**. **2.54 s → 0.72 s.**
|
||||
- [x] Smart recompute deferred until after the first paint (`main.py`), so the
|
||||
compositor always has a window that answers.
|
||||
- [x] `_reconcile_track` skips the two `to_dict()` round trips when a track is
|
||||
unchanged (`MERGEABLE_FIELDS` in the resolver is now the single source of
|
||||
truth for what a merge can touch). A sync changes a handful of tracks out
|
||||
of 21k; this was most of the cost of a reload.
|
||||
- [x] No git subprocess on the startup path: `Updater.enabled` is probed lazily
|
||||
on the background check thread (it was two `rev-parse` calls with 15 s
|
||||
timeouts in `__init__`), and the version button loads its commit-hash
|
||||
tooltip after the window is up.
|
||||
- [x] `.resolved/` is pruned to the newest 10 snapshots. It had reached **1.7 GB
|
||||
across 73 snapshots**, never pruned — and it lives inside the Syncthing
|
||||
share, so every 15 MB pre-merge copy was being replicated to the other
|
||||
machine.
|
||||
|
||||
Net: **time-to-interactive-window ~15 s → 3.2 s**, and the mid-session freeze
|
||||
when Syncthing delivers a change (which is what GNOME was actually offering to
|
||||
force-quit — a full reload + recompute + table rebuild with the window already
|
||||
on screen) **~16 s → 3.8 s**.
|
||||
|
||||
Not in this round, deliberately: the merge rework itself (playlist ordering,
|
||||
per-machine play journal, quieting the dialog) — see TASKS.md.
|
||||
|
||||
### Round 34 (2026-08-15) — The parking brake: stop playing audio at 3am (v0.6.1)
|
||||
|
||||
The long-running "LinTunes plays by itself" haunting, finally attributed and
|
||||
|
||||
Reference in New Issue
Block a user