From e5e19b9aa25a573ebac6855b83a0ea967f7a0192 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:13:53 -0600 Subject: [PATCH] Revert "perf(qt): build only the discover rails that fit the viewport" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit dc8cb1fa3. It broke delegate creation on a real, populated Discover page. Measured by running both binaries against the owner's own profile: the pre-change build logs ONE (benign) QML line, this one logged FOURTEEN — 7x HomeView.qml: Object or context destroyed during incubation 4x HomeView.qml:511:27: QML Component: Cannot create delegate 2x HomeView.qml:1379:39: QML Component: Cannot create delegate 1x HomeView.qml:791:27: QML Component: Cannot create delegate "Cannot create delegate" is not cosmetic — those cards do not exist. The ramp opens an ~80 ms window in which rails are still being built, and the home document republishes inside it (the artwork pass), which rebuilds the whole Repeater and destroys ListViews mid-incubation. The old one-pass mount had no such window. The measured navigation win does NOT depend on this. The acknowledgement frame comes from the two-phase route commit (5b274cab5), which is what took a click from 590 ms of frozen window to 73-90 ms across five routes; and the adversarial pass on the blank-pane question already refuted the idea that these rails were what filled it — Library has no ramp and shows the same shape. So this was cost without an isolated benefit, and it is going out rather than being patched at 5am on a sleeping owner's tree. If it comes back it needs a model-republish interlock (stop or complete the ramp when sectionsModel changes) and a check against a FULL Discover page, which is the case that fails. cc --- crates/qbz-qt/qml/views/HomeView.qml | 51 +--------------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/crates/qbz-qt/qml/views/HomeView.qml b/crates/qbz-qt/qml/views/HomeView.qml index 8242a6d33..36379f336 100644 --- a/crates/qbz-qt/qml/views/HomeView.qml +++ b/crates/qbz-qt/qml/views/HomeView.qml @@ -1198,54 +1198,11 @@ Rectangle { width: parent ? parent.width : 0 spacing: 40 - // ── PROGRESSIVE MOUNT — the rails BELOW THE FOLD are not built with - // the page ────────────────────────────────────────────────────── - // - // 2026-08-17 fixed the tabs (`visible: false` does not stop QML from - // instantiating, so all four Discover tabs were built on every mount). - // This is the same disease one level down, and it is what was left: - // a Repeater builds EVERY delegate in its model, so a ten-section tab - // built ten rails — and each rail's horizontal ListView then created - // its own delegates during the first layout pass — while the viewport - // has room for about two and a half. - // - // Measured on 2026-08-21 by capturing the real window frame by frame: - // Discover > Home froze for 590 ms with ZERO pixels changing, and the - // Loader's own stopwatch attributed only 263 ms of that to the mount. - // The rest was `to-idle`: the layout and delegate refills the mount - // DEFERS, i.e. the seven rails nobody could see paying for their cards - // before the GUI thread could reach the event loop and let a frame out. - // - // So the seed is "one viewport" and the rest arrive after the first - // frame. A rail is ~328px tall with the Column spacing, the content - // pane is ~740px, so three covers the fold with room to spare; +2 per - // tick reaches even the twelve-section For You tab inside ~80 ms, - // which is far shorter than the flick that would be needed to outrun - // it. This is NOT `asynchronous: true` (see ContentRouter's header — - // incubation is time-sliced and was reverted the same day): every rail - // is still built synchronously, there are simply fewer of them in the - // turn that owes the user a frame. - property int budget: 3 - readonly property int railCount: rails.sectionsModel ? rails.sectionsModel.length : 0 - // Bounded by construction: it stops itself the moment the budget - // covers the model, and nothing restarts it (a republish rebuilds the - // delegates but the budget stays where it got to — the progressive - // path is for the MOUNT, not for every artwork refresh). - Timer { - interval: 16 - repeat: true - running: rails.budget < rails.railCount - onTriggered: rails.budget += 2 - } - Repeater { model: sectionsModel delegate: Loader { required property var modelData - required property int index property string railTab: rails.tabId - // NOT built yet — see PROGRESSIVE MOUNT above. - active: index < rails.budget width: parent ? parent.width : 0 // The pinned slot is always in the document (it is where the // discover prefs put the rail); it renders only once the @@ -1259,13 +1216,7 @@ Rectangle { // `ForYouState.radio-stations.length > 0` / // `spotlight-visible` gates. A Column skips invisible children // entirely, spacing included, so an empty one leaves no gap. - // `active &&` is load-bearing: a Column skips INVISIBLE - // children entirely, spacing included, but an active:false - // Loader is still visible and 0px tall — which would reserve - // one 40px spacing slot per pending rail and then collapse it - // as the rail lands, i.e. the page reflowing under the user. - visible: !active ? false - : modelData.kind === "pinned" ? root.pinnedItems.length > 0 + visible: modelData.kind === "pinned" ? root.pinnedItems.length > 0 : modelData.kind === "radio" ? root.radioStations.length > 0 : modelData.kind === "spotlight" ? root.spotlight.visible === true : root.isWeeklySlot(modelData.id) ? root.weeklyRows(modelData.id).length > 0