| 1 | // SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. |
| 2 | // |
| 3 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 4 | |
| 5 | #include "perfflamegraphscripts.h" |
| 6 | #include "config.h" |
| 7 | |
| 8 | #include <QCoreApplication> |
| 9 | #include <QStandardPaths> |
| 10 | #include <QDir> |
| 11 | #include <QQueue> |
| 12 | #include <QDesktopServices> |
| 13 | |
| 14 | namespace { |
| 15 | |
| 16 | QString flameGraphPath() |
| 17 | { |
| 18 | static QString flameGraph{"FlameGraph" }; |
| 19 | if (CustomPaths::installed()) |
| 20 | return CustomPaths::global(CustomPaths::Scripts) + QDir::separator() + flameGraph; |
| 21 | else { |
| 22 | return QString(CMAKE_SOURCE_DIR) + QDir::separator() + "3rdparty" + QDir::separator() + flameGraph; |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | QString cachePath() |
| 27 | { |
| 28 | QDir dir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); |
| 29 | QString appName = QCoreApplication::applicationName(); |
| 30 | |
| 31 | if (!dir.exists()) { |
| 32 | dir.cdUp(); |
| 33 | dir.mkdir(appName); |
| 34 | dir.cd(appName); |
| 35 | } |
| 36 | |
| 37 | if (!dir.cd(PROJECT_NAME)) { |
| 38 | dir.mkdir(PROJECT_NAME); |
| 39 | dir.cd(PROJECT_NAME); |
| 40 | } |
| 41 | |
| 42 | return dir.path(); |
| 43 | } |
| 44 | |
| 45 | }; |
| 46 | |
| 47 | PerfRecord::PerfRecord(const QString &perfRecordOutFile) |
| 48 | : pid (0) |
| 49 | , ouFile (perfRecordOutFile) |
| 50 | { |
| 51 | setProgram("perf" ); |
| 52 | } |
| 53 | |
| 54 | PerfScript::PerfScript(const QString &perfRecordOutFileIn, const QString &outFile) |
| 55 | { |
| 56 | setProgram("perf" ); |
| 57 | setArguments({"script" , "-i" , perfRecordOutFileIn}); |
| 58 | setStandardOutputFile(outFile); |
| 59 | QObject::connect(this, &QProcess::errorOccurred, [=](QProcess::ProcessError error){ |
| 60 | qCritical() << error << errorString(); |
| 61 | }); |
| 62 | } |
| 63 | |
| 64 | StackCollapse::StackCollapse(const QString &perfScriptOutFile, const QString &outFile) |
| 65 | { |
| 66 | setWorkingDirectory(flameGraphPath()); |
| 67 | setProgram("perl" ); |
| 68 | setArguments({"./stackcollapse-perf.pl" , perfScriptOutFile}); |
| 69 | setStandardOutputFile(outFile); |
| 70 | QObject::connect(this, &QProcess::errorOccurred, [=](QProcess::ProcessError error){ |
| 71 | qCritical() << error << errorString(); |
| 72 | }); |
| 73 | } |
| 74 | |
| 75 | FlameGraph::FlameGraph(const QString &stackCollapseOutFile, const QString &outFile) |
| 76 | { |
| 77 | setWorkingDirectory(flameGraphPath()); |
| 78 | setProgram("perl" ); |
| 79 | setArguments({"./flamegraph.pl" , stackCollapseOutFile}); |
| 80 | setStandardOutputFile(outFile); |
| 81 | QObject::connect(this, &QProcess::errorOccurred, [=](QProcess::ProcessError error){ |
| 82 | qCritical() << error << errorString(); |
| 83 | }); |
| 84 | } |
| 85 | |
| 86 | |
| 87 | class FlameGraphGenTaskPrivate |
| 88 | { |
| 89 | friend class FlameGraphGenTask; |
| 90 | PerfRecord *perfRecord{nullptr}; |
| 91 | PerfScript *perfScript{nullptr}; |
| 92 | StackCollapse *stackCollapse{nullptr}; |
| 93 | FlameGraph *flameGraph{nullptr}; |
| 94 | QString perfRecordOutFile{cachePath() + QDir::separator() + "perfRecordOut.data" }; |
| 95 | QString perfScriptOutFile{cachePath() + QDir::separator() + "perfScriptOut.data" }; |
| 96 | QString stackCollapseOutFile{cachePath() + QDir::separator() + "stackCollapseOut.data" }; |
| 97 | QString flameGraphOutFile{cachePath() + QDir::separator() + "flameGraphOut.svg" }; |
| 98 | bool showWebBrowserFlag{false}; |
| 99 | }; |
| 100 | |
| 101 | FlameGraphGenTask::FlameGraphGenTask(QObject *parent) |
| 102 | : QObject (parent) |
| 103 | , d (new FlameGraphGenTaskPrivate) |
| 104 | { |
| 105 | d->perfRecord = new PerfRecord(d->perfRecordOutFile); |
| 106 | d->perfScript = new PerfScript(d->perfRecordOutFile, d->perfScriptOutFile); |
| 107 | d->stackCollapse = new StackCollapse(d->perfScriptOutFile, d->stackCollapseOutFile); |
| 108 | d->flameGraph = new FlameGraph(d->stackCollapseOutFile, d->flameGraphOutFile); |
| 109 | |
| 110 | // perf recode exit |
| 111 | QObject::connect(d->perfRecord, &QProcess::errorOccurred, |
| 112 | this, [=](QProcess::ProcessError pError) |
| 113 | { |
| 114 | QString errorOut = d->perfRecord->readAllStandardError(); |
| 115 | if (pError == QProcess::ProcessError::Crashed |
| 116 | && errorOut.contains("[ perf record: Woken up" ) |
| 117 | && errorOut.contains("times to write data ]\n[ perf record: Captured and wrote" ) |
| 118 | && errorOut.contains("samples) ]\n" )) { |
| 119 | qInfo() << "start perfScript with crashed perfRecord" ; |
| 120 | d->perfScript->start(); |
| 121 | } else { |
| 122 | emit error(d->perfScript->program() |
| 123 | + " " + d->perfScript->arguments().join(" " ) |
| 124 | + ": " + d->perfScript->errorString()); |
| 125 | } |
| 126 | }); |
| 127 | |
| 128 | QObject::connect(d->perfRecord, &QProcess::readAllStandardOutput, |
| 129 | this, [=](){ |
| 130 | qCritical() << "perfRecord output: \n" |
| 131 | << d->perfRecord->readAllStandardOutput(); |
| 132 | }); |
| 133 | QObject::connect(d->perfRecord, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), |
| 134 | this, [=](int exitCode, QProcess::ExitStatus status) |
| 135 | { |
| 136 | qInfo() << "perfRecord exit:" << exitCode << status; |
| 137 | if (exitCode == 0 && d->perfScript) { |
| 138 | qInfo() << "start perfScript" ; |
| 139 | d->perfScript->start(); |
| 140 | } else { |
| 141 | qCritical() << "exit not's 0, this unknow error from perfRecord" |
| 142 | << d->perfScript->errorString(); |
| 143 | emit error(d->perfScript->program() |
| 144 | + " " + d->perfScript->arguments().join(" " ) |
| 145 | + ": " + d->perfScript->errorString()); |
| 146 | } |
| 147 | }); |
| 148 | |
| 149 | QObject::connect(d->perfScript, &QProcess::readAllStandardOutput, |
| 150 | this, [=](){ |
| 151 | qCritical() << "perfScript output: \n" |
| 152 | << d->perfScript->readAllStandardOutput(); |
| 153 | }); |
| 154 | QObject::connect(d->perfScript, &QProcess::readyReadStandardError, |
| 155 | this, [=]() |
| 156 | { |
| 157 | qCritical() << "perfScript error output: \n" |
| 158 | << d->perfScript->readAllStandardError(); |
| 159 | }); |
| 160 | // perf script exit |
| 161 | QObject::connect(d->perfScript, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), |
| 162 | this, [=](int exitCode, QProcess::ExitStatus status) |
| 163 | { |
| 164 | qInfo() << "perfScript exit:" << exitCode << status; |
| 165 | if (exitCode == 0 && d->stackCollapse) { |
| 166 | qInfo() << "start stackCollapse script" ; |
| 167 | d->stackCollapse->start(); |
| 168 | } else { |
| 169 | qCritical() << "exit not's 0, this unknow error from perfScript" |
| 170 | << d->perfScript->errorString(); |
| 171 | emit error(d->perfScript->program() |
| 172 | + " " + d->perfScript->arguments().join(" " ) |
| 173 | + ": " + d->perfScript->errorString()); |
| 174 | } |
| 175 | }); |
| 176 | |
| 177 | QObject::connect(d->stackCollapse, &QProcess::readAllStandardOutput, |
| 178 | this, [=](){ |
| 179 | qCritical() << "stackCollapse output: \n" |
| 180 | << d->stackCollapse->readAllStandardOutput() |
| 181 | << d->stackCollapse->workingDirectory() |
| 182 | << d->stackCollapse->program() |
| 183 | << d->stackCollapse->arguments(); |
| 184 | }); |
| 185 | QObject::connect(d->stackCollapse, &QProcess::readyReadStandardError, |
| 186 | this, [=]() |
| 187 | { |
| 188 | qCritical() << "stackCollapse error output: \n" |
| 189 | << d->stackCollapse->readAllStandardError() |
| 190 | << d->stackCollapse->workingDirectory() |
| 191 | << d->stackCollapse->program() |
| 192 | << d->stackCollapse->arguments(); |
| 193 | }); |
| 194 | // stackCollapse exit |
| 195 | QObject::connect(d->stackCollapse, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), |
| 196 | this, [=](int exitCode, QProcess::ExitStatus status) |
| 197 | { |
| 198 | qInfo() << "stackCollapse exit:" << exitCode << status; |
| 199 | if (exitCode == 0 && d->flameGraph) { |
| 200 | qCritical() << "start flameGraph script" ; |
| 201 | d->flameGraph->start(); |
| 202 | } else { |
| 203 | qCritical() << "exit not's 0, this unknow error from stackCollapse" |
| 204 | << d->flameGraph->errorString(); |
| 205 | emit error(d->stackCollapse->program() |
| 206 | + " " + d->stackCollapse->arguments().join(" " ) |
| 207 | + ": " + d->stackCollapse->errorString()); |
| 208 | } |
| 209 | }); |
| 210 | |
| 211 | QObject::connect(d->flameGraph, &QProcess::readAllStandardOutput, |
| 212 | this, [=](){ |
| 213 | qCritical() << "flameGraph output: \n" |
| 214 | << d->flameGraph->readAllStandardOutput(); |
| 215 | }); |
| 216 | QObject::connect(d->flameGraph, &QProcess::readyReadStandardError, |
| 217 | this, [=]() |
| 218 | { |
| 219 | qCritical() << "flameGraph error output: \n" |
| 220 | << d->flameGraph->readAllStandardError(); |
| 221 | }); |
| 222 | // flameGraph exit |
| 223 | QObject::connect(d->flameGraph, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), |
| 224 | this, [=](int exitCode, QProcess::ExitStatus status) |
| 225 | { |
| 226 | Q_UNUSED(status) |
| 227 | qInfo() << "stackCollapse exit:" << exitCode << status; |
| 228 | if (exitCode == 0) { |
| 229 | if (d->showWebBrowserFlag) { |
| 230 | qCritical() << "show with gnome-www-browser" ; |
| 231 | QString defaultWebCmd = "gnome-www-browser" ; |
| 232 | QProcess::startDetached(defaultWebCmd, {d->flameGraphOutFile}); |
| 233 | emit this->showed(d->flameGraphOutFile); |
| 234 | } |
| 235 | } else { |
| 236 | qCritical() << "exit not's 0, this unknow error from flameGraph" |
| 237 | << d->flameGraph->errorString(); |
| 238 | emit error(d->flameGraph->program() |
| 239 | + " " + d->flameGraph->arguments().join(" " ) |
| 240 | + ": " + d->flameGraph->errorString()); |
| 241 | } |
| 242 | }); |
| 243 | } |
| 244 | |
| 245 | FlameGraphGenTask::~FlameGraphGenTask() |
| 246 | { |
| 247 | if (d->flameGraph) { |
| 248 | if (d->flameGraph->isReadable()) { |
| 249 | d->flameGraph->kill(); |
| 250 | d->flameGraph->waitForFinished(); |
| 251 | } |
| 252 | delete d->flameGraph; |
| 253 | } |
| 254 | |
| 255 | if (d->stackCollapse) { |
| 256 | if (d->stackCollapse->isReadable()) { |
| 257 | d->stackCollapse->kill(); |
| 258 | d->stackCollapse->waitForFinished(); |
| 259 | } |
| 260 | delete d->stackCollapse; |
| 261 | } |
| 262 | |
| 263 | if (d->perfScript) { |
| 264 | if (d->perfScript->isReadable()) { |
| 265 | d->perfScript->kill(); |
| 266 | d->perfScript->waitForFinished(); |
| 267 | } |
| 268 | delete d->perfScript; |
| 269 | } |
| 270 | |
| 271 | if (d->perfRecord) { |
| 272 | if (d->perfRecord->isReadable()) { |
| 273 | d->perfRecord->kill(); |
| 274 | d->perfRecord->waitForFinished(); |
| 275 | } |
| 276 | delete d->perfRecord; |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | void FlameGraphGenTask::showWebBrowser(bool flag) |
| 281 | { |
| 282 | d->showWebBrowserFlag = flag; |
| 283 | } |
| 284 | |
| 285 | void FlameGraphGenTask::start(uint pid) |
| 286 | { |
| 287 | if (d->perfRecord) { |
| 288 | d->perfRecord->setAttachPid(pid); |
| 289 | d->perfRecord->start(); |
| 290 | qInfo() << d->perfRecord->program() << d->perfRecord->arguments(); |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | void FlameGraphGenTask::stop() |
| 295 | { |
| 296 | if (d->perfRecord && d->perfRecord->isReadable()) { |
| 297 | d->perfRecord->terminate(); |
| 298 | d->perfRecord->waitForFinished(); |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | void PerfRecord::setAttachPid(uint pid) |
| 303 | { |
| 304 | this->pid = pid; |
| 305 | setArguments({"record" , "-g" , "-e" , "cpu-clock" , "-p" , QString::number(pid), "-o" , ouFile}); |
| 306 | } |
| 307 | |