Keep panel loaders across plugin changes so the OSD loads once (#13439)
Every plugin change handed the panel Instantiator a fresh array, which rebuilds every panel. The panels were rebuilt four times in the first 700ms of startup, each rebuild starting a new asynchronous load of every keepLoaded panel while the last was still in flight. Under load two OSDs could run at once, each registering the osd IPC handler: the runtime smoke test's intermittent "another handler is registered for target osd". The entries now live in a ListModel synced in place. A panel whose plugin still loads the same way keeps its loader, so startup adds each panel once, and a full plugin reload still clears the model to rebuild from fresh code. Under a six-core busy load the smoke test hit the duplicate in 1 of 5 runs on quattro and in none of 10 with this change. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5929a9b80c
commit
0066ea216b
2 files changed
+77
-11
No files matched your search
+47
-11
@@ -1235,7 +1235,7 @@ ShellRoot {
|
|||||||
|
|
||||||
function unloadPanels() {
|
function unloadPanels() {
|
||||||
for (var id in panelLoaders) hide(id)
|
for (var id in panelLoaders) hide(id)
|
||||||
panelEntries = []
|
panelEntryModel.clear()
|
||||||
panelLoaders = ({})
|
panelLoaders = ({})
|
||||||
pendingPayloads = ({})
|
pendingPayloads = ({})
|
||||||
openPanelIds = ({})
|
openPanelIds = ({})
|
||||||
@@ -1284,7 +1284,43 @@ ShellRoot {
|
|||||||
// One Loader per discoverable panel/overlay/menu plugin. Active when the
|
// One Loader per discoverable panel/overlay/menu plugin. Active when the
|
||||||
// host marks it open. The Loader holds onto the instance while active so the
|
// host marks it open. The Loader holds onto the instance while active so the
|
||||||
// plugin's FloatingWindow + state survive between summons within a session.
|
// plugin's FloatingWindow + state survive between summons within a session.
|
||||||
property var panelEntries: []
|
//
|
||||||
|
// The entries live in a ListModel synced in place. Handing the Instantiator
|
||||||
|
// a fresh array instead rebuilt every panel on every plugin change, four
|
||||||
|
// times over during startup: each rebuild started a new asynchronous load of
|
||||||
|
// every keepLoaded panel while the last was in flight, so two OSDs could run
|
||||||
|
// at once, each registering its IPC handler. A full plugin reload clears
|
||||||
|
// the model in unloadPanels, so panels still rebuild from fresh code then.
|
||||||
|
ListModel { id: panelEntryModel }
|
||||||
|
|
||||||
|
function syncPanelEntries() {
|
||||||
|
var wanted = ({})
|
||||||
|
var order = []
|
||||||
|
var entries = computePanelEntries()
|
||||||
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
var entry = entries[i]
|
||||||
|
entry.sourceUrl = String(shell.pluginRegistry.entryPointUrl(entry.manifest, entry.kind) || "")
|
||||||
|
wanted[entry.id] = entry
|
||||||
|
order.push(entry.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep an entry whose plugin still loads the same way; drop the rest.
|
||||||
|
for (var j = panelEntryModel.count - 1; j >= 0; j--) {
|
||||||
|
var row = panelEntryModel.get(j)
|
||||||
|
var next = wanted[row.pluginId]
|
||||||
|
if (next && next.kind === row.entryKind && next.keepLoaded === row.keepLoaded && next.sourceUrl === row.sourceUrl) {
|
||||||
|
delete wanted[row.pluginId]
|
||||||
|
} else {
|
||||||
|
panelEntryModel.remove(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var k = 0; k < order.length; k++) {
|
||||||
|
var added = wanted[order[k]]
|
||||||
|
if (!added) continue
|
||||||
|
panelEntryModel.append({ pluginId: added.id, entryKind: added.kind, keepLoaded: added.keepLoaded, sourceUrl: added.sourceUrl })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function computePanelEntries() {
|
function computePanelEntries() {
|
||||||
var out = []
|
var out = []
|
||||||
@@ -1307,21 +1343,21 @@ ShellRoot {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: shell.pluginRegistry
|
target: shell.pluginRegistry
|
||||||
function onPluginsChanged() { if (!shell.pluginReloading) shell.panelEntries = shell.computePanelEntries() }
|
function onPluginsChanged() { if (!shell.pluginReloading) shell.syncPanelEntries() }
|
||||||
}
|
}
|
||||||
|
|
||||||
Instantiator {
|
Instantiator {
|
||||||
model: shell.panelEntries
|
model: panelEntryModel
|
||||||
active: true
|
active: true
|
||||||
|
|
||||||
delegate: QtObject {
|
delegate: QtObject {
|
||||||
id: panelEntry
|
id: panelEntry
|
||||||
required property var modelData
|
required property string pluginId
|
||||||
readonly property string pluginId: modelData.id
|
required property string entryKind
|
||||||
readonly property var manifest: modelData.manifest
|
required property bool keepLoaded
|
||||||
readonly property string entryKind: modelData.kind
|
required property string sourceUrl
|
||||||
readonly property bool keepLoaded: modelData.keepLoaded === true
|
// The manifest can be replaced on a rescan without the entry changing.
|
||||||
readonly property string sourceUrl: shell.pluginRegistry.entryPointUrl(manifest, entryKind)
|
readonly property var manifest: shell.pluginRegistry.installedPlugins[pluginId]
|
||||||
|
|
||||||
property Loader panelLoader: Loader {
|
property Loader panelLoader: Loader {
|
||||||
source: panelEntry.sourceUrl
|
source: panelEntry.sourceUrl
|
||||||
@@ -1477,7 +1513,7 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
shell.pluginReloading = false
|
shell.pluginReloading = false
|
||||||
shell._syncServices()
|
shell._syncServices()
|
||||||
shell.panelEntries = shell.computePanelEntries()
|
shell.syncPanelEntries()
|
||||||
shell.syncPluginWidgets()
|
shell.syncPluginWidgets()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
|
||||||
|
run_node_test <<'JS'
|
||||||
|
const fs = require('fs')
|
||||||
|
const shellQml = fs.readFileSync(path.join(root, 'shell/shell.qml'), 'utf8')
|
||||||
|
|
||||||
|
// Handing the Instantiator a fresh array rebuilt every panel on each plugin
|
||||||
|
// change, several times during startup, so a keepLoaded panel such as the OSD
|
||||||
|
// could load twice at once and register its IPC handler twice.
|
||||||
|
assert(
|
||||||
|
/Instantiator \{\s*model: panelEntryModel/.test(shellQml) && !shellQml.includes('panelEntries'),
|
||||||
|
'panel loaders come from a model synced in place, not a reassigned array'
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
/if \(next && next\.kind === row\.entryKind && next\.keepLoaded === row\.keepLoaded && next\.sourceUrl === row\.sourceUrl\) \{\s*delete wanted\[row\.pluginId\]/.test(shellQml),
|
||||||
|
'a panel whose plugin still loads the same way keeps its loader'
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
/function unloadPanels\(\) \{[\s\S]*?panelEntryModel\.clear\(\)/.test(shellQml),
|
||||||
|
'a full plugin reload still rebuilds every panel from fresh code'
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
(shellQml.match(/shell\.syncPanelEntries\(\)/g) || []).length === 2,
|
||||||
|
'plugin changes and finished scans sync the panel entries'
|
||||||
|
)
|
||||||
|
JS
|
||||||
Reference in new issue
Block a user