perf: cut DB scans, timeline repaints, and per-frame allocations

Database:
- Enable WAL + synchronous=NORMAL + bigger cache pragmas
- Add (profile, filename) index covering the hot queries
- _refresh_playlist_checks: one get_clip_counts_grouped() scan for the whole
  profile instead of one query per file (was O(N) full scans per keystroke/
  tab switch/file load)

Timeline (60fps playback):
- set_play_position only repaints when the playhead moves a whole pixel or the
  view scrolls (≈30x fewer full repaints in non-zoomed playback)
- Cache all per-paint QColor/QPen objects and the other-folder color table in
  __init__ instead of allocating them every frame; drop the per-paint
  visible-markers list comprehension

File load / startup:
- PlaylistWidget stats files for the missing-set only when paths change, not on
  every filter keystroke
- Cache the vid-folder lookup (DB + os.listdir) per (file, folder) so spinner
  ticks don't repeat it; m-counter still recomputed so it stays correct
- Swap the waveform worker without blocking the UI thread (no wait(1000))
- Defer the changelog modal so the window is interactive first

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 19:50:41 +02:00
parent dbd8e6a8ac
commit b738a19304
2 changed files with 130 additions and 41 deletions
+36
View File
@@ -24,6 +24,18 @@ class ProcessedDB:
self._lock = threading.Lock()
try:
self._con = sqlite3.connect(db_path, check_same_thread=False)
# Performance pragmas: WAL cuts lock contention and fsync cost,
# a bigger page cache keeps hot scans in memory.
for pragma in (
"PRAGMA journal_mode = WAL",
"PRAGMA synchronous = NORMAL",
"PRAGMA temp_store = MEMORY",
"PRAGMA cache_size = -65536", # ~64 MB
):
try:
self._con.execute(pragma)
except sqlite3.Error:
pass
self._migrate()
self._enabled = True
_log(f"DB opened: {db_path}")
@@ -85,6 +97,11 @@ class ProcessedDB:
self._con.execute(
"CREATE INDEX IF NOT EXISTS idx_filename ON processed(filename)"
)
# Most hot queries filter by profile, often with filename too.
self._con.execute(
"CREATE INDEX IF NOT EXISTS idx_profile_filename"
" ON processed(profile, filename)"
)
self._con.execute(
"CREATE TABLE IF NOT EXISTS hidden_files ("
" filename TEXT NOT NULL,"
@@ -552,6 +569,25 @@ class ProcessedDB:
counts[folder] = counts.get(folder, 0) + 1
return counts
def get_clip_counts_grouped(self, profile: str = "default"
) -> dict[str, dict[str, int]]:
"""Return ``{filename: {export_folder: count}}`` for a whole profile
in a single scan (replaces N per-file queries on the hot path)."""
if not self._enabled:
return {}
rows = self._con.execute(
"SELECT filename, output_path FROM processed WHERE profile = ?",
(profile,),
).fetchall()
out: dict[str, dict[str, int]] = {}
for fn, op in rows:
folder = os.path.basename(os.path.dirname(os.path.dirname(op)))
d = out.get(fn)
if d is None:
d = out[fn] = {}
d[folder] = d.get(folder, 0) + 1
return out
def get_all_folder_counts(self, profile: str = "default") -> dict[str, int]:
"""Return clip counts per export folder across all videos in *profile*.