Skip to content

Commit 31355d0

Browse files
committed
feat(dock): add app launch duration reporting for taskbar icons
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
1 parent cd850c0 commit 31355d0

5 files changed

Lines changed: 344 additions & 1 deletion

File tree

panels/dock/taskmanager/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ add_library(dock-taskmanager SHARED ${DBUS_INTERFACES}
7676
dockgroupmodel.h
7777
hoverpreviewproxymodel.cpp
7878
hoverpreviewproxymodel.h
79+
launchdurationreporter.cpp
80+
launchdurationreporter.h
7981
taskmanager.cpp
8082
taskmanager.h
8183
treelandwindow.cpp
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
#include "globals.h"
6+
#include "launchdurationreporter.h"
7+
#include "applicationmanager1interface.h"
8+
9+
#ifdef HAVE_DDE_API_EVENTLOGGER
10+
#include <dde-api/eventlogger.hpp>
11+
#endif
12+
13+
#include <QDBusConnection>
14+
#include <QDBusObjectPath>
15+
#include <QDBusReply>
16+
#include <QDBusUnixFileDescriptor>
17+
#include <QDateTime>
18+
#include <QFileInfo>
19+
#include <QJsonDocument>
20+
#include <QJsonObject>
21+
#include <QLoggingCategory>
22+
#include <QProcess>
23+
#include <QSettings>
24+
#include <QStandardPaths>
25+
#include <QtConcurrent>
26+
27+
#include <sys/syscall.h>
28+
#include <unistd.h>
29+
30+
Q_LOGGING_CATEGORY(launchDurationReporter, "org.deepin.dde.shell.dock.launchDurationReporter")
31+
32+
namespace {
33+
34+
constexpr auto kAmService = "org.desktopspec.ApplicationManager1";
35+
constexpr auto kAmPath = "/org/desktopspec/ApplicationManager1";
36+
constexpr auto kInstanceIface = "org.desktopspec.ApplicationManager1.Instance";
37+
constexpr auto kPackageCacheTTLSeconds = 1800;
38+
39+
// pidfd_open is available since Linux 5.3; glibc may not wrap it, so call the syscall directly.
40+
int pidfd_open(pid_t pid, unsigned int flags)
41+
{
42+
return static_cast<int>(syscall(SYS_pidfd_open, pid, flags));
43+
}
44+
45+
struct InstanceIdentity {
46+
QString instanceId;
47+
QString launchType;
48+
};
49+
50+
// Map the just-appeared window (by pid) to its exact ApplicationManager instance via Identify(pidfd),
51+
// reading that instance's LaunchType from the same reply. This is reliable per-window, unlike
52+
// enumerating Application.Instances and guessing the latest one.
53+
InstanceIdentity identifyInstance(pid_t pid)
54+
{
55+
InstanceIdentity identity;
56+
if (pid <= 0) {
57+
return identity;
58+
}
59+
60+
const int pidfd = pidfd_open(pid, 0);
61+
if (pidfd < 0) {
62+
qCWarning(launchDurationReporter) << "[DockIconTiming] pidfd_open failed for pid:" << pid;
63+
return identity;
64+
}
65+
66+
ApplicationManager am(QString::fromUtf8(kAmService),
67+
QString::fromUtf8(kAmPath),
68+
QDBusConnection::sessionBus());
69+
am.setTimeout(1000);
70+
71+
QDBusObjectPath instancePath;
72+
ObjectInterfaceMap instanceInfo;
73+
const QDBusReply<QString> reply = am.Identify(QDBusUnixFileDescriptor(pidfd), instancePath, instanceInfo);
74+
close(pidfd);
75+
76+
if (!reply.isValid()) {
77+
return identity;
78+
}
79+
80+
identity.instanceId = instancePath.path().section(QLatin1Char('/'), -1);
81+
identity.launchType = instanceInfo.value(QString::fromUtf8(kInstanceIface))
82+
.value(QStringLiteral("LaunchType")).toString().trimmed();
83+
if (identity.launchType.isEmpty()) {
84+
identity.launchType = QStringLiteral("unknown");
85+
}
86+
87+
return identity;
88+
}
89+
90+
QString resolveDesktopFilePath(const QString &desktopId, const QString &desktopSourcePath)
91+
{
92+
QString desktopFilePath = desktopSourcePath;
93+
if (desktopFilePath.isEmpty() || !QFileInfo::exists(desktopFilePath)) {
94+
const auto desktopFileName = desktopId.endsWith(QStringLiteral(".desktop")) ? desktopId : desktopId + QStringLiteral(".desktop");
95+
desktopFilePath = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, desktopFileName);
96+
}
97+
98+
return desktopFilePath;
99+
}
100+
101+
QString linglongIdFromDesktopFile(const QString &desktopFilePath)
102+
{
103+
if (desktopFilePath.isEmpty()) {
104+
return QString();
105+
}
106+
107+
QSettings settings(desktopFilePath, QSettings::IniFormat);
108+
return settings.value(QStringLiteral("Desktop Entry/X-linglong")).toString().trimmed();
109+
}
110+
111+
QString queryLinglongVersion(const QString &linglongId)
112+
{
113+
QProcess proc;
114+
proc.start(QStringLiteral("ll-cli"), {QStringLiteral("--json"), QStringLiteral("info"), linglongId});
115+
if (!proc.waitForFinished(1000)) {
116+
qCWarning(launchDurationReporter) << "[DockIconTiming] ll-cli info timeout for" << linglongId;
117+
return QString();
118+
}
119+
120+
if (proc.exitCode() != 0) {
121+
return QString();
122+
}
123+
124+
const auto document = QJsonDocument::fromJson(proc.readAllStandardOutput());
125+
if (!document.isObject()) {
126+
return QString();
127+
}
128+
129+
return document.object().value(QStringLiteral("version")).toString().trimmed();
130+
}
131+
132+
QString queryDebVersion(const QString &desktopFilePath)
133+
{
134+
if (desktopFilePath.isEmpty()) {
135+
return QString();
136+
}
137+
138+
// The desktopId is often a reverse-DNS id (e.g. org.deepin.dde.control-center) that is NOT the
139+
// deb package name, so reverse-lookup the owning package from the .desktop file path.
140+
//
141+
// dpkg's data dir defaults to /var/lib/dpkg (overridable via DPKG_ADMINDIR); per-package file
142+
// lists live under <admindir>/info/*.list. grepping those directly is several times faster than
143+
// `dpkg -S`, which parses its whole database. When that dir is missing (non-standard layout) we
144+
// fall back to `dpkg -S` so correctness never depends on the directory guess.
145+
const auto infoDir = qEnvironmentVariable("DPKG_ADMINDIR", QStringLiteral("/var/lib/dpkg")) + QStringLiteral("/info");
146+
147+
QString packageName;
148+
if (QFileInfo::exists(infoDir)) {
149+
QProcess search;
150+
search.start(QStringLiteral("grep"),
151+
{QStringLiteral("-rlFx"), QStringLiteral("--include=*.list"), desktopFilePath, infoDir});
152+
if (!search.waitForFinished(1000)) {
153+
qCWarning(launchDurationReporter) << "[DockIconTiming] grep dpkg file list timeout for" << desktopFilePath;
154+
return QString();
155+
}
156+
// grep exit code: 0 = matched, 1 = no match, >1 = error; empty output means no owning package.
157+
const auto listPath = QString::fromUtf8(search.readAllStandardOutput()).section(QLatin1Char('\n'), 0, 0).trimmed();
158+
if (!listPath.isEmpty()) {
159+
// <admindir>/info/<package>[:arch].list -> <package>
160+
packageName = QFileInfo(listPath).completeBaseName().section(QLatin1Char(':'), 0, 0);
161+
}
162+
} else {
163+
QProcess search;
164+
search.start(QStringLiteral("dpkg"), {QStringLiteral("-S"), desktopFilePath});
165+
if (!search.waitForFinished(2000)) {
166+
qCWarning(launchDurationReporter) << "[DockIconTiming] dpkg -S timeout for" << desktopFilePath;
167+
return QString();
168+
}
169+
if (search.exitCode() == 0) {
170+
// Output format: "package[:arch][, package2 ...]: /path/to/file".
171+
packageName = QString::fromUtf8(search.readAllStandardOutput())
172+
.section(QLatin1Char(':'), 0, 0).section(QLatin1Char(','), 0, 0).trimmed();
173+
}
174+
}
175+
176+
if (packageName.isEmpty()) {
177+
return QString();
178+
}
179+
180+
QProcess query;
181+
query.start(QStringLiteral("dpkg-query"), {QStringLiteral("-W"), QStringLiteral("-f=${Version}"), packageName});
182+
if (!query.waitForFinished(1000)) {
183+
qCWarning(launchDurationReporter) << "[DockIconTiming] dpkg-query timeout for" << packageName;
184+
return QString();
185+
}
186+
if (query.exitCode() != 0) {
187+
return QString();
188+
}
189+
190+
return QString::fromUtf8(query.readAllStandardOutput()).trimmed();
191+
}
192+
193+
}
194+
195+
namespace dock {
196+
197+
LaunchDurationReporter::LaunchDurationReporter(QObject *parent)
198+
: QObject(parent)
199+
{
200+
}
201+
202+
LaunchDurationReporter::~LaunchDurationReporter()
203+
{
204+
m_workerPool.waitForDone();
205+
}
206+
207+
void LaunchDurationReporter::reportWindowAppeared(const QString &desktopId, const QString &desktopSourcePath, pid_t pid)
208+
{
209+
if (desktopId.isEmpty()) {
210+
return;
211+
}
212+
213+
auto future = QtConcurrent::run(&m_workerPool, [this, desktopId, desktopSourcePath, pid]() {
214+
const auto identity = identifyInstance(pid);
215+
const QString uniqueId = identity.instanceId;
216+
const QString launchType = identity.launchType;
217+
218+
if (uniqueId.isEmpty()) {
219+
return;
220+
}
221+
222+
const auto desktopFilePath = resolveDesktopFilePath(desktopId, desktopSourcePath);
223+
const auto linglongId = linglongIdFromDesktopFile(desktopFilePath);
224+
const auto packageName = linglongId.isEmpty() ? desktopId : linglongId;
225+
QString version;
226+
QString pakType;
227+
228+
{
229+
QMutexLocker locker(&m_cacheMutex);
230+
const auto entry = m_packageCache.value(packageName);
231+
if ((QDateTime::currentSecsSinceEpoch() - entry.timestamp) <= kPackageCacheTTLSeconds) {
232+
version = entry.version;
233+
pakType = entry.pakType;
234+
}
235+
}
236+
237+
if (pakType.isEmpty()) {
238+
if (!linglongId.isEmpty()) {
239+
version = queryLinglongVersion(linglongId);
240+
pakType = QStringLiteral("linglong");
241+
} else {
242+
version = queryDebVersion(desktopFilePath);
243+
pakType = version.isEmpty() ? QStringLiteral("unknown") : QStringLiteral("deb");
244+
}
245+
246+
QMutexLocker locker(&m_cacheMutex);
247+
m_packageCache.insert(packageName, {version, pakType, QDateTime::currentSecsSinceEpoch()});
248+
}
249+
250+
QMetaObject::invokeMethod(this, [this, desktopId, uniqueId, launchType, version, pakType]() {
251+
doReport(desktopId, uniqueId, launchType, version, pakType);
252+
}, Qt::QueuedConnection);
253+
});
254+
Q_UNUSED(future)
255+
}
256+
257+
void LaunchDurationReporter::doReport(const QString &desktopId,
258+
const QString &uniqueId,
259+
const QString &launchType,
260+
const QString &version,
261+
const QString &pakType)
262+
{
263+
#ifdef HAVE_DDE_API_EVENTLOGGER
264+
DDE_EventLogger::EventLogger::instance().writeEventLog({
265+
1000610003,
266+
desktopId,
267+
QJsonObject{
268+
{"app_name", desktopId},
269+
{"launch_type", launchType},
270+
{"app_version", version},
271+
{"unique_id", uniqueId},
272+
{"time", QDateTime::currentMSecsSinceEpoch()},
273+
{"app_package_type", pakType},
274+
},
275+
});
276+
#else
277+
Q_UNUSED(desktopId)
278+
Q_UNUSED(uniqueId)
279+
Q_UNUSED(launchType)
280+
Q_UNUSED(version)
281+
Q_UNUSED(pakType)
282+
#endif
283+
}
284+
285+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
#pragma once
6+
7+
#include <sys/types.h>
8+
9+
#include <QHash>
10+
#include <QMutex>
11+
#include <QObject>
12+
#include <QString>
13+
#include <QThreadPool>
14+
15+
namespace dock {
16+
17+
struct PackageCacheEntry {
18+
QString version;
19+
QString pakType;
20+
qint64 timestamp;
21+
};
22+
23+
class LaunchDurationReporter : public QObject
24+
{
25+
Q_OBJECT
26+
public:
27+
explicit LaunchDurationReporter(QObject *parent = nullptr);
28+
~LaunchDurationReporter() override;
29+
30+
void reportWindowAppeared(const QString &desktopId, const QString &desktopSourcePath, pid_t pid);
31+
32+
private:
33+
void doReport(const QString &desktopId,
34+
const QString &uniqueId,
35+
const QString &launchType,
36+
const QString &version,
37+
const QString &pakType);
38+
39+
QHash<QString, PackageCacheEntry> m_packageCache;
40+
QMutex m_cacheMutex;
41+
QThreadPool m_workerPool;
42+
};
43+
44+
}

panels/dock/taskmanager/taskmanager.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "globals.h"
1616
#include "hoverpreviewproxymodel.h"
1717
#include "itemmodel.h"
18+
#include "launchdurationreporter.h"
1819
#include "pluginfactory.h"
1920
#include "taskmanager.h"
2021
#include "taskmanageradaptor.h"
@@ -153,6 +154,8 @@ TaskManager::TaskManager(QObject *parent)
153154
connect(Settings, &TaskManagerSettings::allowedForceQuitChanged, this, &TaskManager::allowedForceQuitChanged);
154155
connect(Settings, &TaskManagerSettings::showAttentionAnimationChanged, this, &TaskManager::showAttentionAnimationChanged);
155156
connect(Settings, &TaskManagerSettings::windowSplitChanged, this, &TaskManager::windowSplitChanged);
157+
158+
m_launchDurationReporter = new LaunchDurationReporter(this);
156159
}
157160

158161
bool TaskManager::load()
@@ -336,8 +339,11 @@ void TaskManager::handleWindowAdded(QPointer<AbstractWindow> window)
336339

337340
QSharedPointer<DesktopfileAbstractParser> desktopfile = nullptr;
338341
QString desktopId;
342+
QString desktopSourcePath;
339343
if (res.size() > 0) {
340-
desktopId = res.first().data(m_activeAppModel->roleNames().key("desktopId")).toString();
344+
const auto index = res.first();
345+
desktopId = index.data(TaskManager::DesktopIdRole).toString();
346+
desktopSourcePath = index.data(TaskManager::DesktopSourcePathRole).toString();
341347
qCDebug(taskManagerLog()) << "identify by model:" << desktopId;
342348
}
343349

@@ -362,6 +368,10 @@ void TaskManager::handleWindowAdded(QPointer<AbstractWindow> window)
362368
appitem->setDesktopFileParser(desktopfile);
363369

364370
ItemModel::instance()->addItem(appitem);
371+
372+
if (m_launchDurationReporter && !desktopId.isEmpty()) {
373+
m_launchDurationReporter->reportWindowAppeared(desktopId, desktopSourcePath, window->pid());
374+
}
365375
}
366376

367377
void TaskManager::dropFilesOnItem(const QString& itemId, const QStringList& urls)

panels/dock/taskmanager/taskmanager.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
namespace dock {
1818
class AppItem;
1919
class AbstractWindowMonitor;
20+
class LaunchDurationReporter;
2021
class TaskManager : public DS_NAMESPACE::DContainment, public AbstractTaskManagerInterface
2122
{
2223
Q_OBJECT
@@ -125,6 +126,7 @@ private Q_SLOTS:
125126
DockGlobalElementModel *m_dockGlobalElementModel = nullptr;
126127
DockItemModel *m_itemModel = nullptr;
127128
HoverPreviewProxyModel *m_hoverPreviewModel = nullptr;
129+
LaunchDurationReporter *m_launchDurationReporter = nullptr;
128130
int queryTrashCount() const;
129131
};
130132

0 commit comments

Comments
 (0)