1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "qmlprofilerapplication.h"
5#include "constants.h"
6#include <QtCore/QStringList>
7#include <QtCore/QProcess>
8#include <QtCore/QTimer>
9#include <QtCore/QDateTime>
10#include <QtCore/QFileInfo>
11#include <QtCore/QDebug>
12#include <QtCore/QCommandLineParser>
13#include <QtCore/QTemporaryFile>
14#include <QtCore/QLibraryInfo>
15
16#include <iostream>
17
18static const char commandTextC[] =
19 "The following commands are available:\n"
20 "'r', 'record'\n"
21 " Switch recording on or off.\n"
22 "'o [file]', 'output [file]'\n"
23 " Output profiling data to <file>. If no <file>\n"
24 " parameter is given, output to whatever was given\n"
25 " with --output, or standard output.\n"
26 "'c', 'clear'\n"
27 " Clear profiling data recorded so far from memory.\n"
28 "'f [file]', 'flush [file]'\n"
29 " Stop recording if it is running, then output the\n"
30 " data, and finally clear it from memory.\n"
31 "'q', 'quit'\n"
32 " Terminate the target process if started from\n"
33 " qmlprofiler, and qmlprofiler itself.";
34
35static const char *features[] = {
36 "javascript",
37 "memory",
38 "pixmapcache",
39 "scenegraph",
40 "animations",
41 "painting",
42 "compiling",
43 "creating",
44 "binding",
45 "handlingsignal",
46 "inputevents",
47 "debugmessages"
48};
49
50Q_STATIC_ASSERT(sizeof(features) == MaximumProfileFeature * sizeof(char *));
51
52QmlProfilerApplication::QmlProfilerApplication(int &argc, char **argv) :
53 QCoreApplication(argc, argv),
54 m_runMode(LaunchMode),
55 m_process(nullptr),
56 m_hostName(QLatin1String("127.0.0.1")),
57 m_port(0),
58 m_pendingRequest(REQUEST_NONE),
59 m_verbose(false),
60 m_recording(true),
61 m_interactive(false),
62 m_connectionAttempts(0)
63{
64 m_connection.reset(other: new QQmlDebugConnection);
65 m_profilerData.reset(other: new QmlProfilerData);
66 m_qmlProfilerClient.reset(other: new QmlProfilerClient(m_connection.data(), m_profilerData.data()));
67 m_connectTimer.setInterval(1000);
68 connect(sender: &m_connectTimer, signal: &QTimer::timeout, context: this, slot: &QmlProfilerApplication::tryToConnect);
69
70 connect(sender: m_connection.data(), signal: &QQmlDebugConnection::connected,
71 context: this, slot: &QmlProfilerApplication::connected);
72 connect(sender: m_connection.data(), signal: &QQmlDebugConnection::disconnected,
73 context: this, slot: &QmlProfilerApplication::disconnected);
74
75 connect(sender: m_qmlProfilerClient.data(), signal: &QmlProfilerClient::enabledChanged,
76 context: this, slot: &QmlProfilerApplication::traceClientEnabledChanged);
77 connect(sender: m_qmlProfilerClient.data(), signal: &QmlProfilerClient::traceStarted,
78 context: this, slot: &QmlProfilerApplication::notifyTraceStarted);
79 connect(sender: m_qmlProfilerClient.data(), signal: &QmlProfilerClient::error,
80 context: this, slot: &QmlProfilerApplication::logError);
81
82 connect(sender: m_profilerData.data(), signal: &QmlProfilerData::error,
83 context: this, slot: &QmlProfilerApplication::logError);
84 connect(sender: m_profilerData.data(), signal: &QmlProfilerData::dataReady,
85 context: this, slot: &QmlProfilerApplication::traceFinished);
86
87}
88
89QmlProfilerApplication::~QmlProfilerApplication()
90{
91 if (!m_process)
92 return;
93 logStatus(status: "Terminating process ...");
94 m_process->disconnect();
95 m_process->terminate();
96 if (!m_process->waitForFinished(msecs: 1000)) {
97 logStatus(status: "Killing process ...");
98 m_process->kill();
99 }
100 if (isInteractive())
101 std::cerr << std::endl;
102 delete m_process;
103}
104
105void QmlProfilerApplication::parseArguments()
106{
107 setApplicationName(QLatin1String("qmlprofiler"));
108 setApplicationVersion(QLatin1String(qVersion()));
109
110 QCommandLineParser parser;
111 parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
112 parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments);
113
114 parser.setApplicationDescription(QChar::LineFeed + tr(
115 s: "The QML Profiler retrieves QML tracing data from an application. The data\n"
116 "collected can then be visualized in Qt Creator. The application to be profiled\n"
117 "has to enable QML debugging. See the Qt Creator documentation on how to do\n"
118 "this for different Qt versions."));
119
120 QCommandLineOption attach(QStringList() << QLatin1String("a") << QLatin1String("attach"),
121 tr(s: "Attach to an application already running on <hostname>, "
122 "instead of starting it locally."),
123 QLatin1String("hostname"));
124 parser.addOption(commandLineOption: attach);
125
126 QCommandLineOption port(QStringList() << QLatin1String("p") << QLatin1String("port"),
127 tr(s: "Connect to the TCP port <port>. The default is 3768."),
128 QLatin1String("port"), QLatin1String("3768"));
129 parser.addOption(commandLineOption: port);
130
131 QCommandLineOption output(QStringList() << QLatin1String("o") << QLatin1String("output"),
132 tr(s: "Save tracing data in <file>. By default the data is sent to the "
133 "standard output."), QLatin1String("file"), QString());
134 parser.addOption(commandLineOption: output);
135
136 QCommandLineOption record(QLatin1String("record"),
137 tr(s: "If set to 'off', don't immediately start recording data when the "
138 "QML engine starts, but instead either start the recording "
139 "interactively or with the JavaScript console.profile() function. "
140 "By default the recording starts immediately."),
141 QLatin1String("on|off"), QLatin1String("on"));
142 parser.addOption(commandLineOption: record);
143
144 QStringList featureList;
145 for (int i = 0; i < MaximumProfileFeature; ++i)
146 featureList << QLatin1String(features[i]);
147
148 QCommandLineOption include(QLatin1String("include"),
149 tr(s: "Comma-separated list of features to record. By default all "
150 "features supported by the QML engine are recorded. If --include "
151 "is specified, only the given features will be recorded. "
152 "The following features are unserstood by qmlprofiler: %1").arg(
153 a: featureList.join(sep: ", ")),
154 QLatin1String("feature,..."));
155 parser.addOption(commandLineOption: include);
156
157 QCommandLineOption exclude(QLatin1String("exclude"),
158 tr(s: "Comma-separated list of features to exclude when recording. By "
159 "default all features supported by the QML engine are recorded. "
160 "See --include for the features understood by qmlprofiler."),
161 QLatin1String("feature,..."));
162 parser.addOption(commandLineOption: exclude);
163
164 QCommandLineOption interactive(QLatin1String("interactive"),
165 tr(s: "Manually control the recording from the command line. The "
166 "profiler will not terminate itself when the application "
167 "does so in this case.") + QChar::Space + tr(s: commandTextC));
168 parser.addOption(commandLineOption: interactive);
169
170 QCommandLineOption verbose(QStringList() << QLatin1String("verbose"),
171 tr(s: "Print debugging output."));
172 parser.addOption(commandLineOption: verbose);
173
174 parser.addHelpOption();
175 parser.addVersionOption();
176
177 parser.addPositionalArgument(name: QLatin1String("executable"),
178 description: tr(s: "The executable to be started and profiled."),
179 syntax: QLatin1String("[executable]"));
180 parser.addPositionalArgument(name: QLatin1String("parameters"),
181 description: tr(s: "Parameters for the executable to be started."),
182 syntax: QLatin1String("[parameters...]"));
183
184 parser.process(app: *this);
185
186 if (parser.isSet(option: attach)) {
187 m_hostName = parser.value(option: attach);
188 m_runMode = AttachMode;
189 m_port = 3768;
190 }
191
192 if (parser.isSet(option: port)) {
193 bool isNumber;
194 m_port = parser.value(option: port).toUShort(ok: &isNumber);
195 if (!isNumber) {
196 logError(error: tr(s: "'%1' is not a valid port.").arg(a: parser.value(option: port)));
197 parser.showHelp(exitCode: 1);
198 }
199 } else if (m_port == 0) {
200 QTemporaryFile file;
201 if (file.open())
202 m_socketFile = file.fileName();
203 }
204
205 m_outputFile = parser.value(option: output);
206
207 m_recording = (parser.value(option: record) == QLatin1String("on"));
208 m_interactive = parser.isSet(option: interactive);
209
210 quint64 features = std::numeric_limits<quint64>::max();
211 if (parser.isSet(option: include)) {
212 if (parser.isSet(option: exclude)) {
213 logError(error: tr(s: "qmlprofiler can only process either --include or --exclude, not both."));
214 parser.showHelp(exitCode: 4);
215 }
216 features = parseFeatures(featureList, values: parser.value(option: include), exclude: false);
217 }
218
219 if (parser.isSet(option: exclude))
220 features = parseFeatures(featureList, values: parser.value(option: exclude), exclude: true);
221
222 if (features == 0)
223 parser.showHelp(exitCode: 4);
224
225 m_qmlProfilerClient->setRequestedFeatures(features);
226
227 if (parser.isSet(option: verbose))
228 m_verbose = true;
229
230 m_arguments = parser.positionalArguments();
231 if (!m_arguments.isEmpty())
232 m_executablePath = m_arguments.takeFirst();
233
234 if (m_runMode == LaunchMode && m_executablePath.isEmpty()) {
235 logError(error: tr(s: "You have to specify either --attach or an executable to start."));
236 parser.showHelp(exitCode: 2);
237 }
238
239 if (m_runMode == AttachMode && !m_executablePath.isEmpty()) {
240 logError(error: tr(s: "--attach cannot be used when starting an executable."));
241 parser.showHelp(exitCode: 3);
242 }
243}
244
245int QmlProfilerApplication::exec()
246{
247 QTimer::singleShot(interval: 0, receiver: this, slot: &QmlProfilerApplication::run);
248 return QCoreApplication::exec();
249}
250
251bool QmlProfilerApplication::isInteractive() const
252{
253 return m_interactive;
254}
255
256quint64 QmlProfilerApplication::parseFeatures(const QStringList &featureList, const QString &values,
257 bool exclude)
258{
259 quint64 features = exclude ? std::numeric_limits<quint64>::max() : 0;
260 const QStringList givenFeatures = values.split(sep: QLatin1Char(','));
261 for (const QString &f : givenFeatures) {
262 int index = featureList.indexOf(str: f);
263 if (index < 0) {
264 logError(error: tr(s: "Unknown feature '%1'").arg(a: f));
265 return 0;
266 }
267 quint64 flag = static_cast<quint64>(1) << index;
268 features = (exclude ? (features ^ flag) : (features | flag));
269 }
270 if (features == 0) {
271 logError(error: exclude ? tr(s: "No features remaining to record after processing --exclude.") :
272 tr(s: "No features specified for --include."));
273 }
274 return features;
275}
276
277void QmlProfilerApplication::flush()
278{
279 if (m_recording) {
280 m_pendingRequest = REQUEST_FLUSH;
281 m_qmlProfilerClient->setRecording(false);
282 } else {
283 if (m_profilerData->save(filename: m_interactiveOutputFile)) {
284 m_profilerData->clear();
285 if (!m_interactiveOutputFile.isEmpty())
286 prompt(line: tr(s: "Data written to %1.").arg(a: m_interactiveOutputFile));
287 else
288 prompt();
289 } else {
290 prompt(line: tr(s: "Saving failed."));
291 }
292 m_interactiveOutputFile.clear();
293 m_pendingRequest = REQUEST_NONE;
294 }
295}
296
297void QmlProfilerApplication::output()
298{
299 if (m_profilerData->save(filename: m_interactiveOutputFile)) {
300 if (!m_interactiveOutputFile.isEmpty())
301 prompt(line: tr(s: "Data written to %1.").arg(a: m_interactiveOutputFile));
302 else
303 prompt();
304 } else {
305 prompt(line: tr(s: "Saving failed"));
306 }
307
308 m_interactiveOutputFile.clear();
309 m_pendingRequest = REQUEST_NONE;
310}
311
312bool QmlProfilerApplication::checkOutputFile(PendingRequest pending)
313{
314 if (m_interactiveOutputFile.isEmpty())
315 return true;
316 QFileInfo file(m_interactiveOutputFile);
317 if (file.exists()) {
318 if (!file.isFile()) {
319 prompt(line: tr(s: "Cannot overwrite %1.").arg(a: m_interactiveOutputFile));
320 m_interactiveOutputFile.clear();
321 } else {
322 prompt(line: tr(s: "%1 exists. Overwrite (y/n)?").arg(a: m_interactiveOutputFile));
323 m_pendingRequest = pending;
324 }
325 return false;
326 } else {
327 return true;
328 }
329}
330
331void QmlProfilerApplication::userCommand(const QString &command)
332{
333 auto args = QStringView{command}.split(sep: QChar::Space, behavior: Qt::SkipEmptyParts);
334 if (args.isEmpty()) {
335 prompt();
336 return;
337 }
338
339 QByteArray cmd = args.takeFirst().trimmed().toLatin1();
340
341 if (m_pendingRequest == REQUEST_QUIT) {
342 if (cmd == Constants::CMD_YES || cmd == Constants::CMD_YES2) {
343 quit();
344 } else if (cmd == Constants::CMD_NO || cmd == Constants::CMD_NO2) {
345 m_pendingRequest = REQUEST_NONE;
346 prompt();
347 } else {
348 prompt(line: tr(s: "Really quit (y/n)?"));
349 }
350 return;
351 }
352
353 if (m_pendingRequest == REQUEST_OUTPUT_FILE || m_pendingRequest == REQUEST_FLUSH_FILE) {
354 if (cmd == Constants::CMD_YES || cmd == Constants::CMD_YES2) {
355 if (m_pendingRequest == REQUEST_OUTPUT_FILE)
356 output();
357 else
358 flush();
359 } else if (cmd == Constants::CMD_NO || cmd == Constants::CMD_NO2) {
360 m_pendingRequest = REQUEST_NONE;
361 m_interactiveOutputFile.clear();
362 prompt();
363 } else {
364 prompt(line: tr(s: "%1 exists. Overwrite (y/n)?"));
365 }
366 return;
367 }
368
369 if (cmd == Constants::CMD_RECORD || cmd == Constants::CMD_RECORD2) {
370 m_pendingRequest = REQUEST_TOGGLE_RECORDING;
371 m_qmlProfilerClient->setRecording(!m_recording);
372 } else if (cmd == Constants::CMD_QUIT || cmd == Constants::CMD_QUIT2) {
373 m_pendingRequest = REQUEST_QUIT;
374 if (m_recording) {
375 prompt(line: tr(s: "The application is still generating data. Really quit (y/n)?"));
376 } else if (!m_profilerData->isEmpty()) {
377 prompt(line: tr(s: "There is still trace data in memory. Really quit (y/n)?"));
378 } else {
379 quit();
380 }
381 } else if (cmd == Constants::CMD_OUTPUT || cmd == Constants::CMD_OUTPUT2) {
382 if (m_recording) {
383 prompt(line: tr(s: "Cannot output while recording data."));
384 } else if (m_profilerData->isEmpty()) {
385 prompt(line: tr(s: "No data was recorded so far."));
386 } else {
387 m_interactiveOutputFile = args.size() > 0 ? args.at(i: 0).toString() : m_outputFile;
388 if (checkOutputFile(pending: REQUEST_OUTPUT_FILE))
389 output();
390 }
391 } else if (cmd == Constants::CMD_CLEAR || cmd == Constants::CMD_CLEAR2) {
392 if (m_recording) {
393 prompt(line: tr(s: "Cannot clear data while recording."));
394 } else if (m_profilerData->isEmpty()) {
395 prompt(line: tr(s: "No data was recorded so far."));
396 } else {
397 m_profilerData->clear();
398 prompt(line: tr(s: "Trace data cleared."));
399 }
400 } else if (cmd == Constants::CMD_FLUSH || cmd == Constants::CMD_FLUSH2) {
401 if (!m_recording && m_profilerData->isEmpty()) {
402 prompt(line: tr(s: "No data was recorded so far."));
403 } else {
404 m_interactiveOutputFile = args.size() > 0 ? args.at(i: 0).toString() : m_outputFile;
405 if (checkOutputFile(pending: REQUEST_FLUSH_FILE))
406 flush();
407 }
408 } else {
409 prompt(line: tr(s: commandTextC));
410 }
411}
412
413void QmlProfilerApplication::notifyTraceStarted()
414{
415 // Synchronize to server state. It doesn't hurt to do this multiple times in a row for
416 // different traces. There is no symmetric event to "Complete" after all.
417 m_recording = true;
418
419 if (m_pendingRequest == REQUEST_TOGGLE_RECORDING) {
420 m_pendingRequest = REQUEST_NONE;
421 prompt(line: tr(s: "Recording started"));
422 } else {
423 prompt(line: tr(s: "Application started recording"), ready: false);
424 }
425}
426
427void QmlProfilerApplication::outputData()
428{
429 if (!m_profilerData->isEmpty()) {
430 m_profilerData->save(filename: m_outputFile);
431 m_profilerData->clear();
432 }
433}
434
435void QmlProfilerApplication::run()
436{
437 if (m_runMode == LaunchMode) {
438 if (!m_socketFile.isEmpty()) {
439 logStatus(status: QString::fromLatin1(ba: "Listening on %1 ...").arg(a: m_socketFile));
440 m_connection->startLocalServer(fileName: m_socketFile);
441 }
442 m_process = new QProcess(this);
443 QStringList arguments;
444 arguments << QString::fromLatin1(ba: "-qmljsdebugger=%1:%2,block,services:CanvasFrameRate")
445 .arg(a: QLatin1String(m_socketFile.isEmpty() ? "port" : "file"))
446 .arg(a: m_socketFile.isEmpty() ? QString::number(m_port) : m_socketFile);
447 arguments << m_arguments;
448
449 m_process->setProcessChannelMode(QProcess::MergedChannels);
450 connect(sender: m_process, signal: &QIODevice::readyRead, context: this, slot: &QmlProfilerApplication::processHasOutput);
451 connect(sender: m_process, signal: QOverload<int, QProcess::ExitStatus>::of(ptr: &QProcess::finished),
452 context: this, slot: [this](int){ processFinished(); });
453 logStatus(status: QString("Starting '%1 %2' ...").arg(args&: m_executablePath,
454 args: arguments.join(sep: QLatin1Char(' '))));
455 m_process->start(program: m_executablePath, arguments);
456 if (!m_process->waitForStarted()) {
457 logError(error: QString("Could not run '%1': %2").arg(args&: m_executablePath,
458 args: m_process->errorString()));
459 exit(retcode: 1);
460 }
461 }
462 m_connectTimer.start();
463}
464
465void QmlProfilerApplication::tryToConnect()
466{
467 Q_ASSERT(!m_connection->isConnected());
468 ++ m_connectionAttempts;
469
470 if (!(m_connectionAttempts % 5)) {// print every 5 seconds
471 if (m_socketFile.isEmpty()) {
472 logWarning(warning: QString::fromLatin1(ba: "Could not connect to %1:%2 for %3 seconds ...")
473 .arg(a: m_hostName).arg(a: m_port).arg(a: m_connectionAttempts));
474 } else {
475 logWarning(warning: QString::fromLatin1(ba: "No connection received on %1 for %2 seconds ...")
476 .arg(a: m_socketFile).arg(a: m_connectionAttempts));
477 }
478 }
479
480 if (m_socketFile.isEmpty()) {
481 logStatus(status: QString::fromLatin1(ba: "Connecting to %1:%2 ...").arg(a: m_hostName).arg(a: m_port));
482 m_connection->connectToHost(hostName: m_hostName, port: m_port);
483 }
484}
485
486void QmlProfilerApplication::connected()
487{
488 m_connectTimer.stop();
489 QString endpoint = m_socketFile.isEmpty() ?
490 QString::fromLatin1(ba: "%1:%2").arg(a: m_hostName).arg(a: m_port) :
491 m_socketFile;
492 prompt(line: tr(s: "Connected to %1. Wait for profile data or type a command (type 'help' to show list "
493 "of commands).\nRecording Status: %2")
494 .arg(a: endpoint).arg(a: m_recording ? tr(s: "on") : tr(s: "off")));
495}
496
497void QmlProfilerApplication::disconnected()
498{
499 if (m_runMode == AttachMode) {
500 int exitCode = 0;
501 if (m_recording) {
502 logError(error: "Connection dropped while recording, last trace is damaged!");
503 exitCode = 2;
504 }
505
506 if (!m_interactive )
507 exit(retcode: exitCode);
508 else
509 m_qmlProfilerClient->clearAll();
510 }
511}
512
513void QmlProfilerApplication::processHasOutput()
514{
515 Q_ASSERT(m_process);
516 while (m_process->bytesAvailable())
517 std::cerr << m_process->readAll().constData();
518}
519
520void QmlProfilerApplication::processFinished()
521{
522 Q_ASSERT(m_process);
523 int exitCode = 0;
524 if (m_process->exitStatus() == QProcess::NormalExit) {
525 logStatus(status: QString("Process exited (%1).").arg(a: m_process->exitCode()));
526 if (m_recording) {
527 logError(error: "Process exited while recording, last trace is damaged!");
528 exitCode = 2;
529 }
530 } else {
531 logError(error: "Process crashed!");
532 exitCode = 3;
533 }
534 if (!m_interactive)
535 exit(retcode: exitCode);
536 else
537 m_qmlProfilerClient->clearAll();
538}
539
540void QmlProfilerApplication::traceClientEnabledChanged(bool enabled)
541{
542 if (enabled) {
543 logStatus(status: "Trace client is attached.");
544 // blocked server is waiting for recording message from both clients
545 // once the last one is connected, both messages should be sent
546 m_qmlProfilerClient->setRecording(m_recording);
547 }
548}
549
550void QmlProfilerApplication::traceFinished()
551{
552 m_recording = false; // only on "Complete" we know that the trace is really finished.
553
554 if (m_pendingRequest == REQUEST_FLUSH) {
555 flush();
556 } else if (m_pendingRequest == REQUEST_TOGGLE_RECORDING) {
557 m_pendingRequest = REQUEST_NONE;
558 prompt(line: tr(s: "Recording stopped."));
559 } else {
560 prompt(line: tr(s: "Application stopped recording."), ready: false);
561 }
562
563 m_qmlProfilerClient->clearEvents();
564}
565
566void QmlProfilerApplication::prompt(const QString &line, bool ready)
567{
568 if (m_interactive) {
569 if (!line.isEmpty())
570 std::cerr << qPrintable(line) << std::endl;
571 std::cerr << "> ";
572 if (ready)
573 emit readyForCommand();
574 }
575}
576
577void QmlProfilerApplication::logError(const QString &error)
578{
579 std::cerr << "Error: " << qPrintable(error) << std::endl;
580}
581
582void QmlProfilerApplication::logWarning(const QString &warning)
583{
584 std::cerr << "Warning: " << qPrintable(warning) << std::endl;
585}
586
587void QmlProfilerApplication::logStatus(const QString &status)
588{
589 if (!m_verbose)
590 return;
591 std::cerr << qPrintable(status) << std::endl;
592}
593

source code of qtdeclarative/tools/qmlprofiler/qmlprofilerapplication.cpp