From 31355d0aabe224a5b3fa133b1be3666119567573 Mon Sep 17 00:00:00 2001 From: Ivy233 Date: Mon, 1 Jun 2026 16:09:52 +0800 Subject: [PATCH] feat(dock): add app launch duration reporting for taskbar icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use AM.Identify(pidfd) for precise per-window instance mapping, read X-linglong from desktop files, and query dpkg/linglong for package version info. Report the data via DDE EventLogger (event ID 1000610003). 使用 pidfd 精准匹配窗口与 AM 实例,从桌面文件读取玲珑包名, 通过 dpkg/玲珑查询包版本信息,并通过 DDE EventLogger 上报数据。 Log: 新增任务栏图标启动时长上报 PMS: TASK-389405 --- panels/dock/taskmanager/CMakeLists.txt | 2 + .../taskmanager/launchdurationreporter.cpp | 285 ++++++++++++++++++ .../dock/taskmanager/launchdurationreporter.h | 44 +++ panels/dock/taskmanager/taskmanager.cpp | 12 +- panels/dock/taskmanager/taskmanager.h | 2 + 5 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 panels/dock/taskmanager/launchdurationreporter.cpp create mode 100644 panels/dock/taskmanager/launchdurationreporter.h diff --git a/panels/dock/taskmanager/CMakeLists.txt b/panels/dock/taskmanager/CMakeLists.txt index 3854b7e27..84dfcc76f 100644 --- a/panels/dock/taskmanager/CMakeLists.txt +++ b/panels/dock/taskmanager/CMakeLists.txt @@ -76,6 +76,8 @@ add_library(dock-taskmanager SHARED ${DBUS_INTERFACES} dockgroupmodel.h hoverpreviewproxymodel.cpp hoverpreviewproxymodel.h + launchdurationreporter.cpp + launchdurationreporter.h taskmanager.cpp taskmanager.h treelandwindow.cpp diff --git a/panels/dock/taskmanager/launchdurationreporter.cpp b/panels/dock/taskmanager/launchdurationreporter.cpp new file mode 100644 index 000000000..dc264bc1b --- /dev/null +++ b/panels/dock/taskmanager/launchdurationreporter.cpp @@ -0,0 +1,285 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "globals.h" +#include "launchdurationreporter.h" +#include "applicationmanager1interface.h" + +#ifdef HAVE_DDE_API_EVENTLOGGER +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +Q_LOGGING_CATEGORY(launchDurationReporter, "org.deepin.dde.shell.dock.launchDurationReporter") + +namespace { + +constexpr auto kAmService = "org.desktopspec.ApplicationManager1"; +constexpr auto kAmPath = "/org/desktopspec/ApplicationManager1"; +constexpr auto kInstanceIface = "org.desktopspec.ApplicationManager1.Instance"; +constexpr auto kPackageCacheTTLSeconds = 1800; + +// pidfd_open is available since Linux 5.3; glibc may not wrap it, so call the syscall directly. +int pidfd_open(pid_t pid, unsigned int flags) +{ + return static_cast(syscall(SYS_pidfd_open, pid, flags)); +} + +struct InstanceIdentity { + QString instanceId; + QString launchType; +}; + +// Map the just-appeared window (by pid) to its exact ApplicationManager instance via Identify(pidfd), +// reading that instance's LaunchType from the same reply. This is reliable per-window, unlike +// enumerating Application.Instances and guessing the latest one. +InstanceIdentity identifyInstance(pid_t pid) +{ + InstanceIdentity identity; + if (pid <= 0) { + return identity; + } + + const int pidfd = pidfd_open(pid, 0); + if (pidfd < 0) { + qCWarning(launchDurationReporter) << "[DockIconTiming] pidfd_open failed for pid:" << pid; + return identity; + } + + ApplicationManager am(QString::fromUtf8(kAmService), + QString::fromUtf8(kAmPath), + QDBusConnection::sessionBus()); + am.setTimeout(1000); + + QDBusObjectPath instancePath; + ObjectInterfaceMap instanceInfo; + const QDBusReply reply = am.Identify(QDBusUnixFileDescriptor(pidfd), instancePath, instanceInfo); + close(pidfd); + + if (!reply.isValid()) { + return identity; + } + + identity.instanceId = instancePath.path().section(QLatin1Char('/'), -1); + identity.launchType = instanceInfo.value(QString::fromUtf8(kInstanceIface)) + .value(QStringLiteral("LaunchType")).toString().trimmed(); + if (identity.launchType.isEmpty()) { + identity.launchType = QStringLiteral("unknown"); + } + + return identity; +} + +QString resolveDesktopFilePath(const QString &desktopId, const QString &desktopSourcePath) +{ + QString desktopFilePath = desktopSourcePath; + if (desktopFilePath.isEmpty() || !QFileInfo::exists(desktopFilePath)) { + const auto desktopFileName = desktopId.endsWith(QStringLiteral(".desktop")) ? desktopId : desktopId + QStringLiteral(".desktop"); + desktopFilePath = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, desktopFileName); + } + + return desktopFilePath; +} + +QString linglongIdFromDesktopFile(const QString &desktopFilePath) +{ + if (desktopFilePath.isEmpty()) { + return QString(); + } + + QSettings settings(desktopFilePath, QSettings::IniFormat); + return settings.value(QStringLiteral("Desktop Entry/X-linglong")).toString().trimmed(); +} + +QString queryLinglongVersion(const QString &linglongId) +{ + QProcess proc; + proc.start(QStringLiteral("ll-cli"), {QStringLiteral("--json"), QStringLiteral("info"), linglongId}); + if (!proc.waitForFinished(1000)) { + qCWarning(launchDurationReporter) << "[DockIconTiming] ll-cli info timeout for" << linglongId; + return QString(); + } + + if (proc.exitCode() != 0) { + return QString(); + } + + const auto document = QJsonDocument::fromJson(proc.readAllStandardOutput()); + if (!document.isObject()) { + return QString(); + } + + return document.object().value(QStringLiteral("version")).toString().trimmed(); +} + +QString queryDebVersion(const QString &desktopFilePath) +{ + if (desktopFilePath.isEmpty()) { + return QString(); + } + + // The desktopId is often a reverse-DNS id (e.g. org.deepin.dde.control-center) that is NOT the + // deb package name, so reverse-lookup the owning package from the .desktop file path. + // + // dpkg's data dir defaults to /var/lib/dpkg (overridable via DPKG_ADMINDIR); per-package file + // lists live under /info/*.list. grepping those directly is several times faster than + // `dpkg -S`, which parses its whole database. When that dir is missing (non-standard layout) we + // fall back to `dpkg -S` so correctness never depends on the directory guess. + const auto infoDir = qEnvironmentVariable("DPKG_ADMINDIR", QStringLiteral("/var/lib/dpkg")) + QStringLiteral("/info"); + + QString packageName; + if (QFileInfo::exists(infoDir)) { + QProcess search; + search.start(QStringLiteral("grep"), + {QStringLiteral("-rlFx"), QStringLiteral("--include=*.list"), desktopFilePath, infoDir}); + if (!search.waitForFinished(1000)) { + qCWarning(launchDurationReporter) << "[DockIconTiming] grep dpkg file list timeout for" << desktopFilePath; + return QString(); + } + // grep exit code: 0 = matched, 1 = no match, >1 = error; empty output means no owning package. + const auto listPath = QString::fromUtf8(search.readAllStandardOutput()).section(QLatin1Char('\n'), 0, 0).trimmed(); + if (!listPath.isEmpty()) { + // /info/[:arch].list -> + packageName = QFileInfo(listPath).completeBaseName().section(QLatin1Char(':'), 0, 0); + } + } else { + QProcess search; + search.start(QStringLiteral("dpkg"), {QStringLiteral("-S"), desktopFilePath}); + if (!search.waitForFinished(2000)) { + qCWarning(launchDurationReporter) << "[DockIconTiming] dpkg -S timeout for" << desktopFilePath; + return QString(); + } + if (search.exitCode() == 0) { + // Output format: "package[:arch][, package2 ...]: /path/to/file". + packageName = QString::fromUtf8(search.readAllStandardOutput()) + .section(QLatin1Char(':'), 0, 0).section(QLatin1Char(','), 0, 0).trimmed(); + } + } + + if (packageName.isEmpty()) { + return QString(); + } + + QProcess query; + query.start(QStringLiteral("dpkg-query"), {QStringLiteral("-W"), QStringLiteral("-f=${Version}"), packageName}); + if (!query.waitForFinished(1000)) { + qCWarning(launchDurationReporter) << "[DockIconTiming] dpkg-query timeout for" << packageName; + return QString(); + } + if (query.exitCode() != 0) { + return QString(); + } + + return QString::fromUtf8(query.readAllStandardOutput()).trimmed(); +} + +} + +namespace dock { + +LaunchDurationReporter::LaunchDurationReporter(QObject *parent) + : QObject(parent) +{ +} + +LaunchDurationReporter::~LaunchDurationReporter() +{ + m_workerPool.waitForDone(); +} + +void LaunchDurationReporter::reportWindowAppeared(const QString &desktopId, const QString &desktopSourcePath, pid_t pid) +{ + if (desktopId.isEmpty()) { + return; + } + + auto future = QtConcurrent::run(&m_workerPool, [this, desktopId, desktopSourcePath, pid]() { + const auto identity = identifyInstance(pid); + const QString uniqueId = identity.instanceId; + const QString launchType = identity.launchType; + + if (uniqueId.isEmpty()) { + return; + } + + const auto desktopFilePath = resolveDesktopFilePath(desktopId, desktopSourcePath); + const auto linglongId = linglongIdFromDesktopFile(desktopFilePath); + const auto packageName = linglongId.isEmpty() ? desktopId : linglongId; + QString version; + QString pakType; + + { + QMutexLocker locker(&m_cacheMutex); + const auto entry = m_packageCache.value(packageName); + if ((QDateTime::currentSecsSinceEpoch() - entry.timestamp) <= kPackageCacheTTLSeconds) { + version = entry.version; + pakType = entry.pakType; + } + } + + if (pakType.isEmpty()) { + if (!linglongId.isEmpty()) { + version = queryLinglongVersion(linglongId); + pakType = QStringLiteral("linglong"); + } else { + version = queryDebVersion(desktopFilePath); + pakType = version.isEmpty() ? QStringLiteral("unknown") : QStringLiteral("deb"); + } + + QMutexLocker locker(&m_cacheMutex); + m_packageCache.insert(packageName, {version, pakType, QDateTime::currentSecsSinceEpoch()}); + } + + QMetaObject::invokeMethod(this, [this, desktopId, uniqueId, launchType, version, pakType]() { + doReport(desktopId, uniqueId, launchType, version, pakType); + }, Qt::QueuedConnection); + }); + Q_UNUSED(future) +} + +void LaunchDurationReporter::doReport(const QString &desktopId, + const QString &uniqueId, + const QString &launchType, + const QString &version, + const QString &pakType) +{ +#ifdef HAVE_DDE_API_EVENTLOGGER + DDE_EventLogger::EventLogger::instance().writeEventLog({ + 1000610003, + desktopId, + QJsonObject{ + {"app_name", desktopId}, + {"launch_type", launchType}, + {"app_version", version}, + {"unique_id", uniqueId}, + {"time", QDateTime::currentMSecsSinceEpoch()}, + {"app_package_type", pakType}, + }, + }); +#else + Q_UNUSED(desktopId) + Q_UNUSED(uniqueId) + Q_UNUSED(launchType) + Q_UNUSED(version) + Q_UNUSED(pakType) +#endif +} + +} diff --git a/panels/dock/taskmanager/launchdurationreporter.h b/panels/dock/taskmanager/launchdurationreporter.h new file mode 100644 index 000000000..febeddc39 --- /dev/null +++ b/panels/dock/taskmanager/launchdurationreporter.h @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace dock { + +struct PackageCacheEntry { + QString version; + QString pakType; + qint64 timestamp; +}; + +class LaunchDurationReporter : public QObject +{ + Q_OBJECT +public: + explicit LaunchDurationReporter(QObject *parent = nullptr); + ~LaunchDurationReporter() override; + + void reportWindowAppeared(const QString &desktopId, const QString &desktopSourcePath, pid_t pid); + +private: + void doReport(const QString &desktopId, + const QString &uniqueId, + const QString &launchType, + const QString &version, + const QString &pakType); + + QHash m_packageCache; + QMutex m_cacheMutex; + QThreadPool m_workerPool; +}; + +} diff --git a/panels/dock/taskmanager/taskmanager.cpp b/panels/dock/taskmanager/taskmanager.cpp index 0c64335f5..f1aa7f66c 100644 --- a/panels/dock/taskmanager/taskmanager.cpp +++ b/panels/dock/taskmanager/taskmanager.cpp @@ -15,6 +15,7 @@ #include "globals.h" #include "hoverpreviewproxymodel.h" #include "itemmodel.h" +#include "launchdurationreporter.h" #include "pluginfactory.h" #include "taskmanager.h" #include "taskmanageradaptor.h" @@ -153,6 +154,8 @@ TaskManager::TaskManager(QObject *parent) connect(Settings, &TaskManagerSettings::allowedForceQuitChanged, this, &TaskManager::allowedForceQuitChanged); connect(Settings, &TaskManagerSettings::showAttentionAnimationChanged, this, &TaskManager::showAttentionAnimationChanged); connect(Settings, &TaskManagerSettings::windowSplitChanged, this, &TaskManager::windowSplitChanged); + + m_launchDurationReporter = new LaunchDurationReporter(this); } bool TaskManager::load() @@ -336,8 +339,11 @@ void TaskManager::handleWindowAdded(QPointer window) QSharedPointer desktopfile = nullptr; QString desktopId; + QString desktopSourcePath; if (res.size() > 0) { - desktopId = res.first().data(m_activeAppModel->roleNames().key("desktopId")).toString(); + const auto index = res.first(); + desktopId = index.data(TaskManager::DesktopIdRole).toString(); + desktopSourcePath = index.data(TaskManager::DesktopSourcePathRole).toString(); qCDebug(taskManagerLog()) << "identify by model:" << desktopId; } @@ -362,6 +368,10 @@ void TaskManager::handleWindowAdded(QPointer window) appitem->setDesktopFileParser(desktopfile); ItemModel::instance()->addItem(appitem); + + if (m_launchDurationReporter && !desktopId.isEmpty()) { + m_launchDurationReporter->reportWindowAppeared(desktopId, desktopSourcePath, window->pid()); + } } void TaskManager::dropFilesOnItem(const QString& itemId, const QStringList& urls) diff --git a/panels/dock/taskmanager/taskmanager.h b/panels/dock/taskmanager/taskmanager.h index 517bd0da3..e7b4eb7a4 100644 --- a/panels/dock/taskmanager/taskmanager.h +++ b/panels/dock/taskmanager/taskmanager.h @@ -17,6 +17,7 @@ namespace dock { class AppItem; class AbstractWindowMonitor; +class LaunchDurationReporter; class TaskManager : public DS_NAMESPACE::DContainment, public AbstractTaskManagerInterface { Q_OBJECT @@ -125,6 +126,7 @@ private Q_SLOTS: DockGlobalElementModel *m_dockGlobalElementModel = nullptr; DockItemModel *m_itemModel = nullptr; HoverPreviewProxyModel *m_hoverPreviewModel = nullptr; + LaunchDurationReporter *m_launchDurationReporter = nullptr; int queryTrashCount() const; };