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

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