perf(queue): stop a finished track refetching the whole library
`recordPlay` emitted `TrackMetadataChanged`, which the frontend correctly reads as "tags were rewritten" and answers by discarding every cached collection: measured at 8 binding calls, 71.18 MB across the IPC and a 765 ms longest task per two track changes at 50 000 tracks — once per song, while clearing the user's selection. It now emits `TrackPlayCountChanged` with everything needed to patch the one track in place, read back with `UPDATE ... RETURNING` so the count cannot drift from the stored one. Measured after: 0 calls, 0 MB, 0 ms.
This commit is contained in:
@@ -36,14 +36,25 @@ func (q *Queue) recordPlay(audioFileID int64) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update denormalized columns on audio_files.
|
||||
_, err = q.db.ExecContext(
|
||||
// Update the denormalized columns on audio_files, and read back what
|
||||
// they became in the same statement: the event below has to carry the
|
||||
// new values, and a follow-up SELECT could race another play.
|
||||
//
|
||||
// RETURNING requires the writer connection — the read pool is a
|
||||
// separate sql.DB, and this is a write.
|
||||
var (
|
||||
playCount int64
|
||||
filePath string
|
||||
)
|
||||
|
||||
err = q.db.QueryRowWriter(
|
||||
`UPDATE audio_files
|
||||
SET play_count = play_count + 1,
|
||||
last_played = ?
|
||||
WHERE id = ?`,
|
||||
WHERE id = ?
|
||||
RETURNING play_count, file_path`,
|
||||
now, audioFileID,
|
||||
)
|
||||
).Scan(&playCount, &filePath)
|
||||
if err != nil {
|
||||
q.logger.Error(
|
||||
"failed to update play count",
|
||||
@@ -57,8 +68,22 @@ func (q *Queue) recordPlay(audioFileID int64) {
|
||||
q.logger.Info(
|
||||
"Play recorded",
|
||||
"audioFileId", audioFileID,
|
||||
"playCount", playCount,
|
||||
)
|
||||
|
||||
// Notify frontend so the track list refreshes play count.
|
||||
events.Emit(q.ctx, events.TrackMetadataChanged)
|
||||
// Deliberately NOT TrackMetadataChanged. That event means "the tags
|
||||
// on disk were rewritten", which can change an album, an artist or a
|
||||
// genre, so the frontend answers it by discarding its whole library
|
||||
// cache and refetching — measured at ~37 MB across the IPC and ~0.8 s
|
||||
// of blocked main thread, once per finished song, plus clearing
|
||||
// whatever the user had selected in the track list (perf.C1/C2).
|
||||
//
|
||||
// A play count is one integer on one row, and this payload carries
|
||||
// enough for the frontend to patch it in place.
|
||||
events.Emit(q.ctx, events.TrackPlayCountChanged, map[string]any{
|
||||
"audioFileId": audioFileID,
|
||||
"filePath": filePath,
|
||||
"playCount": playCount,
|
||||
"lastPlayed": now,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user