diff --git a/CMakeLists.txt b/CMakeLists.txt index c91a797f..4837ba1f 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/CMakeLists.txt b/src/CMakeLists.txt index 7b97c854..1df9a586 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 00000000..8d17a468 --- /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/models/data.h b/src/models/data.h index d4935e9f..e52b9336 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/multiconfigwidget.cpp b/src/multiconfigwidget.cpp index 072c29ad..5225111f 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/parsers/perf/perfparser.cpp b/src/parsers/perf/perfparser.cpp index 5d125924..7b9d3a8b 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; @@ -1560,14 +1563,14 @@ void PerfParser::startParseFile(const QString& path) emit tracepointDataAvailable(d.tracepointResult); emit eventsAvailable(d.eventResult); 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 " "dwarf to perf record.")); } + + emit parsingFinished(); }; if (path.endsWith(QLatin1String(".perfparser"))) { diff --git a/src/parsers/perf/perfparser.h b/src/parsers/perf/perfparser.h index 19b1475b..cd49bd60 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; }; diff --git a/src/perfrecord.cpp b/src/perfrecord.cpp index a152b636..e3adc589 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 b5feeece..ebcc4ce2 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 00000000..bfea262f --- /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 00000000..df5b53f5 --- /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 00000000..5023d1cb --- /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 db4e4f0e..d3068635 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,13 +34,12 @@ #include #include #include +#include #include #include #include -#include "hotspot-config.h" - #include "multiconfigwidget.h" #include "perfoutputwidgetkonsole.h" #include "perfoutputwidgettext.h" @@ -73,9 +73,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 +84,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 +108,8 @@ KConfigGroup config() KConfigGroup applicationConfig(const QString& application) { + if (application.isEmpty()) + return {}; return config().group(QLatin1String("Application ") + KShell::tildeExpand(application)); } @@ -160,7 +162,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,19 +180,77 @@ RecordPage::RecordPage(QWidget* parent) ui->setupUi(contents); } - auto completion = ui->applicationName->completionObject(); + 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); - // 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")); + 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)")); @@ -234,26 +295,26 @@ 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, - &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())); @@ -362,33 +423,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; + + 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(ui->elevatePrivilegesCheckBox, &QCheckBox::toggled, this, &RecordPage::updateOffCpuCheckboxState); + 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); @@ -396,8 +487,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 @@ -420,37 +509,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); @@ -481,6 +539,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(); @@ -505,7 +565,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"); @@ -516,13 +576,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 @@ -533,11 +593,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); } @@ -572,13 +632,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(); } @@ -587,7 +647,7 @@ void RecordPage::onStartRecordingButtonClicked(bool checked) KShell::splitArgs(appParameters), workingDir); break; } - case AttachToProcess: { + case RecordType::AttachToProcess: { QItemSelectionModel* selectionModel = ui->processesTableView->selectionModel(); QStringList pids; @@ -601,11 +661,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 { @@ -635,78 +695,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); -} - -void RecordPage::onWorkingDirectoryNameChanged(const QString& folderPath) -{ - 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); + m_recordHost->setClientApplication(filePath); } -void RecordPage::onViewPerfRecordResultsButtonClicked() +void RecordPage::onOutputFileNameChanged(const QString& filePath) { - 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) @@ -727,9 +726,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); } } @@ -750,44 +749,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 0183b78d..ad2c09e8 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 2a6dd2b2..9bbb47e6 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 c6c791e8..9021cd14 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 ba22ee7d..426cb41e 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 c2bed498..36ad2a22 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 6de21fec..48e751f9 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 a9460b8b..73f65fdf 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 336535a8..41cbbe25 100644 --- a/tests/modeltests/CMakeLists.txt +++ b/tests/modeltests/CMakeLists.txt @@ -49,28 +49,25 @@ 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/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}" +) diff --git a/tests/modeltests/callgraph.perfparser b/tests/modeltests/callgraph.perfparser new file mode 100644 index 00000000..dad21e45 Binary files /dev/null and b/tests/modeltests/callgraph.perfparser differ diff --git a/tests/modeltests/tst_callgraphgenerator.cpp b/tests/modeltests/tst_callgraphgenerator.cpp index 6733923a..3a16f4b2 100644 --- a/tests/modeltests/tst_callgraphgenerator.cpp +++ b/tests/modeltests/tst_callgraphgenerator.cpp @@ -7,7 +7,6 @@ #include #include "../../src/parsers/perf/perfparser.h" -#include "../../src/perfrecord.h" #include "../testutils.h" #include "data.h" @@ -15,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++) { @@ -50,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++) { @@ -81,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); @@ -96,27 +86,7 @@ private slots: return parser.callerCalleeResults(); } - void perfRecord(const QStringList& perfOptions, const QString& exePath, const QStringList& exeOptions, - const QString& fileName) - { - PerfRecord perf(this); - 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)