From f4a310cf28f08a74b0d5180edfc9da49355ac251 Mon Sep 17 00:00:00 2001 From: Lieven Hey Date: Wed, 4 Jan 2023 16:58:10 +0100 Subject: [PATCH 1/7] bump required version of kf5 to 5.42.0 nowadays we build everything ourself so we don't need to keep the workaround for 5 year old patches around --- CMakeLists.txt | 2 +- src/recordpage.cpp | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c91a797f9..4837ba1f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,7 +92,7 @@ set_package_properties( set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) find_package( - KF${QT_MAJOR_VERSION} + KF${QT_MAJOR_VERSION} 5.42.0 COMPONENTS ThreadWeaver ConfigWidgets CoreAddons diff --git a/src/recordpage.cpp b/src/recordpage.cpp index db4e4f0e6..8d5ca935f 100644 --- a/src/recordpage.cpp +++ b/src/recordpage.cpp @@ -177,17 +177,14 @@ RecordPage::RecordPage(QWidget* parent) ui->setupUi(contents); } - auto completion = ui->applicationName->completionObject(); ui->applicationName->comboBox()->setEditable(true); - // NOTE: workaround until https://phabricator.kde.org/D7966 has landed and we bump the required version - ui->applicationName->comboBox()->setCompletionObject(completion); ui->applicationName->setMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly); -#if KIO_VERSION >= QT_VERSION_CHECK(5, 31, 0) + // we are only interested in executable files, so set the mime type filter accordingly // note that exe's build with PIE are actually "shared libs"... ui->applicationName->setMimeTypeFilters( {QStringLiteral("application/x-executable"), QStringLiteral("application/x-sharedlib")}); -#endif + ui->workingDirectory->setMode(KFile::Directory | KFile::LocalOnly); ui->outputFile->setText(QDir::currentPath() + QDir::separator() + QStringLiteral("perf.data")); ui->outputFile->setMode(KFile::File | KFile::LocalOnly); @@ -234,9 +231,6 @@ RecordPage::RecordPage(QWidget* parent) connect(ui->homeButton, &QPushButton::clicked, this, &RecordPage::homeButtonClicked); connect(ui->applicationName, &KUrlRequester::textChanged, this, &RecordPage::onApplicationNameChanged); - // NOTE: workaround until https://phabricator.kde.org/D7968 has landed and we bump the required version - connect(ui->applicationName->comboBox()->lineEdit(), &QLineEdit::textChanged, this, - &RecordPage::onApplicationNameChanged); connect(ui->startRecordingButton, &QPushButton::toggled, this, &RecordPage::onStartRecordingButtonClicked); connect(ui->workingDirectory, &KUrlRequester::textChanged, this, &RecordPage::onWorkingDirectoryNameChanged); connect(ui->viewPerfRecordResultsButton, &QPushButton::clicked, this, From 0fa1a0664c499023495cb47cfed167b3a447e363 Mon Sep 17 00:00:00 2001 From: Milian Wolff Date: Wed, 30 Aug 2023 16:00:56 +0200 Subject: [PATCH 2/7] Always compile tst_callgraphgenerator It doesn't depend on the viewer to be available --- tests/modeltests/CMakeLists.txt | 48 ++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/modeltests/CMakeLists.txt b/tests/modeltests/CMakeLists.txt index 336535a81..3934ae30b 100644 --- a/tests/modeltests/CMakeLists.txt +++ b/tests/modeltests/CMakeLists.txt @@ -49,28 +49,28 @@ set_target_properties( tst_disassemblyoutput PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${KDE_INSTALL_BINDIR}" ) -if(KGraphViewerPart_FOUND) - ecm_add_test( - tst_callgraphgenerator.cpp - ../../src/parsers/perf/perfparser.cpp - ../../src/initiallystoppedprocess.cpp - ../../src/perfcontrolfifowrapper.cpp - ../../src/perfrecord.cpp - ../../src/callgraphgenerator.cpp - ../../src/errnoutil.cpp - LINK_LIBRARIES - Qt::Core - Qt::Test - KF${QT_MAJOR_VERSION}::KIOCore - KF${QT_MAJOR_VERSION}::ThreadWeaver - KF${QT_MAJOR_VERSION}::Archive - KF${QT_MAJOR_VERSION}::WindowSystem - models - TEST_NAME - tst_callgraphgenerator - ) - - set_target_properties( - tst_callgraphgenerator PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${KDE_INSTALL_BINDIR}" - ) +ecm_add_test( + tst_callgraphgenerator.cpp + ../../src/parsers/perf/perfparser.cpp + ../../src/initiallystoppedprocess.cpp + ../../src/perfcontrolfifowrapper.cpp + ../../src/perfrecord.cpp + ../../src/callgraphgenerator.cpp + ../../src/errnoutil.cpp + LINK_LIBRARIES + Qt::Core + Qt::Test + KF${QT_MAJOR_VERSION}::KIOCore + KF${QT_MAJOR_VERSION}::ThreadWeaver + KF${QT_MAJOR_VERSION}::WindowSystem + models + TEST_NAME + tst_callgraphgenerator +) +if(${KFArchive_FOUND}) + target_link_libraries(tst_callgraphgenerator KF${QT_MAJOR_VERSION}::Archive) endif() + +set_target_properties( + tst_callgraphgenerator PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${KDE_INSTALL_BINDIR}" +) From fd3c50651b94adbd4afd9afa0ab7e719b4afbb43 Mon Sep 17 00:00:00 2001 From: Lieven Hey Date: Wed, 4 Jan 2023 15:16:55 +0100 Subject: [PATCH 3/7] add perf query interface and untangle it from recordpage moving the query part of the recordpage into its own class will make implementing remote recording much easier --- src/CMakeLists.txt | 2 + src/jobtracker.h | 63 ++++ src/multiconfigwidget.cpp | 3 + src/perfrecord.cpp | 156 +-------- src/perfrecord.h | 21 +- src/perfsettingspage.ui | 45 +++ src/recordhost.cpp | 368 ++++++++++++++++++++ src/recordhost.h | 128 +++++++ src/recordpage.cpp | 326 ++++++++--------- src/recordpage.h | 14 +- src/settings.cpp | 13 + src/settings.h | 9 + src/settingsdialog.cpp | 33 +- src/settingsdialog.h | 3 + tests/integrationtests/CMakeLists.txt | 1 + tests/integrationtests/tst_perfparser.cpp | 31 +- tests/modeltests/CMakeLists.txt | 1 + tests/modeltests/tst_callgraphgenerator.cpp | 4 +- 18 files changed, 858 insertions(+), 363 deletions(-) create mode 100644 src/jobtracker.h create mode 100644 src/perfsettingspage.ui create mode 100644 src/recordhost.cpp create mode 100644 src/recordhost.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7b97c8549..1df9a5864 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,6 +53,7 @@ set(HOTSPOT_SRCS initiallystoppedprocess.cpp perfcontrolfifowrapper.cpp errnoutil.cpp + recordhost.cpp # ui files: mainwindow.ui aboutdialog.ui @@ -73,6 +74,7 @@ set(HOTSPOT_SRCS callgraphsettingspage.ui frequencypage.ui sourcepathsettings.ui + perfsettingspage.ui # resources: resources.qrc ) diff --git a/src/jobtracker.h b/src/jobtracker.h new file mode 100644 index 000000000..8d17a4685 --- /dev/null +++ b/src/jobtracker.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: Milian Wolff + SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +class JobTracker +{ +public: + explicit JobTracker(QObject* context) + : m_context(context) + { + } + + bool isJobRunning() const + { + return m_context && m_isRunning; + } + + template + void startJob(Job&& job, SetData&& setData) + { + using namespace ThreadWeaver; + const auto jobId = ++m_currentJobId; + auto jobCancelled = [context = m_context, jobId, currentJobId = &m_currentJobId]() { + return !context || jobId != (*currentJobId); + }; + auto maybeSetData = [jobCancelled, setData = std::forward(setData), + isRunning = &m_isRunning](auto&& results) { + if (!jobCancelled()) { + setData(std::forward(results)); + *isRunning = false; + } + }; + + m_isRunning = true; + stream() << make_job([context = m_context, job = std::forward(job), maybeSetData = std::move(maybeSetData), + jobCancelled = std::move(jobCancelled)]() mutable { + auto results = job(jobCancelled); + if (jobCancelled()) + return; + + QMetaObject::invokeMethod( + context.data(), + [results = std::move(results), maybeSetData = std::move(maybeSetData)]() mutable { + maybeSetData(std::move(results)); + }, + Qt::QueuedConnection); + }); + } + +private: + QPointer m_context; + std::atomic m_currentJobId; + bool m_isRunning = false; +}; diff --git a/src/multiconfigwidget.cpp b/src/multiconfigwidget.cpp index 072c29ade..5225111f1 100644 --- a/src/multiconfigwidget.cpp +++ b/src/multiconfigwidget.cpp @@ -76,6 +76,9 @@ void MultiConfigWidget::setConfig(const KConfigGroup& group) m_comboBox->clear(); m_config = group; + if (!m_config.isValid()) + return; + const auto groups = m_config.groupList(); for (const auto& config : groups) { if (m_config.hasGroup(config)) { diff --git a/src/perfrecord.cpp b/src/perfrecord.cpp index a152b636a..e3adc5895 100644 --- a/src/perfrecord.cpp +++ b/src/perfrecord.cpp @@ -8,6 +8,8 @@ #include "perfrecord.h" +#include "recordhost.h" + #include #include #include @@ -19,8 +21,6 @@ #include #include -#include - #include #if KWINDOWSYSTEM_VERSION >= QT_VERSION_CHECK(5, 101, 0) #include @@ -28,11 +28,6 @@ #include #endif -#include - -#include -#include - namespace { void createOutputFile(const QString& outputPath) { @@ -44,17 +39,11 @@ void createOutputFile(const QString& outputPath) QFile::rename(outputPath, bakPath); QFile(outputPath).open(QIODevice::WriteOnly); } - -QString findPkexec() -{ - return QStandardPaths::findExecutable(QStringLiteral("pkexec")); -} } -PerfRecord::PerfRecord(QObject* parent) +PerfRecord::PerfRecord(const RecordHost* host, QObject* parent) : QObject(parent) - , m_perfRecordProcess(nullptr) - , m_userTerminated(false) + , m_host(host) { connect(&m_perfControlFifo, &PerfControlFifoWrapper::started, this, [this]() { m_targetProcessForPrivilegedPerf.continueStoppedProcess(); }); @@ -72,38 +61,6 @@ PerfRecord::~PerfRecord() } } -static bool privsAlreadyElevated() -{ - auto readSysctl = [](const char* path) { - std::ifstream ifs {path}; - int i = std::numeric_limits::min(); - if (ifs) { - ifs >> i; - } - return i; - }; - - bool isElevated = readSysctl("/proc/sys/kernel/kptr_restrict") == 0; - if (!isElevated) { - return false; - } - - isElevated = readSysctl("/proc/sys/kernel/perf_event_paranoid") == -1; - if (!isElevated) { - return false; - } - - auto checkPerms = [](const char* path) { - const mode_t required = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; // 755 - struct stat buf; - return stat(path, &buf) == 0 && ((buf.st_mode & 07777) & required) == required; - }; - static const auto paths = {"/sys/kernel/debug", "/sys/kernel/debug/tracing"}; - isElevated = std::all_of(paths.begin(), paths.end(), checkPerms); - - return isElevated; -} - bool PerfRecord::runPerf(bool elevatePrivileges, const QStringList& perfOptions, const QString& outputPath, const QString& workingDirectory) { @@ -176,14 +133,14 @@ bool PerfRecord::runPerf(bool elevatePrivileges, const QStringList& perfOptions, perfCommand += perfOptions; if (elevatePrivileges) { - const auto pkexec = findPkexec(); + const auto pkexec = RecordHost::pkexecBinaryPath(); if (pkexec.isEmpty()) { emit recordingFailed(tr("The pkexec utility was not found, cannot elevate privileges.")); return false; } auto options = QStringList(); - options.append(perfBinaryPath()); + options.append(m_host->perfBinaryPath()); options += perfCommand; if (!m_perfControlFifo.open()) { @@ -198,7 +155,7 @@ bool PerfRecord::runPerf(bool elevatePrivileges, const QStringList& perfOptions, m_perfRecordProcess->start(pkexec, options); } else { - m_perfRecordProcess->start(perfBinaryPath(), perfCommand); + m_perfRecordProcess->start(m_host->perfBinaryPath(), perfCommand); } return true; @@ -295,106 +252,13 @@ void PerfRecord::sendInput(const QByteArray& input) m_perfRecordProcess->write(input); } -QString PerfRecord::currentUsername() -{ - return KUser().loginName(); -} - -bool PerfRecord::canTrace(const QString& path) -{ - const auto info = QFileInfo(QLatin1String("/sys/kernel/debug/tracing/") + path); - if (!info.isDir() || !info.isReadable()) { - return false; - } - QFile paranoid(QStringLiteral("/proc/sys/kernel/perf_event_paranoid")); - return paranoid.open(QIODevice::ReadOnly) && paranoid.readAll().trimmed() == "-1"; -} - -static QByteArray perfOutput(const QStringList& arguments) -{ - QProcess process; - - auto reportError = [&]() { - qWarning() << "Failed to run perf" << process.arguments() << process.error() << process.errorString() - << process.readAllStandardError(); - }; - - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - env.insert(QStringLiteral("LANG"), QStringLiteral("C")); - process.setProcessEnvironment(env); - - QObject::connect(&process, &QProcess::errorOccurred, &process, reportError); - process.start(PerfRecord::perfBinaryPath(), arguments); - if (!process.waitForFinished(1000) || process.exitCode() != 0) - reportError(); - return process.readAllStandardOutput(); -} - -static QByteArray perfRecordHelp() -{ - static const QByteArray recordHelp = []() { - static QByteArray help = perfOutput({QStringLiteral("record"), QStringLiteral("--help")}); - if (help.isEmpty()) { - // no man page installed, assume the best - help = "--sample-cpu --switch-events"; - } - return help; - }(); - return recordHelp; -} - -static QByteArray perfBuildOptions() -{ - static const QByteArray buildOptions = perfOutput({QStringLiteral("version"), QStringLiteral("--build-options")}); - return buildOptions; -} - -bool PerfRecord::canProfileOffCpu() -{ - return canTrace(QStringLiteral("events/sched/sched_switch")); -} - QStringList PerfRecord::offCpuProfilingOptions() { return {QStringLiteral("--switch-events"), QStringLiteral("--event"), QStringLiteral("sched:sched_switch")}; } -bool PerfRecord::canSampleCpu() -{ - return perfRecordHelp().contains("--sample-cpu"); -} - -bool PerfRecord::canSwitchEvents() -{ - return perfRecordHelp().contains("--switch-events"); -} - -bool PerfRecord::canUseAio() -{ - return perfBuildOptions().contains("aio: [ on ]"); -} - -bool PerfRecord::canCompress() -{ - return Zstd_FOUND && perfBuildOptions().contains("zstd: [ on ]"); -} - -bool PerfRecord::canElevatePrivileges() -{ - return !findPkexec().isEmpty(); -} - -QString PerfRecord::perfBinaryPath() -{ - return QStandardPaths::findExecutable(QStringLiteral("perf")); -} - -bool PerfRecord::isPerfInstalled() -{ - return !perfBinaryPath().isEmpty(); -} - -bool PerfRecord::actuallyElevatePrivileges(bool elevatePrivileges) +bool PerfRecord::actuallyElevatePrivileges(bool elevatePrivileges) const { - return elevatePrivileges && canElevatePrivileges() && geteuid() != 0 && !privsAlreadyElevated(); + const auto capabilities = m_host->perfCapabilities(); + return elevatePrivileges && capabilities.canElevatePrivileges && !capabilities.privilegesAlreadyElevated; } diff --git a/src/perfrecord.h b/src/perfrecord.h index b5feeece6..ebcc4ce28 100644 --- a/src/perfrecord.h +++ b/src/perfrecord.h @@ -15,12 +15,13 @@ #include class QProcess; +class RecordHost; class PerfRecord : public QObject { Q_OBJECT public: - explicit PerfRecord(QObject* parent = nullptr); + explicit PerfRecord(const RecordHost* host, QObject* parent = nullptr); ~PerfRecord(); void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges, @@ -33,21 +34,8 @@ class PerfRecord : public QObject void stopRecording(); void sendInput(const QByteArray& input); - static QString currentUsername(); - - static bool canTrace(const QString& path); - static bool canProfileOffCpu(); - static bool canSampleCpu(); - static bool canSwitchEvents(); - static bool canUseAio(); - static bool canCompress(); - static bool canElevatePrivileges(); - static QStringList offCpuProfilingOptions(); - static QString perfBinaryPath(); - static bool isPerfInstalled(); - signals: void recordingStarted(const QString& perfBinary, const QStringList& arguments); void recordingFinished(const QString& fileLocation); @@ -56,13 +44,14 @@ class PerfRecord : public QObject void debuggeeCrashed(); private: + const RecordHost* m_host = nullptr; QPointer m_perfRecordProcess; InitiallyStoppedProcess m_targetProcessForPrivilegedPerf; PerfControlFifoWrapper m_perfControlFifo; QString m_outputPath; - bool m_userTerminated; + bool m_userTerminated = false; - static bool actuallyElevatePrivileges(bool elevatePrivileges); + bool actuallyElevatePrivileges(bool elevatePrivileges) const; bool runPerf(bool elevatePrivileges, const QStringList& perfOptions, const QString& outputPath, const QString& workingDirectory = QString()); diff --git a/src/perfsettingspage.ui b/src/perfsettingspage.ui new file mode 100644 index 000000000..bfea262fd --- /dev/null +++ b/src/perfsettingspage.ui @@ -0,0 +1,45 @@ + + + PerfSettingsPage + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + Perf Binary: + + + perfPathEdit + + + + + + + perf + + + + + + + + KUrlRequester + QWidget +
kurlrequester.h
+
+
+ + +
diff --git a/src/recordhost.cpp b/src/recordhost.cpp new file mode 100644 index 000000000..df5b53f58 --- /dev/null +++ b/src/recordhost.cpp @@ -0,0 +1,368 @@ +/* + SPDX-FileCopyrightText: Lieven Hey + SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "recordhost.h" +#include "settings.h" + +#include "hotspot-config.h" + +namespace { +QByteArray perfOutput(const QString& perfPath, const QStringList& arguments) +{ + if (perfPath.isEmpty()) + return {}; + + // TODO handle error if man is not installed + QProcess process; + + auto reportError = [&]() { + qWarning() << "Failed to run perf" << process.arguments() << process.error() << process.errorString() + << process.readAllStandardError(); + }; + + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("LANG"), QStringLiteral("C")); + process.setProcessEnvironment(env); + + QObject::connect(&process, &QProcess::errorOccurred, &process, reportError); + process.start(perfPath, arguments); + if (!process.waitForFinished(1000) || process.exitCode() != 0) + reportError(); + return process.readAllStandardOutput(); +} + +QByteArray perfRecordHelp(const QString& perfPath) +{ + QByteArray recordHelp = [&perfPath]() { + QByteArray help = perfOutput(perfPath, {QStringLiteral("record"), QStringLiteral("--help")}); + if (help.isEmpty()) { + // no man page installed, assume the best + help = "--sample-cpu --switch-events"; + } + return help; + }(); + return recordHelp; +} + +QByteArray perfBuildOptions(const QString& perfPath) +{ + return perfOutput(perfPath, {QStringLiteral("version"), QStringLiteral("--build-options")}); +} + +bool canTrace(const QString& path) +{ + const QFileInfo info(QLatin1String("/sys/kernel/debug/tracing/") + path); + if (!info.isDir() || !info.isReadable()) { + return false; + } + QFile paranoid(QStringLiteral("/proc/sys/kernel/perf_event_paranoid")); + return paranoid.open(QIODevice::ReadOnly) && paranoid.readAll().trimmed() == "-1"; +} + +QString findPkexec() +{ + return QStandardPaths::findExecutable(QStringLiteral("pkexec")); +} + +bool canElevatePrivileges() +{ + return !findPkexec().isEmpty(); +} + +bool privsAlreadyElevated() +{ + if (KUser().isSuperUser()) + return true; + + auto readSysctl = [](const char* path) { + std::ifstream ifs {path}; + int i = std::numeric_limits::min(); + if (ifs) { + ifs >> i; + } + return i; + }; + + bool isElevated = readSysctl("/proc/sys/kernel/kptr_restrict") == 0; + if (!isElevated) { + return false; + } + + isElevated = readSysctl("/proc/sys/kernel/perf_event_paranoid") == -1; + if (!isElevated) { + return false; + } + + auto checkPerms = [](const char* path) { + const mode_t required = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; // 755 + struct stat buf; + return stat(path, &buf) == 0 && ((buf.st_mode & 07777) & required) == required; + }; + static const auto paths = {"/sys/kernel/debug", "/sys/kernel/debug/tracing"}; + isElevated = std::all_of(paths.begin(), paths.end(), checkPerms); + + return isElevated; +} + +RecordHost::PerfCapabilities fetchLocalPerfCapabilities(const QString& perfPath) +{ + RecordHost::PerfCapabilities capabilities; + + const auto buildOptions = perfBuildOptions(perfPath); + const auto help = perfRecordHelp(perfPath); + capabilities.canCompress = Zstd_FOUND && buildOptions.contains("zstd: [ on ]"); + capabilities.canUseAio = buildOptions.contains("aio: [ on ]"); + capabilities.canSwitchEvents = help.contains("--switch-events"); + capabilities.canSampleCpu = help.contains("--sample-cpu"); + capabilities.canProfileOffCpu = canTrace(QStringLiteral("events/sched/sched_switch")); + + const auto isElevated = privsAlreadyElevated(); + capabilities.privilegesAlreadyElevated = isElevated; + capabilities.canElevatePrivileges = isElevated || canElevatePrivileges(); + + return capabilities; +} +} + +RecordHost::RecordHost(QObject* parent) + : QObject(parent) + , m_checkPerfCapabilitiesJob(this) + , m_checkPerfInstalledJob(this) +{ + connect(this, &RecordHost::errorOccurred, this, [this](const QString& message) { m_error = message; }); + + auto connectIsReady = [this](auto&& signal) { + connect(this, signal, this, [this] { emit isReadyChanged(isReady()); }); + }; + + connectIsReady(&RecordHost::clientApplicationChanged); + connectIsReady(&RecordHost::isPerfInstalledChanged); + connectIsReady(&RecordHost::perfCapabilitiesChanged); + connectIsReady(&RecordHost::recordTypeChanged); + connectIsReady(&RecordHost::pidsChanged); + connectIsReady(&RecordHost::currentWorkingDirectoryChanged); + + setHost(QStringLiteral("localhost")); +} + +RecordHost::~RecordHost() = default; + +bool RecordHost::isReady() const +{ + switch (m_recordType) { + case RecordType::LaunchApplication: + // client application is already validated in the setter + if (m_clientApplication.isEmpty() && m_cwd.isEmpty()) + return false; + break; + case RecordType::AttachToProcess: + if (m_pids.isEmpty()) + return false; + break; + case RecordType::ProfileSystem: + break; + case RecordType::NUM_RECORD_TYPES: + Q_ASSERT(false); + } + + // it is save to run, when all queries where resolved and there are now errors + const std::initializer_list jobs = {&m_checkPerfCapabilitiesJob, &m_checkPerfInstalledJob}; + + return m_isPerfInstalled && m_error.isEmpty() + && std::none_of(jobs.begin(), jobs.end(), [](const JobTracker* job) { return job->isJobRunning(); }); +} + +void RecordHost::setHost(const QString& host) +{ + Q_ASSERT(QThread::currentThread() == thread()); + + // don't refresh if on the same host + if (host == m_host) + return; + + emit isReadyChanged(false); + + m_host = host; + emit hostChanged(); + + // invalidate everything + m_cwd.clear(); + emit currentWorkingDirectoryChanged(m_cwd); + + m_clientApplication.clear(); + emit clientApplicationChanged(m_clientApplication); + + m_perfCapabilities = {}; + emit perfCapabilitiesChanged(m_perfCapabilities); + + const auto perfPath = perfBinaryPath(); + m_checkPerfCapabilitiesJob.startJob([perfPath](auto&&) { return fetchLocalPerfCapabilities(perfPath); }, + [this](RecordHost::PerfCapabilities capabilities) { + Q_ASSERT(QThread::currentThread() == thread()); + + m_perfCapabilities = capabilities; + emit perfCapabilitiesChanged(m_perfCapabilities); + }); + + m_checkPerfInstalledJob.startJob( + [isLocal = isLocal(), perfPath](auto&&) { + if (isLocal) { + if (perfPath.isEmpty()) { + return !QStandardPaths::findExecutable(QStringLiteral("perf")).isEmpty(); + } + + return QFileInfo::exists(perfPath); + } + + qWarning() << "remote is not implemented"; + return false; + }, + [this](bool isInstalled) { + if (!isInstalled) { + emit errorOccurred(tr("perf is not installed")); + } + m_isPerfInstalled = isInstalled; + emit isPerfInstalledChanged(isInstalled); + }); +} + +void RecordHost::setCurrentWorkingDirectory(const QString& cwd) +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (isLocal()) { + const QFileInfo folder(cwd); + + if (!folder.exists()) { + emit errorOccurred(tr("Working directory folder cannot be found: %1").arg(cwd)); + } else if (!folder.isDir()) { + emit errorOccurred(tr("Working directory folder is not valid: %1").arg(cwd)); + } else if (!folder.isWritable()) { + emit errorOccurred(tr("Working directory folder is not writable: %1").arg(cwd)); + } else { + emit errorOccurred({}); + m_cwd = cwd; + emit currentWorkingDirectoryChanged(cwd); + } + return; + } + + qWarning() << "is not implemented for remote"; +} + +void RecordHost::setClientApplication(const QString& clientApplication) +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (isLocal()) { + QFileInfo application(KShell::tildeExpand(clientApplication)); + if (!application.exists()) { + application.setFile(QStandardPaths::findExecutable(clientApplication)); + } + + if (!application.exists()) { + emit errorOccurred(tr("Application file cannot be found: %1").arg(clientApplication)); + } else if (!application.isFile()) { + emit errorOccurred(tr("Application file is not valid: %1").arg(clientApplication)); + } else if (!application.isExecutable()) { + emit errorOccurred(tr("Application file is not executable: %1").arg(clientApplication)); + } else { + emit errorOccurred({}); + m_clientApplication = clientApplication; + emit clientApplicationChanged(m_clientApplication); + } + + if (m_cwd.isEmpty()) { + setCurrentWorkingDirectory(application.dir().absolutePath()); + } + return; + } + + qWarning() << "is not implemented for remote"; +} + +void RecordHost::setOutputFileName(const QString& filePath) +{ + if (isLocal()) { + const auto perfDataExtension = QStringLiteral(".data"); + + const QFileInfo file(filePath); + const QFileInfo folder(file.absolutePath()); + + if (!folder.exists()) { + emit errorOccurred(tr("Output file directory folder cannot be found: %1").arg(folder.path())); + } else if (!folder.isDir()) { + emit errorOccurred(tr("Output file directory folder is not valid: %1").arg(folder.path())); + } else if (!folder.isWritable()) { + emit errorOccurred(tr("Output file directory folder is not writable: %1").arg(folder.path())); + } else if (!file.absoluteFilePath().endsWith(perfDataExtension)) { + emit errorOccurred(tr("Output file must end with %1").arg(perfDataExtension)); + } else { + emit errorOccurred({}); + m_outputFileName = filePath; + emit outputFileNameChanged(m_outputFileName); + } + + return; + } + + qWarning() << "is not implemented for remote"; +} + +void RecordHost::setRecordType(RecordType type) +{ + if (m_recordType != type) { + m_recordType = type; + emit recordTypeChanged(m_recordType); + + m_pids.clear(); + emit pidsChanged(); + } +} + +void RecordHost::setPids(const QStringList& pids) +{ + if (m_pids != pids) { + m_pids = pids; + emit pidsChanged(); + } +} + +bool RecordHost::isLocal() const +{ + return m_host == QLatin1String("localhost"); +} + +QString RecordHost::pkexecBinaryPath() +{ + return findPkexec(); +} + +QString RecordHost::perfBinaryPath() const +{ + if (isLocal()) { + auto perf = Settings::instance()->perfPath(); + if (perf.isEmpty()) + perf = QStandardPaths::findExecutable(QStringLiteral("perf")); + return perf; + } + return {}; +} diff --git a/src/recordhost.h b/src/recordhost.h new file mode 100644 index 000000000..5023d1cbf --- /dev/null +++ b/src/recordhost.h @@ -0,0 +1,128 @@ +/* + SPDX-FileCopyrightText: Lieven Hey + SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "jobtracker.h" + +#include + +enum class RecordType +{ + LaunchApplication, + AttachToProcess, + ProfileSystem, + NUM_RECORD_TYPES +}; +Q_DECLARE_METATYPE(RecordType) + +class RecordHost : public QObject +{ + Q_OBJECT +public: + explicit RecordHost(QObject* parent = nullptr); + ~RecordHost() override; + + // might be false when async ops is ongoing internally + bool isReady() const; + QString errorMessage() const + { + return m_error; + } + + bool isPerfInstalled() const + { + return m_isPerfInstalled; + } + + QString host() const + { + return m_host; + } + void setHost(const QString& host); + + // TODO: username etc. pp. + + QString currentWorkingDirectory() const + { + return m_cwd; + } + void setCurrentWorkingDirectory(const QString& cwd); + + QString clientApplication() const + { + return m_clientApplication; + } + void setClientApplication(const QString& clientApplication); + + QString outputFileName() const + { + return m_outputFileName; + } + void setOutputFileName(const QString& filePath); + + static QString pkexecBinaryPath(); + QString perfBinaryPath() const; + + // async query options + struct PerfCapabilities + { + // see all virtuals in PerfRecord can* + bool canProfileOffCpu = false; + bool canSampleCpu = false; + bool canSwitchEvents = false; + bool canUseAio = false; + bool canCompress = false; + bool canElevatePrivileges = false; + bool privilegesAlreadyElevated = false; + }; + PerfCapabilities perfCapabilities() const + { + // reset member to default all = false when host or perf binary changes + return m_perfCapabilities; + } + + RecordType recordType() const + { + return m_recordType; + } + void setRecordType(RecordType type); + + // list of pids to record + void setPids(const QStringList& pids); + +signals: + /// disallow "start" on recordpage until this is ready and that should only be the case when there's no error + void isReadyChanged(bool isReady); + + void errorOccurred(const QString& message); + void hostChanged(); + void currentWorkingDirectoryChanged(const QString& cwd); // Maybe QUrl + void clientApplicationChanged(const QString& clientApplication); + void perfCapabilitiesChanged(RecordHost::PerfCapabilities perfCapabilities); + void isPerfInstalledChanged(bool isInstalled); + void outputFileNameChanged(const QString& outputFileName); + void recordTypeChanged(RecordType type); + void pidsChanged(); + +private: + bool isLocal() const; + + QString m_host; + QString m_error; + QString m_cwd; + QString m_clientApplication; + QString m_outputFileName; + PerfCapabilities m_perfCapabilities; + JobTracker m_checkPerfCapabilitiesJob; + JobTracker m_checkPerfInstalledJob; + RecordType m_recordType = RecordType::LaunchApplication; + bool m_isPerfInstalled = false; + QStringList m_pids; +}; + +Q_DECLARE_METATYPE(RecordHost::PerfCapabilities) diff --git a/src/recordpage.cpp b/src/recordpage.cpp index 8d5ca935f..7d9a4a565 100644 --- a/src/recordpage.cpp +++ b/src/recordpage.cpp @@ -11,6 +11,7 @@ #include "processfiltermodel.h" #include "processmodel.h" +#include "recordhost.h" #include "resultsutil.h" #include "util.h" @@ -33,6 +34,7 @@ #include #include #include +#include #include #include @@ -73,9 +75,9 @@ RecordType selectedRecordType(const std::unique_ptr& ui) return ui->recordTypeComboBox->currentData().value(); } -void updateStartRecordingButtonState(const std::unique_ptr& ui) +void updateStartRecordingButtonState(const RecordHost* host, const std::unique_ptr& ui) { - if (!PerfRecord::isPerfInstalled()) { + if (!host->isPerfInstalled()) { ui->startRecordingButton->setEnabled(false); ui->applicationRecordErrorMessage->setText(QObject::tr("Please install perf before trying to record.")); ui->applicationRecordErrorMessage->setVisible(true); @@ -84,17 +86,17 @@ void updateStartRecordingButtonState(const std::unique_ptr& ui) bool enabled = false; switch (selectedRecordType(ui)) { - case LaunchApplication: + case RecordType::LaunchApplication: enabled = ui->applicationName->url().isValid(); break; - case AttachToProcess: + case RecordType::AttachToProcess: enabled = ui->processesTableView->selectionModel()->hasSelection(); break; - case ProfileSystem: + case RecordType::ProfileSystem: enabled = true; break; - case NUM_RECORD_TYPES: - break; + case RecordType::NUM_RECORD_TYPES: + Q_UNREACHABLE(); } enabled &= ui->applicationRecordErrorMessage->text().isEmpty(); @@ -108,6 +110,8 @@ KConfigGroup config() KConfigGroup applicationConfig(const QString& application) { + if (application.isEmpty()) + return {}; return config().group(QLatin1String("Application ") + KShell::tildeExpand(application)); } @@ -160,7 +164,8 @@ void rememberApplication(const QString& application, const QString& appParameter RecordPage::RecordPage(QWidget* parent) : QWidget(parent) , ui(std::make_unique()) - , m_perfRecord(new PerfRecord(this)) + , m_recordHost(new RecordHost(this)) + , m_perfRecord(new PerfRecord(m_recordHost, this)) , m_updateRuntimeTimer(new QTimer(this)) , m_watcher(new QFutureWatcher(this)) { @@ -177,6 +182,66 @@ RecordPage::RecordPage(QWidget* parent) ui->setupUi(contents); } + connect(m_recordHost, &RecordHost::errorOccurred, this, &RecordPage::setError); + connect(m_recordHost, &RecordHost::isReadyChanged, this, + [this](bool isReady) { ui->startRecordingButton->setEnabled(isReady); }); + + connect(m_recordHost, &RecordHost::isPerfInstalledChanged, this, [this](bool isInstalled) { + if (!isInstalled) { + ui->startRecordingButton->setEnabled(false); + ui->applicationRecordErrorMessage->setText(QObject::tr("Please install perf before trying to record.")); + ui->applicationRecordErrorMessage->setVisible(true); + } + }); + + connect(m_recordHost, &RecordHost::clientApplicationChanged, this, [this](const QString& filePath) { + const auto config = applicationConfig(filePath); + ui->workingDirectory->setText(config.readEntry("workingDir", QString())); + ui->applicationParametersBox->setText(config.readEntry("params", QString())); + + m_multiConfig->setConfig(applicationConfig(ui->applicationName->text())); + }); + + ui->compressionComboBox->addItem(tr("Disabled"), -1); + ui->compressionComboBox->addItem(tr("Enabled (Default Level)"), 0); + ui->compressionComboBox->addItem(tr("Level 1 (Fastest)"), 1); + for (int i = 2; i <= 21; ++i) + ui->compressionComboBox->addItem(tr("Level %1").arg(i), 0); + ui->compressionComboBox->addItem(tr("Level 22 (Slowest)"), 22); + ui->compressionComboBox->setCurrentIndex(1); + const auto defaultLevel = ui->compressionComboBox->currentData().toInt(); + const auto level = config().readEntry(QStringLiteral("compressionLevel"), defaultLevel); + const auto index = ui->compressionComboBox->findData(level); + if (index != -1) + ui->compressionComboBox->setCurrentIndex(index); + + connect(m_recordHost, &RecordHost::perfCapabilitiesChanged, this, + [this](RecordHost::PerfCapabilities capabilities) { + ui->sampleCpuCheckBox->setVisible(capabilities.canSampleCpu); + ui->sampleCpuLabel->setVisible(capabilities.canSampleCpu); + + ui->offCpuCheckBox->setVisible(capabilities.canSwitchEvents); + ui->offCpuLabel->setVisible(capabilities.canSwitchEvents); + + ui->useAioCheckBox->setVisible(capabilities.canUseAio); + ui->useAioLabel->setVisible(capabilities.canUseAio); + + ui->compressionComboBox->setVisible(capabilities.canCompress); + ui->compressionLabel->setVisible(capabilities.canCompress); + + if (!capabilities.canElevatePrivileges) { + ui->elevatePrivilegesCheckBox->setChecked(false); + ui->elevatePrivilegesCheckBox->setEnabled(false); + ui->elevatePrivilegesCheckBox->setText( + tr("(Note: Install pkexec, kdesudo, kdesu or KAuth to temporarily elevate perf privileges.)")); + } else { + ui->elevatePrivilegesCheckBox->setEnabled(true); + ui->elevatePrivilegesCheckBox->setText({}); + } + }); + + m_recordHost->setHost(QStringLiteral("localhost")); + ui->applicationName->comboBox()->setEditable(true); ui->applicationName->setMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly); @@ -187,6 +252,7 @@ RecordPage::RecordPage(QWidget* parent) ui->workingDirectory->setMode(KFile::Directory | KFile::LocalOnly); ui->outputFile->setText(QDir::currentPath() + QDir::separator() + QStringLiteral("perf.data")); + m_recordHost->setOutputFileName(QDir::currentPath() + QDir::separator() + QStringLiteral("perf.data")); ui->outputFile->setMode(KFile::File | KFile::LocalOnly); ui->eventTypeBox->lineEdit()->setPlaceholderText(tr("perf defaults (usually cycles:Pu)")); @@ -232,22 +298,25 @@ RecordPage::RecordPage(QWidget* parent) connect(ui->homeButton, &QPushButton::clicked, this, &RecordPage::homeButtonClicked); connect(ui->applicationName, &KUrlRequester::textChanged, this, &RecordPage::onApplicationNameChanged); connect(ui->startRecordingButton, &QPushButton::toggled, this, &RecordPage::onStartRecordingButtonClicked); - connect(ui->workingDirectory, &KUrlRequester::textChanged, this, &RecordPage::onWorkingDirectoryNameChanged); - connect(ui->viewPerfRecordResultsButton, &QPushButton::clicked, this, - &RecordPage::onViewPerfRecordResultsButtonClicked); + connect(ui->workingDirectory, &KUrlRequester::textChanged, m_recordHost, + &RecordHost::currentWorkingDirectoryChanged); + connect(ui->viewPerfRecordResultsButton, &QPushButton::clicked, this, [this] { emit openFile(m_resultsFile); }); connect(ui->outputFile, &KUrlRequester::textChanged, this, &RecordPage::onOutputFileNameChanged); connect(ui->outputFile, static_cast(&KUrlRequester::returnPressed), this, &RecordPage::onOutputFileNameSelected); connect(ui->outputFile, &KUrlRequester::urlSelected, this, &RecordPage::onOutputFileUrlChanged); ui->recordTypeComboBox->addItem(QIcon::fromTheme(QStringLiteral("run-build")), tr("Launch Application"), - QVariant::fromValue(LaunchApplication)); + QVariant::fromValue(RecordType::LaunchApplication)); ui->recordTypeComboBox->addItem(QIcon::fromTheme(QStringLiteral("run-install")), tr("Attach To Process(es)"), - QVariant::fromValue(AttachToProcess)); + QVariant::fromValue(RecordType::AttachToProcess)); ui->recordTypeComboBox->addItem(QIcon::fromTheme(QStringLiteral("run-build-install-root")), tr("Profile System"), - QVariant::fromValue(ProfileSystem)); - connect(ui->recordTypeComboBox, static_cast(&QComboBox::currentIndexChanged), this, + QVariant::fromValue(RecordType::ProfileSystem)); + connect(ui->recordTypeComboBox, qOverload(&QComboBox::currentIndexChanged), this, &RecordPage::updateRecordType); + connect(ui->recordTypeComboBox, qOverload(&QComboBox::currentIndexChanged), m_recordHost, + [this] { m_recordHost->setRecordType(ui->recordTypeComboBox->currentData().value()); }); + connect(m_recordHost, &RecordHost::clientApplicationChanged, this, &RecordPage::updateRecordType); { ui->callGraphComboBox->addItem(tr("None"), QVariant::fromValue(QString())); @@ -356,33 +425,63 @@ RecordPage::RecordPage(QWidget* parent) ui->processesTableView->setSelectionBehavior(QAbstractItemView::SelectRows); ui->processesTableView->setSelectionMode(QAbstractItemView::MultiSelection); connect(ui->processesTableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, - [this]() { updateStartRecordingButtonState(ui); }); + [this](const QItemSelection& selectedIndexes, const QItemSelection&) { + QStringList pids; + + const auto selection = selectedIndexes.indexes(); + for (const auto& item : selection) { + if (item.column() == 0) { + pids.append(item.data(ProcessModel::PIDRole).toString()); + } + } + m_recordHost->setPids(pids); + }); ResultsUtil::connectFilter(ui->processesFilterBox, m_processProxyModel); connect(m_watcher, &QFutureWatcher::finished, this, &RecordPage::updateProcessesFinished); - if (PerfRecord::currentUsername() == QLatin1String("root")) { - ui->elevatePrivilegesCheckBox->setChecked(true); - ui->elevatePrivilegesCheckBox->setEnabled(false); - } else if (!PerfRecord::canElevatePrivileges()) { - ui->elevatePrivilegesCheckBox->setChecked(false); - ui->elevatePrivilegesCheckBox->setEnabled(false); - ui->elevatePrivilegesCheckBox->setText(tr("(Note: this requires pkexec installed")); - } + auto updateOffCpuCheckboxState = [this](RecordHost::PerfCapabilities capabilities) { + const bool enableOffCpuProfiling = (ui->elevatePrivilegesCheckBox->isChecked() || capabilities.canProfileOffCpu) + && capabilities.canSwitchEvents; - connect(ui->elevatePrivilegesCheckBox, &QCheckBox::toggled, this, &RecordPage::updateOffCpuCheckboxState); + if (enableOffCpuProfiling == ui->offCpuCheckBox->isEnabled()) { + return; + } + + ui->offCpuCheckBox->setEnabled(enableOffCpuProfiling); + + // prevent user confusion: don't show the value as checked when the checkbox is disabled + if (!enableOffCpuProfiling) { + // remember the current value + config().writeEntry(QStringLiteral("offCpuProfiling"), ui->offCpuCheckBox->isChecked()); + ui->offCpuCheckBox->setChecked(false); + } else { + ui->offCpuCheckBox->setChecked(config().readEntry(QStringLiteral("offCpuProfiling"), false)); + } + }; + + connect(ui->elevatePrivilegesCheckBox, &QCheckBox::toggled, this, + [this, updateOffCpuCheckboxState] { updateOffCpuCheckboxState(m_recordHost->perfCapabilities()); }); + + connect(m_recordHost, &RecordHost::perfCapabilitiesChanged, this, updateOffCpuCheckboxState); restoreCombobox(config(), QStringLiteral("applications"), ui->applicationName->comboBox()); restoreCombobox(config(), QStringLiteral("eventType"), ui->eventTypeBox, {ui->eventTypeBox->currentText()}); restoreCombobox(config(), QStringLiteral("customOptions"), ui->perfParams); - ui->elevatePrivilegesCheckBox->setChecked(PerfRecord::canElevatePrivileges() - && config().readEntry(QStringLiteral("elevatePrivileges"), false)); + + // set application in RecordHost if it was restored + m_recordHost->setClientApplication(ui->applicationName->url().toLocalFile()); + + ui->elevatePrivilegesCheckBox->setChecked(config().readEntry(QStringLiteral("elevatePrivileges"), false)); ui->offCpuCheckBox->setChecked(config().readEntry(QStringLiteral("offCpuProfiling"), false)); ui->sampleCpuCheckBox->setChecked(config().readEntry(QStringLiteral("sampleCpu"), true)); ui->mmapPagesSpinBox->setValue(config().readEntry(QStringLiteral("mmapPages"), 16)); ui->mmapPagesUnitComboBox->setCurrentIndex(config().readEntry(QStringLiteral("mmapPagesUnit"), 2)); - ui->useAioCheckBox->setChecked(config().readEntry(QStringLiteral("useAio"), PerfRecord::canUseAio())); + connect(m_recordHost, &RecordHost::perfCapabilitiesChanged, this, + [this](RecordHost::PerfCapabilities capabilities) { + ui->useAioCheckBox->setChecked(config().readEntry(QStringLiteral("useAio"), capabilities.canUseAio)); + }); const auto callGraph = config().readEntry("callGraph", ui->callGraphComboBox->currentData()); const auto callGraphIdx = ui->callGraphComboBox->findData(callGraph); @@ -390,8 +489,6 @@ RecordPage::RecordPage(QWidget* parent) ui->callGraphComboBox->setCurrentIndex(callGraphIdx); } - updateOffCpuCheckboxState(); - m_updateRuntimeTimer->setInterval(1000); connect(m_updateRuntimeTimer, &QTimer::timeout, this, [this] { // round to the nearest second @@ -414,37 +511,6 @@ RecordPage::RecordPage(QWidget* parent) } }); - if (!PerfRecord::canSampleCpu()) { - ui->sampleCpuCheckBox->hide(); - ui->sampleCpuLabel->hide(); - } - if (!PerfRecord::canSwitchEvents()) { - ui->offCpuCheckBox->hide(); - ui->offCpuLabel->hide(); - } - if (!PerfRecord::canUseAio()) { - ui->useAioCheckBox->hide(); - ui->useAioLabel->hide(); - } - if (!PerfRecord::canCompress()) { - ui->compressionComboBox->hide(); - ui->compressionLabel->hide(); - } else { - ui->compressionComboBox->addItem(tr("Disabled"), -1); - ui->compressionComboBox->addItem(tr("Enabled (Default Level)"), 0); - ui->compressionComboBox->addItem(tr("Level 1 (Fastest)"), 1); - for (int i = 2; i <= 21; ++i) - ui->compressionComboBox->addItem(tr("Level %1").arg(i), 0); - ui->compressionComboBox->addItem(tr("Level 22 (Slowest)"), 22); - - ui->compressionComboBox->setCurrentIndex(1); - const auto defaultLevel = ui->compressionComboBox->currentData().toInt(); - const auto level = config().readEntry(QStringLiteral("compressionLevel"), defaultLevel); - const auto index = ui->compressionComboBox->findData(level); - if (index != -1) - ui->compressionComboBox->setCurrentIndex(index); - } - showRecordPage(); ui->applicationRecordWarningMessage->setVisible(false); @@ -475,6 +541,8 @@ void RecordPage::onStartRecordingButtonClicked(bool checked) m_perfOutput->clear(); ui->applicationRecordWarningMessage->hide(); + auto perfCapabilities = m_recordHost->perfCapabilities(); + QStringList perfOptions; const auto callGraphOption = ui->callGraphComboBox->currentData().toString(); @@ -499,7 +567,7 @@ void RecordPage::onStartRecordingButtonClicked(bool checked) perfOptions += KShell::splitArgs(customOptions); const bool offCpuProfilingEnabled = ui->offCpuCheckBox->isChecked(); - if (offCpuProfilingEnabled && PerfRecord::canSwitchEvents()) { + if (offCpuProfilingEnabled && perfCapabilities.canSwitchEvents) { if (eventType.isEmpty()) { // TODO: use clock event in VM context perfOptions += QStringLiteral("--event"); @@ -510,13 +578,13 @@ void RecordPage::onStartRecordingButtonClicked(bool checked) config().writeEntry(QStringLiteral("offCpuProfiling"), offCpuProfilingEnabled); const bool useAioEnabled = ui->useAioCheckBox->isChecked(); - if (useAioEnabled && PerfRecord::canUseAio()) { + if (useAioEnabled && perfCapabilities.canUseAio) { perfOptions += QStringLiteral("--aio"); } config().writeEntry(QStringLiteral("useAio"), useAioEnabled); const auto compressionLevel = ui->compressionComboBox->currentData().toInt(); - if (PerfRecord::canCompress() && compressionLevel >= 0) { + if (perfCapabilities.canCompress && compressionLevel >= 0) { if (compressionLevel == 0) perfOptions += QStringLiteral("-z"); else @@ -527,11 +595,11 @@ void RecordPage::onStartRecordingButtonClicked(bool checked) const bool elevatePrivileges = ui->elevatePrivilegesCheckBox->isChecked(); const bool sampleCpuEnabled = ui->sampleCpuCheckBox->isChecked(); - if (sampleCpuEnabled && PerfRecord::canSampleCpu()) { + if (sampleCpuEnabled && perfCapabilities.canSampleCpu) { perfOptions += QStringLiteral("--sample-cpu"); } - if (recordType != ProfileSystem) { // always true when recording full system + if (recordType != RecordType::ProfileSystem) { // always true when recording full system config().writeEntry(QStringLiteral("elevatePrivileges"), elevatePrivileges); config().writeEntry(QStringLiteral("sampleCpu"), sampleCpuEnabled); } @@ -566,13 +634,13 @@ void RecordPage::onStartRecordingButtonClicked(bool checked) config().writeEntry(QStringLiteral("mmapPages"), mmapPages); config().writeEntry(QStringLiteral("mmapPagesUnit"), mmapPagesUnit); - const auto outputFile = ui->outputFile->url().toLocalFile(); + const auto outputFile = m_recordHost->outputFileName(); switch (recordType) { - case LaunchApplication: { - const auto applicationName = KShell::tildeExpand(ui->applicationName->text()); + case RecordType::LaunchApplication: { + const auto applicationName = m_recordHost->clientApplication(); const auto appParameters = ui->applicationParametersBox->text(); - auto workingDir = ui->workingDirectory->text(); + auto workingDir = m_recordHost->currentWorkingDirectory(); if (workingDir.isEmpty()) { workingDir = ui->workingDirectory->placeholderText(); } @@ -581,7 +649,7 @@ void RecordPage::onStartRecordingButtonClicked(bool checked) KShell::splitArgs(appParameters), workingDir); break; } - case AttachToProcess: { + case RecordType::AttachToProcess: { QItemSelectionModel* selectionModel = ui->processesTableView->selectionModel(); QStringList pids; @@ -595,11 +663,11 @@ void RecordPage::onStartRecordingButtonClicked(bool checked) m_perfRecord->record(perfOptions, outputFile, elevatePrivileges, pids); break; } - case ProfileSystem: { + case RecordType::ProfileSystem: { m_perfRecord->recordSystem(perfOptions, outputFile); break; } - case NUM_RECORD_TYPES: + case RecordType::NUM_RECORD_TYPES: break; } } else { @@ -629,78 +697,17 @@ void RecordPage::stopRecording() void RecordPage::onApplicationNameChanged(const QString& filePath) { - QFileInfo application(KShell::tildeExpand(filePath)); - if (!application.exists()) { - application.setFile(QStandardPaths::findExecutable(filePath)); - } - - if (!application.exists()) { - setError(tr("Application file cannot be found: %1").arg(filePath)); - } else if (!application.isFile()) { - setError(tr("Application file is not valid: %1").arg(filePath)); - } else if (!application.isExecutable()) { - setError(tr("Application file is not executable: %1").arg(filePath)); - } else { - const auto config = applicationConfig(filePath); - ui->workingDirectory->setText(config.readEntry("workingDir", QString())); - ui->applicationParametersBox->setText(config.readEntry("params", QString())); - ui->workingDirectory->setPlaceholderText(application.path()); - setError({}); - - m_multiConfig->setConfig(applicationConfig(ui->applicationName->text())); - } - updateStartRecordingButtonState(ui); + m_recordHost->setClientApplication(filePath); } -void RecordPage::onWorkingDirectoryNameChanged(const QString& folderPath) +void RecordPage::onOutputFileNameChanged(const QString& filePath) { - const auto folder = QFileInfo(ui->workingDirectory->url().toLocalFile()); - - if (!folder.exists()) { - setError(tr("Working directory folder cannot be found: %1").arg(folderPath)); - } else if (!folder.isDir()) { - setError(tr("Working directory folder is not valid: %1").arg(folderPath)); - } else if (!folder.isWritable()) { - setError(tr("Working directory folder is not writable: %1").arg(folderPath)); - } else { - setError({}); - } - updateStartRecordingButtonState(ui); -} - -void RecordPage::onViewPerfRecordResultsButtonClicked() -{ - emit openFile(m_resultsFile); -} - -void RecordPage::onOutputFileNameChanged(const QString& /*filePath*/) -{ - const auto perfDataExtension = QStringLiteral(".data"); - - const auto file = QFileInfo(ui->outputFile->url().toLocalFile()); - const auto folder = QFileInfo(file.absolutePath()); - - if (!folder.exists()) { - setError(tr("Output file directory folder cannot be found: %1").arg(folder.path())); - } else if (!folder.isDir()) { - setError(tr("Output file directory folder is not valid: %1").arg(folder.path())); - } else if (!folder.isWritable()) { - setError(tr("Output file directory folder is not writable: %1").arg(folder.path())); - } else if (!file.absoluteFilePath().endsWith(perfDataExtension)) { - setError(tr("Output file must end with %1").arg(perfDataExtension)); - } else { - setError({}); - } - updateStartRecordingButtonState(ui); + m_recordHost->setOutputFileName(filePath); } void RecordPage::onOutputFileNameSelected(const QString& filePath) { - const auto perfDataExtension = QStringLiteral(".data"); - - if (!filePath.endsWith(perfDataExtension)) { - ui->outputFile->setText(filePath + perfDataExtension); - } + m_recordHost->setOutputFileName(filePath); } void RecordPage::onOutputFileUrlChanged(const QUrl& fileUrl) @@ -721,9 +728,9 @@ void RecordPage::updateProcessesFinished() m_processModel->mergeProcesses(m_watcher->result()); - if (selectedRecordType(ui) == AttachToProcess) { + if (selectedRecordType(ui) == RecordType::AttachToProcess) { // only update the state when we show the attach app page - updateStartRecordingButtonState(ui); + updateStartRecordingButtonState(m_recordHost, ui); QTimer::singleShot(1000, this, &RecordPage::updateProcesses); } } @@ -744,44 +751,13 @@ void RecordPage::updateRecordType() setError({}); const auto recordType = selectedRecordType(ui); - ui->launchAppBox->setVisible(recordType == LaunchApplication); - ui->attachAppBox->setVisible(recordType == AttachToProcess); + ui->launchAppBox->setVisible(recordType == RecordType::LaunchApplication); + ui->attachAppBox->setVisible(recordType == RecordType::AttachToProcess); - m_perfOutput->setInputVisible(recordType == LaunchApplication); + m_perfOutput->setInputVisible(recordType == RecordType::LaunchApplication); m_perfOutput->clear(); - ui->elevatePrivilegesCheckBox->setEnabled(PerfRecord::canElevatePrivileges() && recordType != ProfileSystem); - ui->sampleCpuCheckBox->setEnabled(recordType != ProfileSystem && PerfRecord::canSampleCpu()); - if (recordType == ProfileSystem) { - if (PerfRecord::canElevatePrivileges()) { - ui->elevatePrivilegesCheckBox->setChecked(true); - } - ui->sampleCpuCheckBox->setChecked(PerfRecord::canSampleCpu()); - } - if (recordType == AttachToProcess) { + if (recordType == RecordType::AttachToProcess) { updateProcesses(); } - - updateStartRecordingButtonState(ui); -} - -void RecordPage::updateOffCpuCheckboxState() -{ - const bool enableOffCpuProfiling = - (ui->elevatePrivilegesCheckBox->isChecked() || PerfRecord::canProfileOffCpu()) && PerfRecord::canSwitchEvents(); - - if (enableOffCpuProfiling == ui->offCpuCheckBox->isEnabled()) { - return; - } - - ui->offCpuCheckBox->setEnabled(enableOffCpuProfiling); - - // prevent user confusion: don't show the value as checked when the checkbox is disabled - if (!enableOffCpuProfiling) { - // remember the current value - config().writeEntry(QStringLiteral("offCpuProfiling"), ui->offCpuCheckBox->isChecked()); - ui->offCpuCheckBox->setChecked(false); - } else { - ui->offCpuCheckBox->setChecked(config().readEntry(QStringLiteral("offCpuProfiling"), false)); - } } diff --git a/src/recordpage.h b/src/recordpage.h index 0183b78d4..ad2c09e8d 100644 --- a/src/recordpage.h +++ b/src/recordpage.h @@ -13,6 +13,7 @@ #include #include "processlist.h" +#include "recordhost.h" #include @@ -33,15 +34,6 @@ namespace KParts { class ReadOnlyPart; } -enum RecordType -{ - LaunchApplication = 0, - AttachToProcess, - ProfileSystem, - NUM_RECORD_TYPES -}; -Q_DECLARE_METATYPE(RecordType) - class RecordPage : public QWidget { Q_OBJECT @@ -59,12 +51,9 @@ class RecordPage : public QWidget private slots: void onApplicationNameChanged(const QString& filePath); void onStartRecordingButtonClicked(bool checked); - void onWorkingDirectoryNameChanged(const QString& folderPath); - void onViewPerfRecordResultsButtonClicked(); void onOutputFileNameChanged(const QString& filePath); void onOutputFileUrlChanged(const QUrl& fileUrl); void onOutputFileNameSelected(const QString& filePath); - void updateOffCpuCheckboxState(); void updateProcesses(); void updateProcessesFinished(); @@ -77,6 +66,7 @@ private slots: std::unique_ptr ui; + RecordHost* m_recordHost; PerfRecord* m_perfRecord; QString m_resultsFile; QElapsedTimer m_recordTimer; diff --git a/src/settings.cpp b/src/settings.cpp index 2a6dd2b22..9bbb47e63 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -222,6 +222,11 @@ void Settings::loadFromFile() setArch(currentConfig.readEntry("arch", "")); setObjdump(currentConfig.readEntry("objdump", "")); } + + setPerfPath(sharedConfig->group("Perf").readEntry("path", "")); + connect(this, &Settings::perfPathChanged, this, + [sharedConfig](const QString& perfPath) { sharedConfig->group("Perf").writeEntry("path", perfPath); }); + connect(this, &Settings::lastUsedEnvironmentChanged, this, [sharedConfig](const QString& envName) { sharedConfig->group("PerfPaths").writeEntry("lastUsed", envName); }); @@ -234,3 +239,11 @@ void Settings::setSourceCodePaths(const QString& paths) emit sourceCodePathsChanged(m_sourceCodePaths); } } + +void Settings::setPerfPath(const QString& path) +{ + if (m_perfPath != path) { + m_perfPath = path; + emit perfPathChanged(m_perfPath); + } +} diff --git a/src/settings.h b/src/settings.h index c6c791e81..9021cd14c 100644 --- a/src/settings.h +++ b/src/settings.h @@ -144,6 +144,11 @@ class Settings : public QObject return m_sourceCodePaths; } + QString perfPath() const + { + return m_perfPath; + } + void loadFromFile(); signals: @@ -164,6 +169,7 @@ class Settings : public QObject void callgraphChanged(); void lastUsedEnvironmentChanged(const QString& envName); void sourceCodePathsChanged(const QString& paths); + void perfPathChanged(const QString& perfPath); public slots: void setPrettifySymbols(bool prettifySymbols); @@ -185,6 +191,7 @@ public slots: void setCostAggregation(Settings::CostAggregation costAggregation); void setLastUsedEnvironment(const QString& envName); void setSourceCodePaths(const QString& paths); + void setPerfPath(const QString& path); private: Settings() = default; @@ -214,4 +221,6 @@ public slots: int m_callgraphChildDepth = 2; QColor m_callgraphActiveColor; QColor m_callgraphColor; + + QString m_perfPath; }; diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index ba22ee7d0..426cb41e2 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -11,6 +11,7 @@ #include "ui_callgraphsettingspage.h" #include "ui_debuginfodpage.h" #include "ui_flamegraphsettingspage.h" +#include "ui_perfsettingspage.h" #include "ui_sourcepathsettings.h" #include "ui_unwindsettingspage.h" @@ -49,10 +50,17 @@ QPushButton* setupMultiPath(KEditListWidget* listWidget, QLabel* buddy, QWidget* QWidget::setTabOrder(listWidget->upButton(), listWidget->downButton()); return listWidget->downButton(); } + +QIcon icon() +{ + static const auto icon = QIcon::fromTheme(QStringLiteral("preferences-system-windows-behavior")); + return icon; +} } SettingsDialog::SettingsDialog(QWidget* parent) : KPageDialog(parent) + , perfPage(new Ui::PerfSettingsPage) , unwindPage(new Ui::UnwindSettingsPage) , flamegraphPage(new Ui::FlamegraphSettingsPage) , debuginfodPage(new Ui::DebuginfodPage) @@ -61,6 +69,7 @@ SettingsDialog::SettingsDialog(QWidget* parent) , callgraphPage(new Ui::CallgraphSettingsPage) #endif { + addPerfSettingsPage(); addPathSettingsPage(); addFlamegraphPage(); addDebuginfodPage(); @@ -142,12 +151,28 @@ QString SettingsDialog::objdump() const return unwindPage->lineEditObjdump->text(); } +void SettingsDialog::addPerfSettingsPage() +{ + auto page = new QWidget(this); + auto item = addPage(page, tr("Perf")); + item->setIcon(icon()); + + perfPage->setupUi(page); + + connect(this, &KPageDialog::accepted, this, [this]() { + auto settings = Settings::instance(); + settings->setPerfPath(perfPage->perfPathEdit->url().toLocalFile()); + }); + + perfPage->perfPathEdit->setUrl(QUrl::fromLocalFile(Settings::instance()->perfPath())); +} + void SettingsDialog::addPathSettingsPage() { auto page = new QWidget(this); auto item = addPage(page, tr("Unwinding")); item->setHeader(tr("Unwind Options")); - item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-windows-behavior"))); + item->setIcon(icon()); unwindPage->setupUi(page); @@ -220,7 +245,7 @@ void SettingsDialog::addFlamegraphPage() auto page = new QWidget(this); auto item = addPage(page, tr("Flamegraph")); item->setHeader(tr("Flamegraph Options")); - item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-windows-behavior"))); + item->setIcon(icon()); flamegraphPage->setupUi(page); @@ -247,7 +272,7 @@ void SettingsDialog::addDebuginfodPage() auto page = new QWidget(this); auto item = addPage(page, tr("debuginfod")); item->setHeader(tr("debuginfod Urls")); - item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-windows-behavior"))); + item->setIcon(icon()); debuginfodPage->setupUi(page); @@ -267,7 +292,7 @@ void SettingsDialog::addCallgraphPage() auto page = new QWidget(this); auto item = addPage(page, tr("Callgraph")); item->setHeader(tr("Callgraph Settings")); - item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-windows-behavior"))); + item->setIcon(icon()); callgraphPage->setupUi(page); diff --git a/src/settingsdialog.h b/src/settingsdialog.h index c2bed498d..36ad2a22f 100644 --- a/src/settingsdialog.h +++ b/src/settingsdialog.h @@ -18,6 +18,7 @@ class FlamegraphSettingsPage; class DebuginfodPage; class CallgraphSettingsPage; class SourcePathSettingsPage; +class PerfSettingsPage; } class MultiConfigWidget; @@ -43,12 +44,14 @@ class SettingsDialog : public KPageDialog void keyPressEvent(QKeyEvent* event) override; private: + void addPerfSettingsPage(); void addPathSettingsPage(); void addFlamegraphPage(); void addDebuginfodPage(); void addCallgraphPage(); void addSourcePathPage(); + std::unique_ptr perfPage; std::unique_ptr unwindPage; std::unique_ptr flamegraphPage; std::unique_ptr debuginfodPage; diff --git a/tests/integrationtests/CMakeLists.txt b/tests/integrationtests/CMakeLists.txt index 6de21fec3..48e751f9c 100644 --- a/tests/integrationtests/CMakeLists.txt +++ b/tests/integrationtests/CMakeLists.txt @@ -6,6 +6,7 @@ ecm_add_test( ../../src/initiallystoppedprocess.cpp ../../src/perfcontrolfifowrapper.cpp ../../src/perfrecord.cpp + ../../src/recordhost.cpp ../../src/settings.cpp ../../src/util.cpp ../../src/errnoutil.cpp diff --git a/tests/integrationtests/tst_perfparser.cpp b/tests/integrationtests/tst_perfparser.cpp index a9460b8bf..73f65fdf8 100644 --- a/tests/integrationtests/tst_perfparser.cpp +++ b/tests/integrationtests/tst_perfparser.cpp @@ -18,6 +18,7 @@ #include "data.h" #include "perfparser.h" #include "perfrecord.h" +#include "recordhost.h" #include "unistd.h" #include "util.h" @@ -157,9 +158,18 @@ class TestPerfParser : public QObject private slots: void initTestCase() { - if (!PerfRecord::isPerfInstalled()) { + RecordHost host; + QSignalSpy capabilitiesSpy(&host, &RecordHost::perfCapabilitiesChanged); + QSignalSpy installedSpy(&host, &RecordHost::isPerfInstalledChanged); + QVERIFY(installedSpy.wait()); + if (!host.isPerfInstalled()) { QSKIP("perf is not available, cannot run integration tests."); } + + if (capabilitiesSpy.count() == 0) { + QVERIFY(capabilitiesSpy.wait()); + } + m_capabilities = host.perfCapabilities(); } void init() @@ -202,9 +212,9 @@ private slots: QTest::addColumn("otherOptions"); QTest::addRow("normal") << QStringList(); - if (PerfRecord::canUseAio()) + if (m_capabilities.canUseAio) QTest::addRow("aio") << QStringList(QStringLiteral("--aio")); - if (PerfRecord::canCompress()) + if (m_capabilities.canCompress) QTest::addRow("zstd") << QStringList(QStringLiteral("-z")); } @@ -414,7 +424,8 @@ private slots: QTemporaryFile tempFile; tempFile.open(); - PerfRecord perf; + RecordHost host; + PerfRecord perf(&host); QSignalSpy recordingFinishedSpy(&perf, &PerfRecord::recordingFinished); QSignalSpy recordingFailedSpy(&perf, &PerfRecord::recordingFailed); @@ -493,7 +504,7 @@ private slots: void testOffCpu() { - if (!PerfRecord::canProfileOffCpu()) { + if (!m_capabilities.canProfileOffCpu) { QSKIP("cannot access sched_switch trace points. execute the following to run this test:\n" " sudo mount -o remount,mode=755 /sys/kernel/debug{,/tracing} with mode=755"); } @@ -543,7 +554,7 @@ private slots: QSKIP("no sleep command available"); } - if (!PerfRecord::canProfileOffCpu()) { + if (!m_capabilities.canProfileOffCpu) { QSKIP("cannot access sched_switch trace points. execute the following to run this test:\n" " sudo mount -o remount,mode=755 /sys/kernel/debug{,/tracing} with mode=755"); } @@ -573,7 +584,7 @@ private slots: { QStringList perfOptions = {QStringLiteral("--call-graph"), QStringLiteral("dwarf"), QStringLiteral("--sample-cpu"), QStringLiteral("-e"), QStringLiteral("cycles")}; - if (PerfRecord::canProfileOffCpu()) { + if (m_capabilities.canProfileOffCpu) { perfOptions += PerfRecord::offCpuProfilingOptions(); } @@ -593,7 +604,7 @@ private slots: QCOMPARE(m_eventData.threads.size(), numThreads + 1); QCOMPARE(m_eventData.cpus.size(), numThreads); - if (PerfRecord::canProfileOffCpu()) { + if (m_capabilities.canProfileOffCpu) { QCOMPARE(m_bottomUpData.costs.numTypes(), 3); QCOMPARE(m_bottomUpData.costs.typeName(0), QStringLiteral("cycles")); QCOMPARE(m_bottomUpData.costs.typeName(1), QStringLiteral("sched:sched_switch")); @@ -714,11 +725,13 @@ private slots: QString m_cpuArchitecture; QString m_linuxKernelVersion; QString m_machineHostName; + RecordHost::PerfCapabilities m_capabilities; void perfRecord(const QStringList& perfOptions, const QString& exePath, const QStringList& exeOptions, const QString& fileName) { - PerfRecord perf(this); + RecordHost host; + PerfRecord perf(&host); QSignalSpy recordingFinishedSpy(&perf, &PerfRecord::recordingFinished); QSignalSpy recordingFailedSpy(&perf, &PerfRecord::recordingFailed); diff --git a/tests/modeltests/CMakeLists.txt b/tests/modeltests/CMakeLists.txt index 3934ae30b..e2b9bbb1a 100644 --- a/tests/modeltests/CMakeLists.txt +++ b/tests/modeltests/CMakeLists.txt @@ -55,6 +55,7 @@ ecm_add_test( ../../src/initiallystoppedprocess.cpp ../../src/perfcontrolfifowrapper.cpp ../../src/perfrecord.cpp + ../../src/recordhost.cpp ../../src/callgraphgenerator.cpp ../../src/errnoutil.cpp LINK_LIBRARIES diff --git a/tests/modeltests/tst_callgraphgenerator.cpp b/tests/modeltests/tst_callgraphgenerator.cpp index 6733923ac..008302b5e 100644 --- a/tests/modeltests/tst_callgraphgenerator.cpp +++ b/tests/modeltests/tst_callgraphgenerator.cpp @@ -8,6 +8,7 @@ #include "../../src/parsers/perf/perfparser.h" #include "../../src/perfrecord.h" +#include "../../src/recordhost.h" #include "../testutils.h" #include "data.h" @@ -99,7 +100,8 @@ private slots: void perfRecord(const QStringList& perfOptions, const QString& exePath, const QStringList& exeOptions, const QString& fileName) { - PerfRecord perf(this); + RecordHost host; + PerfRecord perf(&host); QSignalSpy recordingFinishedSpy(&perf, &PerfRecord::recordingFinished); QSignalSpy recordingFailedSpy(&perf, &PerfRecord::recordingFailed); From f847afe4b40fc4b4a213484a86b3cf15f6c46914 Mon Sep 17 00:00:00 2001 From: Lieven Hey Date: Fri, 1 Sep 2023 13:45:58 +0200 Subject: [PATCH 4/7] fix tst_callgraphgenerator calling perf this doesn't work inside a docker container so I added a perfparser dump of the data to process --- tests/modeltests/CMakeLists.txt | 4 -- tests/modeltests/callgraph.perfparser | Bin 0 -> 7917 bytes tests/modeltests/tst_callgraphgenerator.cpp | 52 ++++---------------- 3 files changed, 10 insertions(+), 46 deletions(-) create mode 100644 tests/modeltests/callgraph.perfparser diff --git a/tests/modeltests/CMakeLists.txt b/tests/modeltests/CMakeLists.txt index e2b9bbb1a..41cbbe258 100644 --- a/tests/modeltests/CMakeLists.txt +++ b/tests/modeltests/CMakeLists.txt @@ -52,10 +52,6 @@ set_target_properties( ecm_add_test( tst_callgraphgenerator.cpp ../../src/parsers/perf/perfparser.cpp - ../../src/initiallystoppedprocess.cpp - ../../src/perfcontrolfifowrapper.cpp - ../../src/perfrecord.cpp - ../../src/recordhost.cpp ../../src/callgraphgenerator.cpp ../../src/errnoutil.cpp LINK_LIBRARIES diff --git a/tests/modeltests/callgraph.perfparser b/tests/modeltests/callgraph.perfparser new file mode 100644 index 0000000000000000000000000000000000000000..dad21e455990b3ab640159e939147ae83bbcd780 GIT binary patch literal 7917 zcmb_heQZ51X{|MfffRZ?KoeMun}NDfR&}A6|Gv}#eRNCEF9bQM+imT z_yeu&G^tENNL8gRnkdzTx^7k14bZkUYlk$L%2w8WRMl7qqbsHB_7~bFvET2!cb`8J zg4sCIeed3L&;6Zq?mhS1`|K}2v}woYhacIoY5jI>mZoX*P^O@eW*VuSZY0xY(AaOp z%}i%5xDf2Sq|~X~w*fE{99rN;sgC9b2lsbw z>CP#|LIbxnVS>w+C7StNdhz{LNrYX%DnW#WTFRU2ks% ztJW?{L(SO%U$>d?ZB1m&c>RtQzAwP}`bV~}@Wod7L(T0?4?(mJ61iBy6rslU#!y>p zV+;72>$7+3@uEl5gSkvP7>OlIwZr$qOKMa1nl z(5wnI386aE?@uKCkz6bujYToY7%;)D4+OJjCYzC`--yS|1R8`C#C#31l{Y|m*FE{K zZ!N62(((4y$GYzsdhKgZ{LWl>H~8Cv1Ia-%2#g^!5&YuD^&7y*W>U#)P}KUP=1`g1 zlqNlpN&Gx_;^f%2LsiMV;G5`Md)<7m1XjX%tK6ANd z*z-)S_Q#5B-O06w Q9D_Yi3`H}^-AbzKwHJQmdUyS^r}pjoeb1HO2Cgsp#;1^) znO6~u7{qM{m$6dW)?(P<|);Z(I(QLihIG2%&MFWPeocmyVv zY}Hh9pP5F3yI{HvY($d7Iz8on^5jzKWQ4{svPBQKH0pCv?lLmjaMqMGfhMD>2u1cB znA0L@GrTvIg!5?4LMilu)^K%-hRPCvIW3zG8>Sk1A33(t)ZTuy?m@ZfoDsysvR8TG zMXN{;BVaa)GKkZ|Ve?TcV;!>C%y|`q8BWv;l%nwrjOnFJdSop(DMM>vh65bi+m@QDb02MA3OWi~XYnk22$eKL0t$+kq9Q4(L z*LMB*J;`D;$|gIjNsF*td(CE2KUIt)kAg-1Ane%7<-oeihQ%oh*xfb%CUU22vqUkI&!y!ZGQkW-uH}1Pm4Q zFmUJeGcy%O{9wp-Sn2#iC~b z%&B$~;b!kzE(TAQjn#ONpqugbt;u z5p|9x3dnm&*@cl5_N@a~2?tSE8ywYqaB*}cgHstry~4!X4iT+(9U3GbinE9a1<}(3 zrqa`lI>Un~pxPdFA>g&D8nC^o^ZS9yYq4ib%POOI@eTO2uF5wb3DwtAY7 zrx^+sHVN#($zXX)qbCe_uddT{9T&xPRxg|_xk>{&9z#tWwh3FPd$IziP z@ym^75e4Kurp&@_72)rLtAx3Oi_BuqU^FslDtQ#DSfr#d@m2}NODIy2QTSXDJ+Grf z>ET6|MiB+%eO(!a4iP>Jt`g=E!tbDJ6p;7qPCSbPck%k8(r4v!1}@#^X6MI}(st`0 z9*=zgvrhqm#FOHGM^+Bvek~5ZC}>vlkTsj5@$ou|gJc}J#bM3P9SxJEE0VL;UE#6GN3~uh;;?rV0 zQde6Cr9ez4NE0G7w?AdrzrgkCnJj*+ z8;s=obQ_xqW#^s2j>PbDb<)xh$QsT^9sI=&ul2w>sG4hkY<)oW&ZVO6fc$F0Nn=8u zjfv>&w2?YHWv%L0sMX^1QHs-R=leRrM%(ognJO+YLKJ@!Or`i5>P%Wh5mLg~s*doz zmX$85;9jC?9M%VpcH>1WnV{B+m632J#tX1Lt)DXn!s$kzbg`=A7fwuzu-E$5c6O$6 zk<9*tk#`4r1@6}F)K6j|QH0biLAXp0Cn}lkaTc`sFIm@zDy{QFyCfp=T_i#c7BDg z{7eE>gbQY}rr|u8q!&<}z-SQtjg8#+O8+ycGieb8SF6UQDlm`&F+c8K`bzh_6TXmV zAktXgDel&FV>qn$#S-Co>?!Bh38qr%#@o&^6{mb?fLvGbaI%cw)_A^DBZP>8i8^&& znHLb@mFFfkk9uhyBY(E8N0WMgJQ)ec^=LMk&gkLXu!_j8BWcKjO>Qp-HdHojv%tE~o8`c& z|F{ZHk79G3b#1X=e|>l|Se~?W+=o(W4#%AD1&kJEfu4#t4}(eap*VV4Z9T7+>VXhZ z5I=9es#c~=gk4*9mI_N-q^$(Vld{cb7T91**smFnqf&gq+4{7#rL6B2W)-T&7Ss^u l8lp%;aA;Ub4b!f{b`2)o #include "../../src/parsers/perf/perfparser.h" -#include "../../src/perfrecord.h" -#include "../../src/recordhost.h" #include "../testutils.h" #include "data.h" @@ -16,22 +14,11 @@ class TestCallgraphGenerator : public QObject { Q_OBJECT private slots: - void initTestCase() - { - const QStringList perfOptions = {QStringLiteral("--call-graph"), QStringLiteral("dwarf")}; - QStringList exeOptions; - - const QString exePath = findExe(QStringLiteral("callgraph")); - m_file.open(); - - perfRecord(perfOptions, exePath, exeOptions, m_file.fileName()); - } - void testParent() { - auto results = callerCalleeResults(m_file.fileName()); + auto results = callerCalleeResults(s_fileName); - QVERIFY(callerCalleeResults(m_file.fileName()).entries.size() > 0); + QVERIFY(!callerCalleeResults(s_fileName).entries.empty()); auto key = Data::Symbol(); for (auto it = results.entries.cbegin(); it != results.entries.cend(); it++) { @@ -51,14 +38,15 @@ private slots: int parent1Pos = test.indexOf(QLatin1String("parent1")); QVERIFY(parent3Pos < parent2Pos); + QVERIFY(parent2Pos < parent1Pos); } void testChild() { - auto results = callerCalleeResults(m_file.fileName()); + auto results = callerCalleeResults(s_fileName); - QVERIFY(callerCalleeResults(m_file.fileName()).entries.size() > 0); + QVERIFY(!callerCalleeResults(s_fileName).entries.empty()); auto key = Data::Symbol(); for (auto it = results.entries.cbegin(); it != results.entries.cend(); it++) { @@ -82,10 +70,11 @@ private slots: private: Data::CallerCalleeResults callerCalleeResults(const QString& filename) { - qputenv("HOTSPOT_PERFPARSER", - QCoreApplication::applicationDirPath().toUtf8() + QByteArrayLiteral("/perfparser")); - PerfParser parser(this); + const QByteArray perfparserPath = + QCoreApplication::applicationDirPath().toUtf8() + QByteArrayLiteral("/perfparser"); + qputenv("HOTSPOT_PERFPARSER", perfparserPath); + PerfParser parser(this); QSignalSpy parsingFinishedSpy(&parser, &PerfParser::parsingFinished); QSignalSpy parsingFailedSpy(&parser, &PerfParser::parsingFailed); @@ -97,28 +86,7 @@ private slots: return parser.callerCalleeResults(); } - void perfRecord(const QStringList& perfOptions, const QString& exePath, const QStringList& exeOptions, - const QString& fileName) - { - RecordHost host; - PerfRecord perf(&host); - QSignalSpy recordingFinishedSpy(&perf, &PerfRecord::recordingFinished); - QSignalSpy recordingFailedSpy(&perf, &PerfRecord::recordingFailed); - - // always add `-c 1000000`, as perf's frequency mode is too unreliable for testing purposes - perf.record( - perfOptions - + QStringList {QStringLiteral("-c"), QStringLiteral("1000000"), QStringLiteral("--no-buildid-cache")}, - fileName, false, exePath, exeOptions); - - VERIFY_OR_THROW(recordingFinishedSpy.wait(10000)); - - COMPARE_OR_THROW(recordingFailedSpy.count(), 0); - COMPARE_OR_THROW(recordingFinishedSpy.count(), 1); - COMPARE_OR_THROW(QFileInfo::exists(fileName), true); - } - - QTemporaryFile m_file; + const QString s_fileName = QFINDTESTDATA("callgraph.perfparser"); }; QTEST_GUILESS_MAIN(TestCallgraphGenerator) From 1f7c89eae91a4c204ceaf3049d3ebee7428b917a Mon Sep 17 00:00:00 2001 From: Lieven Hey Date: Fri, 1 Sep 2023 14:56:59 +0200 Subject: [PATCH 5/7] remove unused include --- src/recordpage.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/recordpage.cpp b/src/recordpage.cpp index 7d9a4a565..d3068635f 100644 --- a/src/recordpage.cpp +++ b/src/recordpage.cpp @@ -40,8 +40,6 @@ #include #include -#include "hotspot-config.h" - #include "multiconfigwidget.h" #include "perfoutputwidgetkonsole.h" #include "perfoutputwidgettext.h" From 2dbbf1d1daaa610e5ae3518721415b4affdd23aa Mon Sep 17 00:00:00 2001 From: Milian Wolff Date: Fri, 1 Sep 2023 15:52:42 +0200 Subject: [PATCH 6/7] Fix race condition when setting thread names in perfparser We must not write to any data member from the background thread. Instead, use a signal to serialize the write operation into the context of the main thread. This fixes the data race and potential issues when running this code. --- src/models/data.h | 8 ++++++++ src/parsers/perf/perfparser.cpp | 27 +++++++++++++++------------ src/parsers/perf/perfparser.h | 3 ++- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/models/data.h b/src/models/data.h index d4935e9f8..e52b9336b 100644 --- a/src/models/data.h +++ b/src/models/data.h @@ -821,6 +821,11 @@ struct Summary QStringList errors; }; +struct ThreadNames +{ + QHash> names; +}; + struct EventResults { QVector threads; @@ -951,6 +956,9 @@ Q_DECLARE_TYPEINFO(Data::Summary, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(Data::CostSummary) Q_DECLARE_TYPEINFO(Data::CostSummary, Q_MOVABLE_TYPE); +Q_DECLARE_METATYPE(Data::ThreadNames) +Q_DECLARE_TYPEINFO(Data::ThreadNames, Q_MOVABLE_TYPE); + Q_DECLARE_METATYPE(Data::EventResults) Q_DECLARE_TYPEINFO(Data::EventResults, Q_MOVABLE_TYPE); diff --git a/src/parsers/perf/perfparser.cpp b/src/parsers/perf/perfparser.cpp index 5d125924c..50f9a14f3 100644 --- a/src/parsers/perf/perfparser.cpp +++ b/src/parsers/perf/perfparser.cpp @@ -555,20 +555,20 @@ void addCallerCalleeEvent(const Data::Symbol& symbol, const Data::Location& loca template void addBottomUpResult(Data::BottomUpResults* bottomUpResult, Settings::CostAggregation costAggregation, - const QHash>& commands, int type, quint64 cost, qint32 pid, - qint32 tid, quint32 cpu, const QVector& frames, const FrameCallback& frameCallback) + const Data::ThreadNames& commands, int type, quint64 cost, qint32 pid, qint32 tid, quint32 cpu, + const QVector& frames, const FrameCallback& frameCallback) { switch (costAggregation) { case Settings::CostAggregation::BySymbol: bottomUpResult->addEvent(type, cost, frames, frameCallback); break; case Settings::CostAggregation::ByThread: { - auto thread = commands.value(pid).value(tid); + auto thread = commands.names.value(pid).value(tid); bottomUpResult->addEvent(thread.isEmpty() ? QString::number(tid) : thread, type, cost, frames, frameCallback); break; } case Settings::CostAggregation::ByProcess: { - auto process = commands.value(pid).value(pid); + auto process = commands.names.value(pid).value(pid); bottomUpResult->addEvent(process.isEmpty() ? QString::number(pid) : process, type, cost, frames, frameCallback); break; } @@ -747,8 +747,8 @@ class PerfParserPrivate : public QObject auto thread = addThread(threadStart); thread->time.start = threadStart.time; if (threadStart.ppid != threadStart.pid) { - const auto parentComm = commands.value(threadStart.ppid).value(threadStart.ppid); - commands[threadStart.pid][threadStart.pid] = parentComm; + const auto parentComm = commands.names.value(threadStart.ppid).value(threadStart.ppid); + commands.names[threadStart.pid][threadStart.pid] = parentComm; thread->name = parentComm; } break; @@ -960,9 +960,9 @@ class PerfParserPrivate : public QObject // we started the application, otherwise we override the start time when // we encounter a ThreadStart event thread.time.start = applicationTime.start; - thread.name = commands.value(thread.pid).value(thread.tid); + thread.name = commands.names.value(thread.pid).value(thread.tid); if (thread.name.isEmpty() && thread.pid != thread.tid) - thread.name = commands.value(thread.pid).value(thread.pid); + thread.name = commands.names.value(thread.pid).value(thread.pid); eventResult.threads.push_back(thread); return &eventResult.threads.last(); } @@ -984,7 +984,7 @@ class PerfParserPrivate : public QObject thread->name = comm; } // and remember the command, maybe a future ThreadStart event references it - commands[command.pid][command.tid] = comm; + commands.names[command.pid][command.tid] = comm; } void addLocation(const LocationDefinition& location) @@ -1124,7 +1124,7 @@ class PerfParserPrivate : public QObject void addSampleToBottomUp(const Sample& sample, SampleCost sampleCost) { if (perfScriptOutput) { - *perfScriptOutput << commands.value(sample.pid).value(sample.pid) << '\t' << sample.pid << '\t' + *perfScriptOutput << commands.names.value(sample.pid).value(sample.pid) << '\t' << sample.pid << '\t' << sample.time / 1000000000 << '.' << qSetFieldWidth(9) << qSetPadChar(QLatin1Char('0')) << sample.time % 1000000000 << qSetFieldWidth(0) << ":\t" << sampleCost.cost << ' ' << strings.value(attributes.value(sampleCost.attributeId).name.id) << '\n'; @@ -1376,7 +1376,7 @@ class PerfParserPrivate : public QObject Data::EventResults eventResult; Data::TracepointResults tracepointResult; Data::FrequencyResults frequencyResult; - QHash> commands; + Data::ThreadNames commands; std::unique_ptr perfScriptOutput; QHash numSymbolsByModule; QSet encounteredErrors; @@ -1420,6 +1420,7 @@ PerfParser::PerfParser(QObject* parent) qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); // set data via signal/slot connection to ensure we don't introduce a data race connect(this, &PerfParser::bottomUpDataAvailable, this, [this](const Data::BottomUpResults& data) { @@ -1447,6 +1448,8 @@ PerfParser::PerfParser(QObject* parent) m_tracepointResults = data; } }); + connect(this, &PerfParser::threadNamesAvailable, this, + [this](const Data::ThreadNames& threadNames) { m_threadNames = threadNames; }); connect(this, &PerfParser::parsingStarted, this, [this]() { m_isParsing = true; m_stopRequested = false; @@ -1562,7 +1565,7 @@ void PerfParser::startParseFile(const QString& path) emit frequencyDataAvailable(d.frequencyResult); emit parsingFinished(); - m_threadNames = d.commands; + emit threadNamesAvailable(d.commands); if (d.m_numSamplesWithMoreThanOneFrame == 0) { emit parserWarning(tr("Samples contained no call stack frames. Consider passing --call-graph " diff --git a/src/parsers/perf/perfparser.h b/src/parsers/perf/perfparser.h index 19b1475bb..cd49bd606 100644 --- a/src/parsers/perf/perfparser.h +++ b/src/parsers/perf/perfparser.h @@ -58,6 +58,7 @@ class PerfParser : public QObject void tracepointDataAvailable(const Data::TracepointResults& data); void frequencyDataAvailable(const Data::FrequencyResults& data); void eventsAvailable(const Data::EventResults& events); + void threadNamesAvailable(const Data::ThreadNames& threadNames); void parsingFinished(); void parsingFailed(const QString& errorMessage); void exportFailed(const QString& errorMessage); @@ -86,5 +87,5 @@ class PerfParser : public QObject std::atomic m_stopRequested; std::atomic m_costAggregationChanged; std::unique_ptr m_decompressed; - QHash> m_threadNames; + Data::ThreadNames m_threadNames; }; From 012183024a84045829fbed83b1e94b5cf94062ba Mon Sep 17 00:00:00 2001 From: Milian Wolff Date: Fri, 1 Sep 2023 15:53:47 +0200 Subject: [PATCH 7/7] Emit parsing finished at the very end of the background thread Otherwise, we might get unlucky and crash when our tests await this signal and then destroy the parser object as a result, leading to crashes. --- src/parsers/perf/perfparser.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parsers/perf/perfparser.cpp b/src/parsers/perf/perfparser.cpp index 50f9a14f3..7b9d3a8b8 100644 --- a/src/parsers/perf/perfparser.cpp +++ b/src/parsers/perf/perfparser.cpp @@ -1563,14 +1563,14 @@ void PerfParser::startParseFile(const QString& path) emit tracepointDataAvailable(d.tracepointResult); emit eventsAvailable(d.eventResult); emit frequencyDataAvailable(d.frequencyResult); - emit parsingFinished(); - emit threadNamesAvailable(d.commands); if (d.m_numSamplesWithMoreThanOneFrame == 0) { emit parserWarning(tr("Samples contained no call stack frames. Consider passing --call-graph " "dwarf to perf record.")); } + + emit parsingFinished(); }; if (path.endsWith(QLatin1String(".perfparser"))) {