perf(qt): recycle the album page's track list, on a uniform-cell ListView

DID NOT DELIVER ITS GOAL. Read this before extending it: the wheel scroll on
a 189-track album is essentially unchanged. The page is correct and the
structure is better, but the theory that motivated the work is not supported.

What the work assumed: the ceiling was the `Repeater`, which has no windowing
and so built all 189 track blocks — each a Column with a disc-header Loader, a
work-header Loader (both gated on DATA, never on the viewport, so every act and
scene header in a Wagner box was alive) and the band-gated row. Roughly a
thousand structural items before counting anything visible.

What it is now: one ListView over a UNIFORM 10px cell tape. Each piece of
content owns a run of cells, its visual item lives in the first cell of the run
and paints over the inert ones — 50px row = 5 cells, 40px header = 4, rail =
29. Uniform cells are the point: measured in a standalone probe, a ListView
with variable delegates ESTIMATES contentHeight and ignores an explicit
override (5703 then 5396 against an exact 5400), which makes the scrollbar
thumb resize and skip as sizes are discovered. With uniform cells,
count * 10 is exact from the first frame, so the scrollbar the owner had just
called the best gesture on the page keeps its behaviour.

Falling out of the design, replacing machinery that was reverted a commit ago:
`cacheBuffer` incubates upcoming delegates asynchronously in idle time, so a
heavy rail is built before it is visible without a hand-rolled band, a settle
Timer, or an unload grace — and without the "reach the bottom, then wait" that
made the owner reject that approach.

Two correctness items that came with it:
  * `header` is a ListView property. The page's own `header` had to become
    `albumHeader` or every `header.x` binding inside the view would silently
    resolve against the view's header instead. One handler was missed in the
    rename and QML refused to create the component at all — the whole page
    rendered black, and no static gate caught it because the failure happens at
    instantiation.
  * `recycleActive` on the shared TrackRow, default true, gates the row's
    Connections. Without it a pooled row keeps listening and consumes a cache
    or favourite settle signal meant for a different track. Every other host
    is unaffected by the default.

WHAT THIS RESULT MEANS, for whoever picks it up: delegate count and
construction cost are NOT what makes this page feel heavy. Removing ~950
structural items changed nothing the owner could feel, which points at
per-frame cost instead — consistent with his own observation that the page is
sluggish ALWAYS, not only while it loads, and with the documented finding that
this shell's bill is full-window presents rather than scene work. Do not
re-attempt a structural rewrite here on intuition. Take the QSG_RENDER_TIMING
split (polish/sync vs render) on this exact album first; the earlier reference
scene measured polish=0ms, render=2ms, swap=12-24ms.

Design and probes came from an independent review; the rebase onto the current
tree, the black-screen fix and the API verification are mine.

cc
This commit is contained in:
vicrodh committed 2026-08-18 23:28:25 -06:00
1 parent 3fdccae22e
commit 047db8ea9f
2 files changed
+477 -370

No files matched your search

+39 -6
View File
@@ -131,6 +131,11 @@ Rectangle {
property bool artPending: false
property bool skelPhase: false
property int artSettleMs: 0
// A ListView reuse-pool item stays alive and keeps receiving signals.
// Hosts that enable reuse flip this attached-state twin from pooled /
// reused handlers so an offstage playing row cannot keep pulsing and an
// old track cannot consume cache/favourite settle signals.
property bool recycleActive: true
// Catalog-backed row? `draggable` is ALREADY the "item.id is a Qobuz
// catalog track id" predicate (the Local Library / ephemeral rows set it
@@ -166,7 +171,7 @@ Rectangle {
}
Connections {
target: QbzShell
enabled: root.hasOfflineCacheSeam
enabled: root.recycleActive && root.hasOfflineCacheSeam
function onTrackCacheStatusChanged(trackId, status, progress) {
if (trackId === (root.item.id || "")) root.cacheStatus = status
}
@@ -315,9 +320,20 @@ Rectangle {
// re-established whenever the host hands over a new row object, so a
// republished document (or a page re-open) overrides a stale local flip.
property bool favorite: root.item.isFavorite === true
onItemChanged: root.favorite = Qt.binding(function () {
return root.item.isFavorite === true
})
onItemChanged: {
// Both properties are writable live state. A signal/click breaks their
// seed binding, so a recycled delegate must explicitly re-establish
// both bindings when ListView hands it a different model row.
root.favorite = Qt.binding(function () {
return root.item.isFavorite === true
})
root.cacheStatus = Qt.binding(function () {
return root.item.cacheStatus !== undefined
? root.item.cacheStatus : 0
})
if (rowMenuLoader.item)
rowMenuLoader.item.close()
}
/// The heart click and the menu's "Add to / Remove from Library" row —
/// ONE implementation, because they used to diverge silently.
@@ -336,6 +352,7 @@ Rectangle {
// republishing a document.
Connections {
target: QbzLibrary
enabled: root.recycleActive
function onLibraryFavoriteChanged(key, value) {
var tid = (root.item && root.item.id !== undefined) ? root.item.id : ""
if (tid !== "" && key === "track:" + tid)
@@ -416,7 +433,7 @@ Rectangle {
// rides QbzShell.pulseMs) — that is what makes the indicator affordable
// here, where a display-rate animation was not.
readonly property bool showBars: root.playIndicatorAnim
&& root.isActive && QbzPlayer.npPlaying && !root.hovered
&& root.recycleActive && root.isActive && QbzPlayer.npPlaying && !root.hovered
&& !root.selectMode
// --- Column geometry: rows/TrackCols.qml, NOT literals ---------------
@@ -593,7 +610,7 @@ Rectangle {
// they ride the shell's frame instead of owning one.
EqualizerBars {
visible: root.showBars
active: root.isActive && QbzPlayer.npPlaying
active: root.recycleActive && root.isActive && QbzPlayer.npPlaying
anchors.centerIn: parent
}
// Selection checkbox — TrackRow.slint:392-419: a 13px disc, accent
@@ -1304,6 +1321,22 @@ Rectangle {
}
return false
}
// Called by a recycling host before this object enters ListView's pool.
// Popups are window-overlay children, so merely hiding the delegate would
// leave them open and tied to the wrong track after reuse.
function releaseForReuse() {
root.endBodyDrag()
if (rowMenuLoader.item)
rowMenuLoader.item.close()
if (trackInfoLoader.item)
trackInfoLoader.item.close()
Qt.callLater(function () {
if (!root.recycleActive) {
rowMenuLoader.active = false
trackInfoLoader.active = false
}
})
}
MouseArea {
id: trArea
anchors.fill: parent
File diff suppressed because it is too large. Load diff