on xenial help sign in
Optimize Wallpaper loading to reduce shell lag

This commit makes the Wallpaper component and the WallpaperResolver load images asynchronously. This improves performance of Lomiri in a few key scenarios: * At startup * On shell resize (switching displays, not orientations. Orientation changes were handled by 37c6afa19) * During wallpaper changes * During testing (on my Ryzen 7 1700 desktop, this reduces the time to run each test which loads the default wallpaper by 100ms.) This also makes a change to the WallpaperResolver so it never loads its final candidate. Since it was created to always return the final candidate regardless of whether it could load or not, there is no reason to check if it loaded. This prevents the default wallpaper from loading even once when it is not needed. It does not prevent two wallpapers from loading if the AccountsService and Gnome wallpapers are set. In all cases, we reduce the wasted image loading by one. I attempted to rewrite WallpaperResolver to fix that issue with both user-set wallpapers loading when only one of them needed to[1], but decided that it was too complex compared to just dropping the default and accepting a wasted image load in limited cases. I wrote more tests to make sure my new WallpaperResolver worked correctly, so I tacked them on here. They can't hurt, they just make regressions easier to spot. Doing this asynchronously means that the wallpaper doesn't load before the element that uses it is shown, though. This would cause an undesirable pop-in effect. To mitigate that, I've added a Rectangle which covers the image until it's loaded, then fades out. It's pretty pleasing, but note that it plays every time a Wallpaper is created. That includes when the Greeter loads, so if someone locks their screen they'll see it turn to black then fade in. When I showed this to people, they asked "what changed?" so I think it's an okay experience. Synchronously-loading images are a pain for QML. Each Image causes the application UI to lag from the time that its source is set until it's fully loaded. This isn't so bad after the first load since the image is cached, but it's a big delay on the first load or when sourceSize changes. [1] https://gist.github.com/UniversalSuperBox/e2d287bc1a65a82dba2decde5fe56a95

commit840777f92fb663a525dbe765155dbcf2a6f7541e authorDalton Durst <dalton@ubports.com> dateThu 3 Jun 2021, 22:01 (5 years ago) parenta81421a
6 files changed+146−26
qml/Components/Wallpaper.qml+13−3 qml/Components/WallpaperResolver.qml+14−4 tests/data/unity/backgrounds/black.png tests/qmltests/Components/tst_Wallpaper.qml+14 tests/qmltests/Components/tst_WallpaperResolver.qml+104−18 tests/qmltests/tst_Shell.qml+1−1
qml/Components/Wallpaper.qml+13−3
@@ -38,9 +38,7 @@ Item {
3838 anchors.fill: parent
3939 fillMode: Image.PreserveAspectCrop
4040
41− // Setting this to true stops the UI from pausing when the shell's
42− // dimensions change but causes the image to flash on screen.
43− asynchronous: false
41+ asynchronous: true
4442
4543 property real oldSourceSize: 0.0
4644 property real intermediarySourceSize: {
@@ -73,6 +71,18 @@ Item {
7371 source: intermediarySourceSize !== -1.0 ? root.source : ""
7472 }
7573
74+ Rectangle {
75+ id: wallpaperFadeRectangle
76+ objectName: "wallpaperFadeRectangle"
77+ color: theme.palette.normal.background
78+ anchors.fill: parent
79+ opacity: image.status === Image.Ready ? 0: 1
80+ visible: opacity !== 0
81+ Behavior on opacity {
82+ UbuntuNumberAnimation { duration: UbuntuAnimation.FastDuration }
83+ }
84+ }
85+
7686 Timer {
7787 id: developerSanityTimer
7888 interval: 2000
qml/Components/WallpaperResolver.qml+14−4
@@ -24,13 +24,21 @@ Item {
2424 // Provide a list of wallpapers to resolve here, preferred ones first
2525 property var candidates: []
2626
27+ property bool cache: true
28+
2729 readonly property url background: {
2830 for (var i = 0; i < repeater.count; i++) {
29− if (repeater.itemAt(i).status === Image.Ready)
31+ var image = repeater.itemAt(i);
32+ var expectedImageSource = Qt.resolvedUrl(candidates[i]);
33+ if (image.source != expectedImageSource)
34+ return "";
35+ if (image.status === Image.Ready)
3036 return candidates[i];
37+ if (image.status === Image.Loading)
38+ return "";
3139 }
32− if (i > 0) {
33− return candidates[i - 1]; // last item is last resort
40+ if (candidates.length > 0) {
41+ return candidates.slice(-1)[0]; // last item is last resort
3442 } else {
3543 return "";
3644 }
@@ -38,9 +46,11 @@ Item {
3846
3947 Repeater {
4048 id: repeater
41− model: root.candidates
49+ model: root.candidates.slice(0, -1)
4250 delegate: Image {
4351 source: modelData
52+ asynchronous: true
53+ cache: root.cache
4454 height: 0
4555 width: 0
4656 sourceSize.height: 1
tests/data/unity/backgrounds/black.pngnew file

Binary file not shown.

tests/qmltests/Components/tst_Wallpaper.qml+14
@@ -54,6 +54,7 @@ Item {
5454
5555 function init() {
5656 root.sourceSize = 0;
57+ root.defaultBackground = "../../data/unity/backgrounds/20x30.png";
5758 }
5859
5960 // Ensures that the Wallpaper's sourceSize causes the image to be
@@ -100,5 +101,18 @@ Item {
100101 // forever.
101102 wait(3000);
102103 }
104+
105+ // Ensures there is a blocker over the wallpaper before it has loaded
106+ // which goes away once loading finishes
107+ function test_wallpaperBlock() {
108+ root.sourceSize = 1;
109+ root.defaultBackground = "";
110+ var wallpaper = createTemporaryObject(wallpaperComponent, root);
111+ var wallpaperFadeRectangle = findChild(wallpaper, "wallpaperFadeRectangle");
112+ compare(wallpaperFadeRectangle.opacity, 1);
113+ root.defaultBackground = "../../data/unity/backgrounds/red.png";
114+ tryCompare(wallpaperFadeRectangle, "opacity", 0);
115+ compare(wallpaperFadeRectangle.visible, false);
116+ }
103117 }
104118}
tests/qmltests/Components/tst_WallpaperResolver.qml+104−18
@@ -1,5 +1,6 @@
11/*
22 * Copyright (C) 2015 Canonical, Ltd.
3+ * Copyright (C) 2021 UBports Foundation
34 *
45 * This program is free software; you can redistribute it and/or modify
56 * it under the terms of the GNU General Public License as published by
@@ -16,20 +17,34 @@
1617
1718import QtQuick 2.4
1819import QtTest 1.0
19−import Ubuntu.Components 1.3
2020import Unity.Test 0.1
2121
2222import "../../../qml/Components"
2323
2424
2525Image {
26− width: units.gu(70)
27− height: units.gu(70)
26+ id: root
27+ width: 70
28+ height: 70
2829
2930 source: wallpaperResolver.background
3031
32+ readonly property url blue: Qt.resolvedUrl("../../data/unity/backgrounds/blue.png")
33+ readonly property url red: Qt.resolvedUrl("../../data/unity/backgrounds/red.png")
34+ readonly property url big: Qt.resolvedUrl("../../graphics/applicationIcons/dash.png")
35+
3136 WallpaperResolver {
3237 id: wallpaperResolver
38+
39+ property var pastBackgrounds: []
40+
41+ onBackgroundChanged: pastBackgrounds.push(background)
42+ }
43+
44+ SignalSpy {
45+ id: spy
46+ signalName: "backgroundChanged"
47+ target: wallpaperResolver
3348 }
3449
3550 UnityTestCase {
@@ -39,37 +54,108 @@ Image {
3954
4055 function test_background_data() {
4156 return [
57+ {tag: "empty-candidates",
58+ list: [],
59+ output: ""},
60+
61+ {tag: "blank-candidate",
62+ list: [""],
63+ output: ""},
64+
65+ {tag: "blank-urls",
66+ list: [Qt.resolvedUrl(""), Qt.resolvedUrl(""), root.blue],
67+ output: root.blue},
68+
69+ {tag: "invalid-urls",
70+ list: [Qt.resolvedUrl("/first"), Qt.resolvedUrl("/middle"), root.blue],
71+ output: root.blue},
72+
73+ {tag: "valid-after-blanks",
74+ list: ["", "", root.red],
75+ output: root.red},
76+
77+ // Ensure that the WallpaperResolver doesn't get stuck if it
78+ // sees the same invalid wallpaper multiple times in a row
79+ {tag: "valid-after-the-same-invalid",
80+ list: ["/first", "/first", "/first", root.red],
81+ output: root.red},
82+
83+ {tag: "naughty",
84+ list: [null, undefined, "", NaN, 1.0, 1, root.red, null],
85+ output: root.red},
86+
4287 {tag: "none-valid",
4388 list: ["/first", "/middle", "/last"],
44− output: "file:///last"},
89+ output: Qt.resolvedUrl("/last")},
4590
4691 {tag: "first-valid",
47− list: [Qt.resolvedUrl("../../data/unity/backgrounds/blue.png"),
48− "/middle", "/last"],
49− output: Qt.resolvedUrl("../../data/unity/backgrounds/blue.png")},
92+ list: [root.blue, "/middle", "/last"],
93+ output: root.blue},
5094
5195 {tag: "middle-valid",
52− list: ["/first",
53− Qt.resolvedUrl("../../data/unity/backgrounds/red.png"),
54− "/last"],
55− output: Qt.resolvedUrl("../../data/unity/backgrounds/red.png")},
96+ list: ["/first", root.red, "/last"],
97+ output: root.red},
5698
5799 {tag: "last-valid",
58− list: ["/first",
59− "/middle",
60− Qt.resolvedUrl("../../data/unity/backgrounds/red.png")],
61− output: Qt.resolvedUrl("../../data/unity/backgrounds/red.png")},
100+ list: ["/first", "/middle", root.red],
101+ output: root.red},
62102
63103 {tag: "multiple-valid",
64− list: [Qt.resolvedUrl("../../data/unity/backgrounds/blue.png"),
65− Qt.resolvedUrl("../../data/unity/backgrounds/red.png")],
66− output: Qt.resolvedUrl("../../data/unity/backgrounds/blue.png")},
104+ list: [root.blue, root.red],
105+ output: root.blue},
106+
107+ {tag: "multiple-valid-after-multiple-invalid",
108+ list: ["/first", "/middle", "/last", root.blue, root.red],
109+ output: root.blue},
110+
67111 ]
68112 }
69113
114+ function init() {
115+ // Make sure we don't have our next test compare() to the results
116+ // of the last test by exercising the resolver
117+ wallpaperResolver.cache = true;
118+ wallpaperResolver.candidates = [];
119+ tryCompare(wallpaperResolver, "background", "");
120+ wallpaperResolver.pastBackgrounds = [];
121+ wallpaperResolver.candidates = [root.blue, root.blue];
122+ tryCompare(wallpaperResolver, "background", root.blue);
123+ wallpaperResolver.candidates = [];
124+ tryCompare(wallpaperResolver, "background", "");
125+ tryCompare(wallpaperResolver, "pastBackgrounds", [root.blue, Qt.resolvedUrl("")])
126+ wallpaperResolver.pastBackgrounds = [];
127+ }
128+
70129 function test_background(data) {
71130 wallpaperResolver.candidates = data.list;
72131 tryCompare(wallpaperResolver, "background", data.output);
73132 }
133+
134+ function test_reload_with_blanks() {
135+ wallpaperResolver.candidates = ["", "", root.red];
136+ tryCompare(wallpaperResolver, "background", root.red);
137+ wallpaperResolver.candidates = ["", "", root.blue];
138+ tryCompare(wallpaperResolver, "background", root.blue);
139+ }
140+
141+ // WallpaperResolver loads images asynchronously. It's important to make
142+ // sure that it never returns the wrong image just because they loaded
143+ // in the wrong order. So we set its images 100 times.
144+ function test_images_changing() {
145+ wallpaperResolver.cache = false;
146+ for (var i = 0; i < 50; i++) {
147+ // We don't spy on the first one because the wallpaper doesn't
148+ // transition from a set url to "" the first time.
149+ wallpaperResolver.candidates = [root.red, root.big, root.blue];
150+ tryCompare(wallpaperResolver, "background", root.red);
151+
152+ spy.clear();
153+ wallpaperResolver.pastBackgrounds = [];
154+ wallpaperResolver.candidates = [root.big, root.blue, root.red];
155+ tryCompare(wallpaperResolver, "background", root.big);
156+ compare(wallpaperResolver.pastBackgrounds, ["", root.big])
157+ compare(spy.count, 4);
158+ }
159+ }
74160 }
75161}
tests/qmltests/tst_Shell.qml+1−1
@@ -1307,7 +1307,7 @@ Rectangle {
13071307
13081308 AccountsService.backgroundFile = Qt.resolvedUrl("../graphics/applicationIcons/dash.png");
13091309 tryCompare(greeter, "hasCustomBackground", true);
1310− compare(wallpaperResolver.background, AccountsService.backgroundFile);
1310+ tryCompare(wallpaperResolver, "background", AccountsService.backgroundFile);
13111311 }
13121312
13131313 function test_tapOnRightEdgeReachesApplicationSurface() {