Skip to content

Stop the workspace row rebuilding itself for no visible change - #9440

Open
VykosMolt wants to merge 1 commit into
omacom:quattrofrom
VykosMolt:perf/workspace-row-rebuild
Open

Stop the workspace row rebuilding itself for no visible change#9440
VykosMolt wants to merge 1 commit into
omacom:quattrofrom
VykosMolt:perf/workspace-row-rebuild

Conversation

@VykosMolt

Copy link
Copy Markdown

A Repeater compares its model by identity, not by contents: hand it a new array and it destroys every delegate and builds them again, however alike the two arrays are. The row's model is root.workspaceIds(), a function call in a binding that allocates a fresh array every time it runs, and it re-runs whenever Hyprland's workspace list changes. columns: calls the same function a second time.

Hyprland's workspace list changes when a workspace is created or destroyed, which is what happens the moment you touch an empty one. Moving between workspaces that hold windows changes nothing and is smooth. Pressing SUPER+4 from an occupied workspace creates 4 and rebuilds the whole row, and leaving 4 empty destroys it and rebuilds the row again. That is why the stutter comes and goes rather than being there every time.

The waste is worse than it first looks. The row always offers 1 through 5, so creating workspace 4 does not change what it draws at all — the ids are [1,2,3,4,5] before and after. Every one of those rebuilds produces an identical row. It only genuinely changes when a workspace outside 1-5 appears or goes away.

The ids now live in a property that is replaced only when the ids really differ, so the array's identity survives a change that does not alter the row.

The delegate's workspaceById() goes with it. Each delegate scanned the whole workspace list to find its own workspace, which made drawing the row O(n²) every time it was rebuilt. The map is built once per change instead.

The model math moved to WorkspacesModel.js, matching MenuModel.js and NightlightModel.js, so it can be unit tested under node.

Testing

workspaces-model-test.sh is new. Under node it checks the row always offering 1-5, a workspace outside that range joining and leaving, ids out of range and junk entries being ignored, and then the case this is about: stableIds returns the same array object when creating or destroying a workspace inside 1-5, and a different one when the row genuinely changes. Same length with different ids must not be mistaken for unchanged, and a first run with nothing to compare against still returns the new array.

Against the QML it checks that the widget assigns through stableIds, that neither model: nor columns: allocates a fresh array in a binding any more, and that the delegate reads the prebuilt map rather than scanning. The whole file fails on quattro, where WorkspacesModel.js does not exist.

While moving the model math out I made it hold the workspace list more defensively than the inline version did: a null entry in Hyprland.workspaces.values used to throw reading .id, and a non-numeric id used to be pushed into the row. Both are covered.

./test/shell — 218 of 222 files pass; the four failures (config-test, runtime-smoke-test, snapper-test, unowned-system-paths-test) fail identically on unmodified quattro on this machine.

This touches the same file as #9419, which changes how the click is dispatched. They are independent changes to different parts of the widget; whichever lands second needs a trivial rebase.

This comes out of https://github.com/VykosMolt/omarchy-desktop, where I have been running the Quattro shell as a plain Arch session and fixing what turned up.

A Repeater compares its model by identity, not by contents: hand it a new array
and it destroys every delegate and builds them again, however alike the two
arrays are. The row's model was `root.workspaceIds()`, a function call in a
binding that allocated a fresh array every time it ran, and it re-ran whenever
Hyprland's workspace list changed. `columns:` called the same function a second
time.

Hyprland's workspace list changes when a workspace is created or destroyed,
which is what happens the moment you touch an empty one. Moving between
workspaces that hold windows changes nothing and is smooth. Pressing SUPER+4
from an occupied workspace created 4 and rebuilt the whole row, and leaving 4
empty destroyed it and rebuilt the row again. That is why the stutter came and
went rather than being there every time.

The waste is worse than it first looks. The row always offers 1 through 5, so
creating workspace 4 does not change what it draws at all: the ids are
[1,2,3,4,5] before and after. Every one of those rebuilds produced an identical
row. It only genuinely changes when a workspace outside 1-5 appears or goes
away.

The ids now live in a property that is replaced only when the ids really differ,
so the array's identity survives a change that does not alter the row.

The delegate's `workspaceById()` went with it. Each delegate scanned the whole
workspace list to find its own workspace, which made drawing the row O(n^2)
every time it was rebuilt. The map is now built once per change.

The model math moved to WorkspacesModel.js, matching MenuModel.js and
NightlightModel.js, so it can be unit tested under node. It also holds a
workspace list more defensively than the inline version did: a null entry in
Hyprland.workspaces.values used to throw reading `.id`, and a non-numeric id
used to be pushed into the row.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant