Skip to content

Commit 1e1ee57

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 6c914c9 commit 1e1ee57

File tree

7 files changed

+517
-98
lines changed

7 files changed

+517
-98
lines changed

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ set(hotspot_SRCS
2626

2727
parsers/perf/perfparser.cpp
2828
perfrecord.cpp
29+
perfrecordssh.cpp
2930

3031
mainwindow.cpp
3132
flamegraph.cpp

src/perfrecord.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,11 @@ QString PerfRecord::sudoUtil()
369369
return {};
370370
}
371371

372+
bool PerfRecord::canElevatePrivileges()
373+
{
374+
return sudoUtil().isEmpty() && !KF5Auth_FOUND;
375+
}
376+
372377
QString PerfRecord::currentUsername()
373378
{
374379
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: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
/*
2+
perfrecordssh.cpp
3+
4+
This file is part of Hotspot, the Qt GUI for performance analysis.
5+
6+
Copyright (C) 2021 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected]
7+
Author: Lieven Hey <[email protected]>
8+
9+
Licensees holding valid commercial KDAB Hotspot licenses may use this file in
10+
accordance with Hotspot Commercial License Agreement provided with the Software.
11+
12+
Contact [email protected] if any conditions of this licensing are not clear to you.
13+
14+
This program is free software; you can redistribute it and/or modify
15+
it under the terms of the GNU General Public License as published by
16+
the Free Software Foundation, either version 2 of the License, or
17+
(at your option) any later version.
18+
19+
This program is distributed in the hope that it will be useful,
20+
but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
GNU General Public License for more details.
23+
24+
You should have received a copy of the GNU General Public License
25+
along with this program. If not, see <http://www.gnu.org/licenses/>.
26+
*/
27+
28+
#include "perfrecordssh.h"
29+
30+
#include <QDir>
31+
#include <QFile>
32+
#include <QFileInfo>
33+
#include <QProcess>
34+
#include <QStandardPaths>
35+
36+
#include <csignal>
37+
38+
#include "hotspot-config.h"
39+
40+
QString sshOutput(const QString& hostname, const QStringList& command)
41+
{
42+
QProcess ssh;
43+
ssh.setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));
44+
const auto arguments = QStringList({hostname}) + command;
45+
ssh.setArguments(arguments);
46+
ssh.start();
47+
ssh.waitForFinished();
48+
return QString::fromUtf8(ssh.readAll());
49+
}
50+
51+
int sshExitCode(const QString& hostname, const QStringList& command)
52+
{
53+
QProcess ssh;
54+
ssh.setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));
55+
const auto arguments = QStringList({hostname}) + command;
56+
ssh.setArguments(arguments);
57+
ssh.start();
58+
ssh.waitForFinished();
59+
return ssh.exitCode();
60+
}
61+
62+
PerfRecordSSH::PerfRecordSSH(QObject* parent)
63+
: PerfRecord(parent)
64+
{
65+
m_hostname = QStringLiteral("user@localhost");
66+
}
67+
68+
PerfRecordSSH::~PerfRecordSSH() = default;
69+
70+
void PerfRecordSSH::record(const QStringList& perfOptions, const QString& outputPath, bool /*elevatePrivileges*/,
71+
const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory)
72+
{
73+
int exitCode = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-e"), exePath});
74+
if (exitCode) {
75+
emit recordingFailed(tr("File '%1' does not exist.").arg(exePath));
76+
}
77+
78+
exitCode = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-f"), exePath});
79+
if (exitCode) {
80+
emit recordingFailed(tr("'%1' is not a file.").arg(exePath));
81+
}
82+
83+
exitCode = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-x"), exePath});
84+
if (exitCode) {
85+
emit recordingFailed(tr("File '%1' is not executable.").arg(exePath));
86+
}
87+
88+
QStringList recordOptions = {exePath};
89+
recordOptions += exeOptions;
90+
91+
startRecording(perfOptions, outputPath, recordOptions, workingDirectory);
92+
}
93+
94+
void PerfRecordSSH::record(const QStringList& perfOptions, const QString& outputPath, bool /*elevatePrivileges*/,
95+
const QStringList& pids)
96+
{
97+
if (pids.empty()) {
98+
emit recordingFailed(tr("Process does not exist."));
99+
return;
100+
}
101+
102+
QStringList options = perfOptions;
103+
options += {QStringLiteral("--pid"), pids.join(QLatin1Char(','))};
104+
startRecording(options, outputPath, {}, {});
105+
}
106+
107+
void PerfRecordSSH::recordSystem(const QStringList& perfOptions, const QString& outputPath)
108+
{
109+
auto options = perfOptions;
110+
options.append(QStringLiteral("--all-cpus"));
111+
startRecording(options, outputPath, {}, {});
112+
}
113+
114+
void PerfRecordSSH::stopRecording()
115+
{
116+
if (m_recordProcess) {
117+
m_userTerminated = true;
118+
m_outputFile->close();
119+
m_recordProcess->terminate();
120+
m_recordProcess->waitForFinished();
121+
m_recordProcess = nullptr;
122+
}
123+
}
124+
125+
void PerfRecordSSH::sendInput(const QByteArray& input)
126+
{
127+
if (m_recordProcess)
128+
m_recordProcess->write(input);
129+
}
130+
131+
QString PerfRecordSSH::currentUsername()
132+
{
133+
if (m_hostname.isEmpty())
134+
return {};
135+
return sshOutput(m_hostname, {QLatin1String("echo"), QLatin1String("$USERNAME")}).simplified();
136+
}
137+
138+
bool PerfRecordSSH::canTrace(const QString& path)
139+
{
140+
if (m_hostname.isEmpty())
141+
return false;
142+
143+
// exit code == 0 -> true
144+
bool isDir = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-d"), path}) == 0;
145+
bool isReadable = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-r"), path}) == 0;
146+
147+
if (!isDir || !isReadable) {
148+
return false;
149+
}
150+
151+
QString paranoid =
152+
sshOutput(m_hostname, {QLatin1String("cat"), QLatin1String("/proc/sys/kernel/perf_event_paranoid")});
153+
return paranoid.trimmed() == QLatin1String("-1");
154+
}
155+
156+
bool PerfRecordSSH::canProfileOffCpu()
157+
{
158+
if (m_hostname.isEmpty())
159+
return false;
160+
return canTrace(QStringLiteral("events/sched/sched_switch"));
161+
}
162+
163+
static QString perfRecordHelp(const QString& hostname)
164+
{
165+
static const QString recordHelp = [hostname]() {
166+
static QString help =
167+
sshOutput(hostname, {QLatin1String("perf"), QLatin1String("record"), QLatin1String("--help")});
168+
if (help.isEmpty()) {
169+
// no man page installed, assume the best
170+
help = QLatin1String("--sample-cpu --switch-events");
171+
}
172+
return help;
173+
}();
174+
return recordHelp;
175+
}
176+
177+
static QString perfBuildOptions(const QString& hostname)
178+
{
179+
static const QString buildOptionsHelper = [hostname]() {
180+
static QString buildOptions =
181+
sshOutput(hostname, {QLatin1String("perf"), QLatin1String("version"), QLatin1String("--build-options")});
182+
return buildOptions;
183+
}();
184+
return buildOptionsHelper;
185+
}
186+
187+
bool PerfRecordSSH::canSampleCpu()
188+
{
189+
if (m_hostname.isEmpty())
190+
return false;
191+
return perfRecordHelp(m_hostname).contains(QLatin1String("--sample-cpu"));
192+
}
193+
194+
bool PerfRecordSSH::canSwitchEvents()
195+
{
196+
if (m_hostname.isEmpty())
197+
return false;
198+
return perfRecordHelp(m_hostname).contains(QLatin1String("--switch-events"));
199+
}
200+
201+
bool PerfRecordSSH::canUseAio()
202+
{
203+
// somehow this doesn't work with ssh
204+
return false;
205+
}
206+
207+
bool PerfRecordSSH::canCompress()
208+
{
209+
if (m_hostname.isEmpty())
210+
return false;
211+
return Zstd_FOUND && perfBuildOptions(m_hostname).contains(QLatin1String("zstd: [ on ]"));
212+
}
213+
214+
bool PerfRecordSSH::isPerfInstalled()
215+
{
216+
if (m_hostname.isEmpty())
217+
return false;
218+
return sshExitCode(m_hostname, {QLatin1String("perf")}) != 127;
219+
}
220+
221+
void PerfRecordSSH::startRecording(const QStringList& perfOptions, const QString& outputPath,
222+
const QStringList& recordOptions, const QString& workingDirectory)
223+
{
224+
if (m_recordProcess) {
225+
stopRecording();
226+
}
227+
228+
QFileInfo outputFileInfo(outputPath);
229+
QString folderPath = outputFileInfo.dir().path();
230+
QFileInfo folderInfo(folderPath);
231+
if (!folderInfo.exists()) {
232+
emit recordingFailed(tr("Folder '%1' does not exist.").arg(folderPath));
233+
return;
234+
}
235+
if (!folderInfo.isDir()) {
236+
emit recordingFailed(tr("'%1' is not a folder.").arg(folderPath));
237+
return;
238+
}
239+
if (!folderInfo.isWritable()) {
240+
emit recordingFailed(tr("Folder '%1' is not writable.").arg(folderPath));
241+
return;
242+
}
243+
244+
QStringList perfCommand = {QStringLiteral("record"), QStringLiteral("-o"), QStringLiteral("-")};
245+
perfCommand += perfOptions;
246+
perfCommand += recordOptions;
247+
248+
m_outputFile = new QFile(outputPath);
249+
m_outputFile->open(QIODevice::WriteOnly);
250+
251+
m_recordProcess = new QProcess(this);
252+
m_recordProcess->setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));
253+
m_recordProcess->setArguments({m_hostname, QLatin1String("perf ") + perfCommand.join(QLatin1Char(' '))});
254+
m_recordProcess->start();
255+
m_recordProcess->waitForStarted();
256+
257+
qDebug() << m_recordProcess->arguments().join(QLatin1Char(' '));
258+
259+
emit recordingStarted(QLatin1String("perf"), perfCommand);
260+
261+
connect(m_recordProcess, &QProcess::readyReadStandardOutput, this,
262+
[this] { m_outputFile->write(m_recordProcess->readAllStandardOutput()); });
263+
264+
connect(m_recordProcess, &QProcess::readyReadStandardError, this,
265+
[this] { emit recordingOutput(QString::fromUtf8(m_recordProcess->readAllStandardError())); });
266+
267+
connect(m_recordProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
268+
[this](int exitCode, QProcess::ExitStatus exitStatus) {
269+
Q_UNUSED(exitStatus)
270+
271+
m_outputFile->close();
272+
273+
if ((exitCode == EXIT_SUCCESS || (exitCode == SIGTERM && m_userTerminated) || m_outputFile->size() > 0)
274+
&& m_outputFile->exists()) {
275+
if (exitCode != EXIT_SUCCESS && !m_userTerminated) {
276+
emit debuggeeCrashed();
277+
}
278+
emit recordingFinished(m_outputFile->fileName());
279+
} else {
280+
emit recordingFailed(tr("Failed to record perf data, error code %1.").arg(exitCode));
281+
}
282+
m_userTerminated = false;
283+
284+
emit recordingFinished(m_outputFile->fileName());
285+
});
286+
}

0 commit comments

Comments
 (0)