Skip to content

Commit 8245222

Browse files
committed
add option to record on remote devices via ssh
most stuff is hardcoded but that will change in the next commit
1 parent ee18c2e commit 8245222

File tree

10 files changed

+518
-96
lines changed

10 files changed

+518
-96
lines changed

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ set(HOTSPOT_SRCS
3030
main.cpp
3131
parsers/perf/perfparser.cpp
3232
perfrecord.cpp
33+
perfrecordssh.cpp
3334
mainwindow.cpp
3435
flamegraph.cpp
3536
aboutdialog.cpp
@@ -52,6 +53,7 @@ set(HOTSPOT_SRCS
5253
perfoutputwidgettext.cpp
5354
perfoutputwidgetkonsole.cpp
5455
costcontextmenu.cpp
56+
ssh.cpp
5557
# ui files:
5658
mainwindow.ui
5759
aboutdialog.ui

src/perfrecord.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,11 @@ QString PerfRecord::sudoUtil()
411411
return {};
412412
}
413413

414+
bool PerfRecord::canElevatePrivileges()
415+
{
416+
return !sudoUtil().isEmpty() || KF5Auth_FOUND;
417+
}
418+
414419
QString PerfRecord::currentUsername()
415420
{
416421
return KUser().loginName();

src/perfrecord.h

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,19 @@ class PerfRecord : public QObject
2020
explicit PerfRecord(QObject* parent = nullptr);
2121
~PerfRecord();
2222

23-
void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
24-
const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory = QString());
25-
void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
26-
const QStringList& pids);
27-
void recordSystem(const QStringList& perfOptions, const QString& outputPath);
23+
virtual void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
24+
const QString& exePath, const QStringList& exeOptions,
25+
const QString& workingDirectory = QString());
26+
virtual void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
27+
const QStringList& pids);
28+
virtual void recordSystem(const QStringList& perfOptions, const QString& outputPath);
2829

2930
const QString perfCommand();
30-
void stopRecording();
31-
void sendInput(const QByteArray& input);
31+
virtual void stopRecording();
32+
virtual void sendInput(const QByteArray& input);
3233

3334
virtual QString sudoUtil();
35+
virtual bool canElevatePrivileges();
3436
virtual QString currentUsername();
3537

3638
virtual bool canTrace(const QString& path);

src/perfrecordssh.cpp

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/*
2+
SPDX-FileCopyrightText: Lieven Hey <[email protected]>
3+
SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected]
4+
5+
SPDX-License-Identifier: GPL-2.0-or-later
6+
*/
7+
8+
#include "perfrecordssh.h"
9+
10+
#include <QDir>
11+
#include <QFile>
12+
#include <QFileInfo>
13+
#include <QProcess>
14+
#include <QStandardPaths>
15+
16+
#include <csignal>
17+
18+
#include "hotspot-config.h"
19+
20+
QString sshOutput(const QString& hostname, const QStringList& command)
21+
{
22+
QProcess ssh;
23+
ssh.setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));
24+
const auto arguments = QStringList({hostname}) + command;
25+
ssh.setArguments(arguments);
26+
ssh.start();
27+
ssh.waitForFinished();
28+
return QString::fromUtf8(ssh.readAll());
29+
}
30+
31+
int sshExitCode(const QString& hostname, const QStringList& command)
32+
{
33+
QProcess ssh;
34+
ssh.setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));
35+
const auto arguments = QStringList({hostname}) + command;
36+
ssh.setArguments(arguments);
37+
ssh.start();
38+
ssh.waitForFinished();
39+
return ssh.exitCode();
40+
}
41+
42+
PerfRecordSSH::PerfRecordSSH(QObject* parent)
43+
: PerfRecord(parent)
44+
{
45+
m_hostname = QStringLiteral("user@localhost");
46+
}
47+
48+
PerfRecordSSH::~PerfRecordSSH() = default;
49+
50+
void PerfRecordSSH::record(const QStringList& perfOptions, const QString& outputPath, bool /*elevatePrivileges*/,
51+
const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory)
52+
{
53+
QStringList recordOptions = {exePath};
54+
recordOptions += exeOptions;
55+
56+
startRecording(perfOptions, outputPath, recordOptions, workingDirectory);
57+
}
58+
59+
void PerfRecordSSH::record(const QStringList& perfOptions, const QString& outputPath, bool /*elevatePrivileges*/,
60+
const QStringList& pids)
61+
{
62+
if (pids.empty()) {
63+
emit recordingFailed(tr("Process does not exist."));
64+
return;
65+
}
66+
67+
QStringList options = perfOptions;
68+
options += {QStringLiteral("--pid"), pids.join(QLatin1Char(','))};
69+
startRecording(options, outputPath, {}, {});
70+
}
71+
72+
void PerfRecordSSH::recordSystem(const QStringList& perfOptions, const QString& outputPath)
73+
{
74+
auto options = perfOptions;
75+
options.append(QStringLiteral("--all-cpus"));
76+
startRecording(options, outputPath, {}, {});
77+
}
78+
79+
void PerfRecordSSH::stopRecording()
80+
{
81+
if (m_recordProcess) {
82+
m_userTerminated = true;
83+
m_outputFile->close();
84+
m_recordProcess->terminate();
85+
m_recordProcess->waitForFinished();
86+
m_recordProcess = nullptr;
87+
}
88+
}
89+
90+
void PerfRecordSSH::sendInput(const QByteArray& input)
91+
{
92+
if (m_recordProcess)
93+
m_recordProcess->write(input);
94+
}
95+
96+
QString PerfRecordSSH::currentUsername()
97+
{
98+
// this is only used to automatically check the elevate privileges checkbox if the user is root
99+
// since we currently do not support privilege elevation over ssh returning an empty string is fine
100+
return {};
101+
}
102+
103+
bool PerfRecordSSH::canTrace(const QString& path)
104+
{
105+
if (m_hostname.isEmpty())
106+
return false;
107+
108+
// assume best case
109+
return true;
110+
}
111+
112+
bool PerfRecordSSH::canProfileOffCpu()
113+
{
114+
if (m_hostname.isEmpty())
115+
return false;
116+
return canTrace(QStringLiteral("events/sched/sched_switch"));
117+
}
118+
119+
QString perfRecordHelp(const QString& hostname)
120+
{
121+
const QString recordHelp = [hostname]() {
122+
static QString help =
123+
sshOutput(hostname, {QLatin1String("perf"), QLatin1String("record"), QLatin1String("--help")});
124+
if (help.isEmpty()) {
125+
// no man page installed, assume the best
126+
help = QLatin1String("--sample-cpu --switch-events");
127+
}
128+
return help;
129+
}();
130+
return recordHelp;
131+
}
132+
133+
QString perfBuildOptions(const QString& hostname)
134+
{
135+
const QString buildOptionsHelper = [hostname]() {
136+
static QString buildOptions =
137+
sshOutput(hostname, {QLatin1String("perf"), QLatin1String("version"), QLatin1String("--build-options")});
138+
return buildOptions;
139+
}();
140+
return buildOptionsHelper;
141+
}
142+
143+
bool PerfRecordSSH::canSampleCpu()
144+
{
145+
if (m_hostname.isEmpty())
146+
return false;
147+
return perfRecordHelp(m_hostname).contains(QLatin1String("--sample-cpu"));
148+
}
149+
150+
bool PerfRecordSSH::canSwitchEvents()
151+
{
152+
if (m_hostname.isEmpty())
153+
return false;
154+
return perfRecordHelp(m_hostname).contains(QLatin1String("--switch-events"));
155+
}
156+
157+
bool PerfRecordSSH::canUseAio()
158+
{
159+
// perf reports "error: Illegal seek" when trying to use aio and streaming data
160+
return false;
161+
}
162+
163+
bool PerfRecordSSH::canCompress()
164+
{
165+
if (m_hostname.isEmpty())
166+
return false;
167+
return Zstd_FOUND && perfBuildOptions(m_hostname).contains(QLatin1String("zstd: [ on ]"));
168+
}
169+
170+
bool PerfRecordSSH::isPerfInstalled()
171+
{
172+
if (m_hostname.isEmpty())
173+
return false;
174+
return sshExitCode(m_hostname, {QLatin1String("command"), QLatin1String("-v"), QLatin1String("perf")}) != 0;
175+
}
176+
177+
void PerfRecordSSH::startRecording(const QStringList& perfOptions, const QString& outputPath,
178+
const QStringList& recordOptions, const QString& workingDirectory)
179+
{
180+
Q_UNUSED(workingDirectory);
181+
182+
if (m_recordProcess) {
183+
stopRecording();
184+
}
185+
186+
QFileInfo outputFileInfo(outputPath);
187+
QString folderPath = outputFileInfo.dir().path();
188+
QFileInfo folderInfo(folderPath);
189+
if (!folderInfo.exists()) {
190+
emit recordingFailed(tr("Folder '%1' does not exist.").arg(folderPath));
191+
return;
192+
}
193+
if (!folderInfo.isDir()) {
194+
emit recordingFailed(tr("'%1' is not a folder.").arg(folderPath));
195+
return;
196+
}
197+
if (!folderInfo.isWritable()) {
198+
emit recordingFailed(tr("Folder '%1' is not writable.").arg(folderPath));
199+
return;
200+
}
201+
202+
QStringList perfCommand = {QStringLiteral("record"), QStringLiteral("-o"), QStringLiteral("-")};
203+
perfCommand += perfOptions;
204+
perfCommand += recordOptions;
205+
206+
m_outputFile = new QFile(outputPath);
207+
m_outputFile->open(QIODevice::WriteOnly);
208+
209+
m_recordProcess = new QProcess(this);
210+
m_recordProcess->setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));
211+
m_recordProcess->setArguments({m_hostname, QLatin1String("perf ") + perfCommand.join(QLatin1Char(' '))});
212+
m_recordProcess->start();
213+
m_recordProcess->waitForStarted();
214+
215+
emit recordingStarted(QLatin1String("perf"), perfCommand);
216+
217+
connect(m_recordProcess, &QProcess::readyReadStandardOutput, this,
218+
[this] { m_outputFile->write(m_recordProcess->readAllStandardOutput()); });
219+
220+
connect(m_recordProcess, &QProcess::readyReadStandardError, this,
221+
[this] { emit recordingOutput(QString::fromUtf8(m_recordProcess->readAllStandardError())); });
222+
223+
connect(m_recordProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
224+
[this](int exitCode, QProcess::ExitStatus exitStatus) {
225+
Q_UNUSED(exitStatus)
226+
227+
m_outputFile->close();
228+
229+
if ((exitCode == EXIT_SUCCESS || (exitCode == SIGTERM && m_userTerminated) || m_outputFile->size() > 0)
230+
&& m_outputFile->exists()) {
231+
if (exitCode != EXIT_SUCCESS && !m_userTerminated) {
232+
emit debuggeeCrashed();
233+
}
234+
emit recordingFinished(m_outputFile->fileName());
235+
} else {
236+
emit recordingFailed(tr("Failed to record perf data, error code %1.").arg(exitCode));
237+
}
238+
m_userTerminated = false;
239+
240+
emit recordingFinished(m_outputFile->fileName());
241+
});
242+
}

src/perfrecordssh.h

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
SPDX-FileCopyrightText: Lieven Hey <[email protected]>
3+
SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected]
4+
5+
SPDX-License-Identifier: GPL-2.0-or-later
6+
*/
7+
8+
#pragma once
9+
10+
#include "perfrecord.h"
11+
12+
class QFile;
13+
14+
class PerfRecordSSH : public PerfRecord
15+
{
16+
Q_OBJECT
17+
public:
18+
PerfRecordSSH(QObject* parent = nullptr);
19+
~PerfRecordSSH();
20+
21+
void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
22+
const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory) override;
23+
void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
24+
const QStringList& pids) override;
25+
void recordSystem(const QStringList& perfOptions, const QString& outputPath) override;
26+
void stopRecording() override;
27+
void sendInput(const QByteArray& input) override;
28+
29+
QString sudoUtil() override
30+
{
31+
return {};
32+
};
33+
bool canElevatePrivileges() override
34+
{
35+
return false;
36+
}
37+
QString currentUsername() override;
38+
39+
bool canTrace(const QString& path) override;
40+
bool canProfileOffCpu() override;
41+
bool canSampleCpu() override;
42+
bool canSwitchEvents() override;
43+
bool canUseAio() override;
44+
bool canCompress() override;
45+
bool isPerfInstalled() override;
46+
47+
private:
48+
void startRecording(const QStringList& perfOptions, const QString& outputPath, const QStringList& recordOptions,
49+
const QString& workingDirectory);
50+
51+
QProcess* m_recordProcess = nullptr;
52+
QFile* m_outputFile;
53+
QString m_hostname;
54+
bool m_userTerminated = false;
55+
};

0 commit comments

Comments
 (0)