From 490a115f706bb5f74e69ff8d7244afa43b61e8dd Mon Sep 17 00:00:00 2001 From: wangjinrun Date: Mon, 26 Oct 2026 15:08:03 +0800 Subject: [PATCH 1/2] feat: add startup performance benchmark infrastructure - Add StartupTimingRecorder singleton and StartupTimingScope RAII helper - Instrument plugin discovery, so load, dconfig creation, translation, applet load/create-root/init phases with timing scopes - Replace anonymous Benchmark class in appletloader with StartupTimingScope, preserving the original qCWarning timeout warning behavior - Add total_startup scope and JSON dump in main.cpp - Zero overhead when DDE_SHELL_BENCHMARK env var is not set - JSON report includes timestamp, version, pid, and samples array - Output path configurable via DDE_SHELL_BENCHMARK_OUTPUT env var --- frame/CMakeLists.txt | 2 + frame/pluginloader.cpp | 3 + frame/startuptimingrecorder.cpp | 111 ++++++++++++++++++++++++++++++++ frame/startuptimingrecorder.h | 64 ++++++++++++++++++ shell/appletconfigmanager.cpp | 3 + shell/appletloader.cpp | 32 ++------- shell/main.cpp | 12 +++- 7 files changed, 199 insertions(+), 28 deletions(-) create mode 100644 frame/startuptimingrecorder.cpp create mode 100644 frame/startuptimingrecorder.h diff --git a/frame/CMakeLists.txt b/frame/CMakeLists.txt index b54909545..b3e4d8a28 100644 --- a/frame/CMakeLists.txt +++ b/frame/CMakeLists.txt @@ -7,6 +7,7 @@ set(PUBLIC_HEADERS pluginmetadata.h pluginloader.h pluginfactory.h + startuptimingrecorder.h applet.h appletdata.h containment.h @@ -55,6 +56,7 @@ add_library(dde-shell-frame SHARED pluginmetadata.cpp pluginloader.cpp pluginfactory.cpp + startuptimingrecorder.cpp applet.cpp containment.cpp panel.cpp diff --git a/frame/pluginloader.cpp b/frame/pluginloader.cpp index 01ab0bd31..cb1f89d95 100644 --- a/frame/pluginloader.cpp +++ b/frame/pluginloader.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "pluginloader.h" +#include "startuptimingrecorder.h" #include "applet.h" #include "containment.h" @@ -63,6 +64,7 @@ class DPluginLoaderPrivate : public DObjectPrivate void initPlugins() { + StartupTimingScope timingScope(QStringLiteral("plugin_discovery")); for (const auto &item : m_pluginDirs) { const QDirIterator::IteratorFlags flags = QDirIterator::Subdirectories | QDirIterator::FollowSymlinks; const QStringList nameFilters = {MetaDataFileName}; @@ -151,6 +153,7 @@ class DPluginLoaderPrivate : public DObjectPrivate if (!existPlugin(data)) return nullptr; + StartupTimingScope timingScope(QStringLiteral("so_load"), data.pluginId()); DAppletFactory *factory = nullptr; const QString fileName = data.pluginId(); QPluginLoader loader(fileName); diff --git a/frame/startuptimingrecorder.cpp b/frame/startuptimingrecorder.cpp new file mode 100644 index 000000000..60ca7edc9 --- /dev/null +++ b/frame/startuptimingrecorder.cpp @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "startuptimingrecorder.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +DS_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(dsBenchmarkLog, "org.deepin.dde.shell.benchmark") + +Q_APPLICATION_STATIC(StartupTimingRecorder, g_instance) + +StartupTimingRecorder::StartupTimingRecorder() + : QObject() +{ +} + +StartupTimingRecorder::~StartupTimingRecorder() = default; + +StartupTimingRecorder *StartupTimingRecorder::instance() +{ + return g_instance; +} + +void StartupTimingRecorder::record(const QString &phase, qint64 elapsedMs, + const QString &pluginId, const QVariantMap &metadata) +{ + QMutexLocker locker(&m_mutex); + m_samples.append({phase, pluginId, elapsedMs, metadata}); +} + +QJsonArray StartupTimingRecorder::toJson() const +{ + QMutexLocker locker(&m_mutex); + QJsonArray array; + for (const auto &sample : m_samples) { + QJsonObject obj; + obj["phase"] = sample.phase; + if (!sample.pluginId.isEmpty()) + obj["plugin_id"] = sample.pluginId; + obj["elapsed_ms"] = sample.elapsedMs; + if (!sample.metadata.isEmpty()) + obj["metadata"] = QJsonObject::fromVariantMap(sample.metadata); + array.append(obj); + } + return array; +} + +bool StartupTimingRecorder::isEnabled() const +{ + return qgetenv("DDE_SHELL_BENCHMARK") == "1"; +} + +void StartupTimingRecorder::dumpToFile(const QString &path) const +{ + QJsonObject root; + root["timestamp"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + root["version"] = "1.0"; + root["pid"] = QCoreApplication::applicationPid(); + root["samples"] = toJson(); + + QFile file(path); + if (file.open(QIODevice::WriteOnly)) { + file.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + file.close(); + } else { + qCWarning(dsBenchmarkLog) << "Cannot open benchmark output:" << path; + } +} + +StartupTimingScope::StartupTimingScope(const QString &phase, const QString &pluginId, + const QVariantMap &metadata, int timeoutMs) + : m_phase(phase) + , m_pluginId(pluginId) + , m_metadata(metadata) + , m_timeoutMs(timeoutMs) +{ + if (StartupTimingRecorder::instance()->isEnabled()) { + m_enabled = true; + m_timer.start(); + } +} + +StartupTimingScope::~StartupTimingScope() +{ + if (!m_enabled) + return; + + const qint64 elapsed = m_timer.elapsed(); + StartupTimingRecorder::instance()->record(m_phase, elapsed, m_pluginId, m_metadata); + + if (m_timeoutMs > 0 && elapsed >= m_timeoutMs) { + if (m_pluginId.isEmpty()) + qCWarning(dsBenchmarkLog) << m_phase << ": elapsed time [" << elapsed << "ms ]."; + else + qCWarning(dsBenchmarkLog) << m_phase << ":" << m_pluginId << ": elapsed time [" << elapsed << "ms ]."; + } +} + +DS_END_NAMESPACE diff --git a/frame/startuptimingrecorder.h b/frame/startuptimingrecorder.h new file mode 100644 index 000000000..c1e894e6a --- /dev/null +++ b/frame/startuptimingrecorder.h @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "dsglobal.h" + +#include +#include +#include +#include +#include +#include +#include + +DS_BEGIN_NAMESPACE + +struct TimingSample +{ + QString phase; + QString pluginId; + qint64 elapsedMs = 0; + QVariantMap metadata; +}; + +class DS_SHARE StartupTimingRecorder : public QObject +{ + Q_OBJECT +public: + StartupTimingRecorder(); + ~StartupTimingRecorder() override; + static StartupTimingRecorder *instance(); + + void record(const QString &phase, qint64 elapsedMs, + const QString &pluginId = {}, const QVariantMap &metadata = {}); + QJsonArray toJson() const; + bool isEnabled() const; + void dumpToFile(const QString &path) const; + +private: + Q_DISABLE_COPY(StartupTimingRecorder) + + QVector m_samples; + mutable QMutex m_mutex; +}; + +class StartupTimingScope +{ +public: + explicit StartupTimingScope(const QString &phase, const QString &pluginId = {}, + const QVariantMap &metadata = {}, int timeoutMs = 0); + ~StartupTimingScope(); + +private: + QString m_phase; + QString m_pluginId; + QVariantMap m_metadata; + int m_timeoutMs = 0; + bool m_enabled = false; + QElapsedTimer m_timer; +}; + +DS_END_NAMESPACE diff --git a/shell/appletconfigmanager.cpp b/shell/appletconfigmanager.cpp index 548d46c62..0112f27bd 100644 --- a/shell/appletconfigmanager.cpp +++ b/shell/appletconfigmanager.cpp @@ -5,6 +5,7 @@ #include "appletconfigmanager.h" #include "pluginloader.h" +#include "startuptimingrecorder.h" #include #include @@ -50,6 +51,7 @@ void AppletConfigManager::setAppletEnabled(const QString &pluginId, bool enabled void AppletConfigManager::ensureAppletConfigs() { + StartupTimingScope totalScope(QStringLiteral("dconfig_create")); auto loader = DPluginLoader::instance(); const auto plugins = loader->plugins(); QSet pluginIds; @@ -61,6 +63,7 @@ void AppletConfigManager::ensureAppletConfigs() if (m_appletConfigs.contains(pluginId)) continue; + StartupTimingScope pluginScope(QStringLiteral("dconfig_create"), pluginId); auto config = DConfig::create(QLatin1String(AppId), QLatin1String(ConfigName), QLatin1Char('/') + pluginId, diff --git a/shell/appletloader.cpp b/shell/appletloader.cpp index f9198b096..f1ca067c7 100644 --- a/shell/appletloader.cpp +++ b/shell/appletloader.cpp @@ -4,6 +4,7 @@ #include "appletloader.h" #include "appletconfigmanager.h" +#include "startuptimingrecorder.h" #include "pluginloader.h" #include "applet.h" #include "containment.h" @@ -27,29 +28,6 @@ DGUI_USE_NAMESPACE Q_LOGGING_CATEGORY(dsLoaderLog, "org.deepin.dde.shell.loader") -namespace { - class Benchmark - { - public: - explicit Benchmark(const QString &text) - : m_text(text) - { - m_timer.start(); - } - ~Benchmark() - { - const auto elasped = m_timer.elapsed(); - if (elasped >= m_timeout) { - qCWarning(dsLoaderLog) << qPrintable(m_text) << ": elasped time [" << elasped << "]."; - } - } - private: - QString m_text; - int m_timeout = 100; - QElapsedTimer m_timer; - }; -} - class DAppletLoaderPrivate : public DObjectPrivate { public: @@ -178,6 +156,7 @@ void DAppletLoaderPrivate::doCreateRootObject(DApplet *applet) if (applet->pluginMetaData().url().isEmpty()) return; + StartupTimingScope timingScope(QStringLiteral("applet_create_root"), applet->pluginId()); DQmlEngine *engine = new DQmlEngine(applet, applet); QObject::connect(engine, &DQmlEngine::createFinished, applet, [this, applet, engine]() { auto rootObject = engine->rootObject(); @@ -202,8 +181,7 @@ void DAppletLoaderPrivate::doCreateRootObject(DApplet *applet) bool DAppletLoaderPrivate::doLoad(DApplet *applet) { D_Q(DAppletLoader); - Benchmark benchmark(QString("Load applet %1").arg(applet->pluginId())); - Q_UNUSED(benchmark); + StartupTimingScope timingScope(QStringLiteral("applet_load"), applet->pluginId(), {}, 100); if (!applet->load()) { qCWarning(dsLoaderLog) << "Plugin load failed:" << applet->pluginId(); if (auto containment = qobject_cast(applet->parentApplet())) { @@ -218,8 +196,7 @@ bool DAppletLoaderPrivate::doLoad(DApplet *applet) bool DAppletLoaderPrivate::doInit(DApplet *applet) { D_Q(DAppletLoader); - Benchmark benchmark(QString("Init applet %1").arg(applet->pluginId())); - Q_UNUSED(benchmark); + StartupTimingScope timingScope(QStringLiteral("applet_init"), applet->pluginId(), {}, 100); if (!applet->init()) { qCWarning(dsLoaderLog) << "Plugin init failed:" << applet->pluginId(); if (auto containment = qobject_cast(applet->parentApplet())) { @@ -333,6 +310,7 @@ void DAppletLoaderPrivate::loadTranslation(const DPluginMetaData &pluginData) { const QString baseDir = pluginData.pluginDir(); const QString pluginId = pluginData.pluginId(); + StartupTimingScope timingScope(QStringLiteral("translation_load"), pluginId); if (!m_configManager->isAppletEnabled(pluginId)) return; diff --git a/shell/main.cpp b/shell/main.cpp index aa7335652..d6bc7477c 100644 --- a/shell/main.cpp +++ b/shell/main.cpp @@ -22,6 +22,7 @@ #include "pluginloader.h" #include "appletconfigmanager.h" #include "appletloader.h" +#include "startuptimingrecorder.h" #include "qmlengine.h" #include "shell.h" @@ -139,6 +140,7 @@ int main(int argc, char *argv[]) setenv("DSG_APP_ID", "org.deepin.dde.shell", 0); DGuiApplicationHelper::setAttribute(DGuiApplicationHelper::UseInactiveColorGroup, false); DApplication a(argc, argv); + StartupTimingScope *totalStartupScope = new StartupTimingScope(QStringLiteral("total_startup")); // Don't apply to plugins qunsetenv("QT_SCALE_FACTOR"); // dde-shell contains UI controls based on QML and Widget technologies. @@ -251,8 +253,16 @@ int main(int argc, char *argv[]) if (parser.isSet(sceneviewOption)) manager.enableSceneview(); - QMetaObject::invokeMethod(&a, [&manager](){ + QMetaObject::invokeMethod(&a, [&manager, totalStartupScope](){ manager.exec(); + delete totalStartupScope; + if (StartupTimingRecorder::instance()->isEnabled()) { + const QString output = qgetenv("DDE_SHELL_BENCHMARK_OUTPUT"); + const QString path = output.isEmpty() + ? QString("/tmp/dde-shell-benchmark-%1.json").arg(QCoreApplication::applicationPid()) + : output; + StartupTimingRecorder::instance()->dumpToFile(path); + } }, Qt::QueuedConnection); QObject::connect(qApp, &QCoreApplication::aboutToQuit, qApp, [&manager]() { From 7f4e22eee3c7ef3c88f1f19b6c7be312fab92eb2 Mon Sep 17 00:00:00 2001 From: wangjinrun Date: Mon, 26 Oct 2026 15:08:35 +0800 Subject: [PATCH 2/2] feat: optimize applet loading to reduce main-thread blocking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 low-risk optimizations for async plugin loading: 1. Translation files lazy-loaded per-applet in load() instead of global recursive load in exec() 2. DConfig instances created lazily via ensureAppletConfig() on first isAppletEnabled() call, instead of bulk creation in ensureAppletConfigs() 3. .so libraries preloaded via posix_fadvise in background thread after metadata scan to warm OS page cache PMS: DDE-482 Log: 优化dde-shell插件加载性能,减少主线程阻塞 Bug: https://agentapi-dev.uniontech.com/multica/issue/DDE-482 fix: address code review issues for async-load phase 1 fix: 修复异步加载阶段一代码审核问题 1. Remove const from ensureAppletConfig() and isAppletEnabled(), eliminate all const_cast 2. Remove unnecessary mutable from m_appletConfigs and m_disabledPlugins 3. Remove unused #include 4. Replace dlopen/dlclose with posix_fadvise to avoid ELF parsing and .init/.fini side effects Log: 修复代码审核中指出的const-correctness违规、不必要的mutable、死代码及dlopen副作用问题 Influence: shell/appletconfigmanager.h shell/appletconfigmanager.cpp frame/pluginloader.cpp cleanup: remove dead code and add preload failure log cleanup: 移除死代码并补充预加载失败日志 1. Remove no-op ensureAppletConfigs() call, declaration, and definition 2. Add qCDebug log for open() failure in preloadPluginLibraries() Log: 移除空的 ensureAppletConfigs() 死代码,补充 preload open 失败日志 Influence: 无功能影响,仅代码清理 --- frame/pluginloader.cpp | 25 +++++++++++++++++ shell/appletconfigmanager.cpp | 52 ++++++++++++++--------------------- shell/appletconfigmanager.h | 4 +-- shell/appletloader.cpp | 9 ++---- shell/main.cpp | 1 - 5 files changed, 50 insertions(+), 41 deletions(-) diff --git a/frame/pluginloader.cpp b/frame/pluginloader.cpp index cb1f89d95..6827e9896 100644 --- a/frame/pluginloader.cpp +++ b/frame/pluginloader.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include DS_BEGIN_NAMESPACE; @@ -62,6 +64,28 @@ class DPluginLoaderPrivate : public DObjectPrivate m_loadMetaDatas = QtConcurrent::run(std::bind(&DPluginLoaderPrivate::initPlugins, this)); } + void preloadPluginLibraries() + { + D_Q(DPluginLoader); + for (const auto &pluginId : m_plugins.keys()) { + for (const auto &dir : q->pluginDirs()) { + const QString soPath = QDir(dir).absoluteFilePath(pluginId + PluginSuffix); + if (!QFile::exists(soPath)) + continue; + const int fd = open(soPath.toUtf8().constData(), O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + if (posix_fadvise(fd, 0, 0, POSIX_FADV_WILLNEED) != 0) { + qCDebug(dsLog) << "posix_fadvise failed for" << soPath; + } + close(fd); + } else { + qCDebug(dsLog) << "Failed to open plugin library for preloading:" << soPath; + } + break; + } + } + } + void initPlugins() { StartupTimingScope timingScope(QStringLiteral("plugin_discovery")); @@ -96,6 +120,7 @@ class DPluginLoaderPrivate : public DObjectPrivate m_plugins[info.pluginId()] = info; } } + preloadPluginLibraries(); } QStringList builtinPackagePaths() diff --git a/shell/appletconfigmanager.cpp b/shell/appletconfigmanager.cpp index 0112f27bd..42a6e274f 100644 --- a/shell/appletconfigmanager.cpp +++ b/shell/appletconfigmanager.cpp @@ -9,7 +9,6 @@ #include #include -#include DS_BEGIN_NAMESPACE @@ -31,8 +30,9 @@ QStringList AppletConfigManager::disabledPlugins() const return m_disabledPlugins; } -bool AppletConfigManager::isAppletEnabled(const QString &pluginId) const +bool AppletConfigManager::isAppletEnabled(const QString &pluginId) { + ensureAppletConfig(pluginId); return !m_disabledPlugins.contains(pluginId); } @@ -49,38 +49,28 @@ void AppletConfigManager::setAppletEnabled(const QString &pluginId, bool enabled Q_EMIT appletEnabledChanged(pluginId, enabled); } -void AppletConfigManager::ensureAppletConfigs() +void AppletConfigManager::ensureAppletConfig(const QString &pluginId) { - StartupTimingScope totalScope(QStringLiteral("dconfig_create")); - auto loader = DPluginLoader::instance(); - const auto plugins = loader->plugins(); - QSet pluginIds; - for (const auto &plugin : plugins) { - pluginIds.insert(plugin.pluginId()); - } + if (m_appletConfigs.contains(pluginId)) + return; - for (const auto &pluginId : pluginIds) { - if (m_appletConfigs.contains(pluginId)) - continue; - - StartupTimingScope pluginScope(QStringLiteral("dconfig_create"), pluginId); - auto config = DConfig::create(QLatin1String(AppId), - QLatin1String(ConfigName), - QLatin1Char('/') + pluginId, - this); - m_appletConfigs.insert(pluginId, config); - if (!config || !config->isValid()) { - qCWarning(dsLoaderLog) << "Unable to create applet DConfig; applet remains enabled:" << pluginId; - setAppletEnabled(pluginId, true); - continue; - } - - setAppletEnabled(pluginId, config->value(QLatin1String(EnableKey), true).toBool()); - QObject::connect(config, &DConfig::valueChanged, this, [this, pluginId, config](const QString &key) { - if (key == QLatin1String(EnableKey)) - setAppletEnabled(pluginId, config->value(QLatin1String(EnableKey), true).toBool()); - }); + StartupTimingScope pluginScope(QStringLiteral("dconfig_create"), pluginId); + auto config = DConfig::create(QLatin1String(AppId), + QLatin1String(ConfigName), + QLatin1Char('/') + pluginId, + this); + m_appletConfigs.insert(pluginId, config); + if (!config || !config->isValid()) { + qCWarning(dsLoaderLog) << "Unable to create applet DConfig; applet remains enabled:" << pluginId; + setAppletEnabled(pluginId, true); + return; } + + setAppletEnabled(pluginId, config->value(QLatin1String(EnableKey), true).toBool()); + QObject::connect(config, &DConfig::valueChanged, this, [this, pluginId, config](const QString &key) { + if (key == QLatin1String(EnableKey)) + setAppletEnabled(pluginId, config->value(QLatin1String(EnableKey), true).toBool()); + }); } DS_END_NAMESPACE diff --git a/shell/appletconfigmanager.h b/shell/appletconfigmanager.h index 4dbb0fd7d..68fd41c4b 100644 --- a/shell/appletconfigmanager.h +++ b/shell/appletconfigmanager.h @@ -23,8 +23,8 @@ class AppletConfigManager : public QObject explicit AppletConfigManager(QObject *parent = nullptr); QStringList disabledPlugins() const; - bool isAppletEnabled(const QString &pluginId) const; - void ensureAppletConfigs(); + bool isAppletEnabled(const QString &pluginId); + void ensureAppletConfig(const QString &pluginId); Q_SIGNALS: void appletEnabledChanged(const QString &pluginId, bool enabled); diff --git a/shell/appletloader.cpp b/shell/appletloader.cpp index f1ca067c7..f199557f0 100644 --- a/shell/appletloader.cpp +++ b/shell/appletloader.cpp @@ -124,8 +124,6 @@ void DAppletLoader::exec() } } - d->loadTranslation(applet->pluginMetaData()); - if (!d->load(applet)) return; @@ -264,6 +262,8 @@ void DAppletLoaderPrivate::fail(const QString &pluginId, const QString &reason) bool DAppletLoaderPrivate::load(DApplet *applet) { + loadTranslation(applet->pluginMetaData()); + if (!doLoad(applet)) { return false; } @@ -333,11 +333,6 @@ void DAppletLoaderPrivate::loadTranslation(const DPluginMetaData &pluginData) translator->deleteLater(); } } - - const auto children = DPluginLoader::instance()->childrenPlugin(pluginId); - for (const auto &childPluginData : children) { - loadTranslation(childPluginData); - } } void DAppletLoaderPrivate::removeTranslation(const QString &pluginId) diff --git a/shell/main.cpp b/shell/main.cpp index d6bc7477c..30d073aed 100644 --- a/shell/main.cpp +++ b/shell/main.cpp @@ -244,7 +244,6 @@ int main(int argc, char *argv[]) shell.dconfigsMigrate(); AppletConfigManager appletConfigManager; - appletConfigManager.ensureAppletConfigs(); // TODO disable qml's cache avoid to parsing error for ExecutionEngine. shell.disableQmlCache(); shell.setFlickableWheelDeceleration(6000);