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..6827e9896 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" @@ -20,6 +21,8 @@ #include #include #include +#include +#include DS_BEGIN_NAMESPACE; @@ -61,8 +64,31 @@ 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")); for (const auto &item : m_pluginDirs) { const QDirIterator::IteratorFlags flags = QDirIterator::Subdirectories | QDirIterator::FollowSymlinks; const QStringList nameFilters = {MetaDataFileName}; @@ -94,6 +120,7 @@ class DPluginLoaderPrivate : public DObjectPrivate m_plugins[info.pluginId()] = info; } } + preloadPluginLibraries(); } QStringList builtinPackagePaths() @@ -151,6 +178,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..42a6e274f 100644 --- a/shell/appletconfigmanager.cpp +++ b/shell/appletconfigmanager.cpp @@ -5,10 +5,10 @@ #include "appletconfigmanager.h" #include "pluginloader.h" +#include "startuptimingrecorder.h" #include #include -#include DS_BEGIN_NAMESPACE @@ -30,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); } @@ -48,36 +49,28 @@ void AppletConfigManager::setAppletEnabled(const QString &pluginId, bool enabled Q_EMIT appletEnabledChanged(pluginId, enabled); } -void AppletConfigManager::ensureAppletConfigs() +void AppletConfigManager::ensureAppletConfig(const QString &pluginId) { - 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; - - 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 f9198b096..f199557f0 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: @@ -146,8 +124,6 @@ void DAppletLoader::exec() } } - d->loadTranslation(applet->pluginMetaData()); - if (!d->load(applet)) return; @@ -178,6 +154,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 +179,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 +194,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())) { @@ -287,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,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; @@ -355,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 aa7335652..30d073aed 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. @@ -242,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); @@ -251,8 +252,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]() {