Properly support all events in trace files and clean up a bit.
[qt:qtdeclarative.git] / tools / qmlprofiler / qmlprofilerapplication.cpp
1 /****************************************************************************
2 **
3 ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
4 ** Contact: http://www.qt-project.org/legal
5 **
6 ** This file is part of the QtQml module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
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 Digia.  For licensing terms and
14 ** conditions see http://qt.digia.com/licensing.  For further information
15 ** use the contact form at http://qt.digia.com/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file.  Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24 **
25 ** In addition, as a special exception, Digia gives you certain additional
26 ** rights.  These rights are described in the Digia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
28 **
29 ** GNU General Public License Usage
30 ** Alternatively, this file may be used under the terms of the GNU
31 ** General Public License version 3.0 as published by the Free Software
32 ** Foundation and appearing in the file LICENSE.GPL included in the
33 ** packaging of this file.  Please review the following information to
34 ** ensure the GNU General Public License version 3.0 requirements will be
35 ** met: http://www.gnu.org/copyleft/gpl.html.
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include "qmlprofilerapplication.h"
43 #include "constants.h"
44 #include <QtCore/QStringList>
45 #include <QtCore/QTextStream>
46 #include <QtCore/QProcess>
47 #include <QtCore/QTimer>
48 #include <QtCore/QDateTime>
49 #include <QtCore/QFileInfo>
50 #include <QtCore/QDebug>
51
52 static const char usageTextC[] =
53 "Usage:\n"
54 "    qmlprofiler [options] [program] [program-options]\n"
55 "    qmlprofiler [options] -attach [hostname]\n"
56 "\n"
57 "QML Profiler retrieves QML tracing data from a running application.\n"
58 "The data collected can then be visualized in Qt Creator.\n"
59 "\n"
60 "The application to be profiled has to enable QML debugging. See the Qt Creator\n"
61 "documentation on how to do this for different Qt versions.\n"
62 "\n"
63 "Options:\n"
64 "    -help  Show this information and exit.\n"
65 "    -fromStart\n"
66 "           Record as soon as the engine is started, default is false.\n"
67 "    -p <number>, -port <number>\n"
68 "           TCP/IP port to use, default is 3768.\n"
69 "    -v, -verbose\n"
70 "           Print debugging output.\n"
71 "    -version\n"
72 "           Show the version of qmlprofiler and exit.\n";
73
74 static const char commandTextC[] =
75 "Commands:\n"
76 "    r, record\n"
77 "           Switch recording on or off.\n"
78 "    q, quit\n"
79 "           Terminate program.";
80
81 static const char TraceFileExtension[] = ".qtd";
82
83 QmlProfilerApplication::QmlProfilerApplication(int &argc, char **argv) :
84     QCoreApplication(argc, argv),
85     m_runMode(LaunchMode),
86     m_process(0),
87     m_tracePrefix(QLatin1String("trace")),
88     m_hostName(QLatin1String("127.0.0.1")),
89     m_port(3768),
90     m_verbose(false),
91     m_quitAfterSave(false),
92     m_qmlProfilerClient(&m_connection),
93     m_v8profilerClient(&m_connection),
94     m_connectionAttempts(0),
95     m_qmlDataReady(false),
96     m_v8DataReady(false)
97 {
98     m_connectTimer.setInterval(1000);
99     connect(&m_connectTimer, SIGNAL(timeout()), this, SLOT(tryToConnect()));
100
101     connect(&m_connection, SIGNAL(connected()), this, SLOT(connected()));
102     connect(&m_connection, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(connectionStateChanged(QAbstractSocket::SocketState)));
103     connect(&m_connection, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(connectionError(QAbstractSocket::SocketError)));
104
105     connect(&m_qmlProfilerClient, SIGNAL(enabledChanged()), this, SLOT(traceClientEnabled()));
106     connect(&m_qmlProfilerClient, SIGNAL(recordingChanged(bool)), this, SLOT(recordingChanged()));
107     connect(&m_qmlProfilerClient, SIGNAL(range(QQmlProfilerService::RangeType,QQmlProfilerService::BindingType,qint64,qint64,QStringList,QmlEventLocation)),
108             &m_profilerData, SLOT(addQmlEvent(QQmlProfilerService::RangeType,QQmlProfilerService::BindingType,qint64,qint64,QStringList,QmlEventLocation)));
109     connect(&m_qmlProfilerClient, SIGNAL(traceFinished(qint64)), &m_profilerData, SLOT(setTraceEndTime(qint64)));
110     connect(&m_qmlProfilerClient, SIGNAL(traceStarted(qint64)), &m_profilerData, SLOT(setTraceStartTime(qint64)));
111     connect(&m_qmlProfilerClient, SIGNAL(frame(qint64,int,int,int)), &m_profilerData, SLOT(addFrameEvent(qint64,int,int,int)));
112     connect(&m_qmlProfilerClient, SIGNAL(sceneGraphFrame(QQmlProfilerService::SceneGraphFrameType,
113                                          qint64,qint64,qint64,qint64,qint64,qint64)),
114             &m_profilerData, SLOT(addSceneGraphFrameEvent(QQmlProfilerService::SceneGraphFrameType,
115                                   qint64,qint64,qint64,qint64,qint64,qint64)));
116     connect(&m_qmlProfilerClient, SIGNAL(pixmapCache(QQmlProfilerService::PixmapEventType,qint64,
117                                                      QmlEventLocation,int,int,int)),
118             &m_profilerData, SLOT(addPixmapCacheEvent(QQmlProfilerService::PixmapEventType,qint64,
119                                                       QmlEventLocation,int,int,int)));
120
121     connect(&m_qmlProfilerClient, SIGNAL(complete()), this, SLOT(qmlComplete()));
122
123     connect(&m_v8profilerClient, SIGNAL(enabledChanged()), this, SLOT(profilerClientEnabled()));
124     connect(&m_v8profilerClient, SIGNAL(range(int,QString,QString,int,double,double)),
125             &m_profilerData, SLOT(addV8Event(int,QString,QString,int,double,double)));
126     connect(&m_v8profilerClient, SIGNAL(complete()), this, SLOT(v8Complete()));
127
128     connect(&m_profilerData, SIGNAL(error(QString)), this, SLOT(logError(QString)));
129     connect(&m_profilerData, SIGNAL(dataReady()), this, SLOT(traceFinished()));
130
131 }
132
133 QmlProfilerApplication::~QmlProfilerApplication()
134 {
135     if (!m_process)
136         return;
137     logStatus("Terminating process ...");
138     m_process->disconnect();
139     m_process->terminate();
140     if (!m_process->waitForFinished(1000)) {
141         logStatus("Killing process ...");
142         m_process->kill();
143     }
144     delete m_process;
145 }
146
147 bool QmlProfilerApplication::parseArguments()
148 {
149     for (int argPos = 1; argPos < arguments().size(); ++argPos) {
150         const QString arg = arguments().at(argPos);
151         if (arg == QLatin1String("-attach") || arg == QLatin1String("-a")) {
152             if (argPos + 1 == arguments().size()) {
153                 return false;
154             }
155             m_hostName = arguments().at(++argPos);
156             m_runMode = AttachMode;
157         } else if (arg == QLatin1String("-port") || arg == QLatin1String("-p")) {
158             if (argPos + 1 == arguments().size()) {
159                 return false;
160             }
161             const QString portStr = arguments().at(++argPos);
162             bool isNumber;
163             m_port = portStr.toUShort(&isNumber);
164             if (!isNumber) {
165                 logError(QString("'%1' is not a valid port").arg(portStr));
166                 return false;
167             }
168         } else if (arg == QLatin1String("-fromStart")) {
169             m_qmlProfilerClient.setRecording(true);
170             m_v8profilerClient.setRecording(true);
171         } else if (arg == QLatin1String("-help") || arg == QLatin1String("-h") || arg == QLatin1String("/h") || arg == QLatin1String("/?")) {
172             return false;
173         } else if (arg == QLatin1String("-verbose") || arg == QLatin1String("-v")) {
174             m_verbose = true;
175         } else if (arg == QLatin1String("-version")) {
176             print(QString("QML Profiler based on Qt %1.").arg(qVersion()));
177             ::exit(1);
178             return false;
179         } else {
180             if (m_programPath.isEmpty()) {
181                 m_programPath = arg;
182                 m_tracePrefix = QFileInfo(m_programPath).fileName();
183             } else {
184                 m_programArguments << arg;
185             }
186         }
187     }
188
189     if (m_runMode == LaunchMode
190             && m_programPath.isEmpty())
191         return false;
192
193     if (m_runMode == AttachMode
194             && !m_programPath.isEmpty())
195         return false;
196
197     return true;
198 }
199
200 void QmlProfilerApplication::printUsage()
201 {
202     print(QLatin1String(usageTextC));
203     print(QLatin1String(commandTextC));
204 }
205
206 int QmlProfilerApplication::exec()
207 {
208     QTimer::singleShot(0, this, SLOT(run()));
209     return QCoreApplication::exec();
210 }
211
212 void QmlProfilerApplication::printCommands()
213 {
214     print(QLatin1String(commandTextC));
215 }
216
217 QString QmlProfilerApplication::traceFileName() const
218 {
219     QString fileName = m_tracePrefix + "_" +
220             QDateTime::currentDateTime().toString(QLatin1String("yyMMdd_hhmmss")) +
221             TraceFileExtension;
222     if (QFileInfo(fileName).exists()) {
223         QString baseName;
224         int suffixIndex = 0;
225         do {
226             baseName = QFileInfo(fileName).baseName()
227                     + QString::number(suffixIndex++);
228         } while (QFileInfo(baseName + TraceFileExtension).exists());
229         fileName = baseName + TraceFileExtension;
230     }
231
232     return QFileInfo(fileName).absoluteFilePath();
233 }
234
235 void QmlProfilerApplication::userCommand(const QString &command)
236 {
237     QString cmd = command.trimmed();
238     if (cmd == Constants::CMD_HELP
239             || cmd == Constants::CMD_HELP2
240             || cmd == Constants::CMD_HELP3) {
241         printCommands();
242     } else if (cmd == Constants::CMD_RECORD
243                || cmd == Constants::CMD_RECORD2) {
244         m_qmlProfilerClient.setRecording(
245                     !m_qmlProfilerClient.isRecording());
246         m_v8profilerClient.setRecording(!m_v8profilerClient.isRecording());
247         m_qmlDataReady = false;
248         m_v8DataReady = false;
249     } else if (cmd == Constants::CMD_QUIT
250                || cmd == Constants::CMD_QUIT2) {
251         print(QLatin1String("Quit"));
252         if (m_qmlProfilerClient.isRecording()) {
253             m_quitAfterSave = true;
254             m_qmlDataReady = false;
255             m_v8DataReady = false;
256             m_qmlProfilerClient.setRecording(false);
257             m_v8profilerClient.setRecording(false);
258         } else {
259             quit();
260         }
261     }
262 }
263
264 void QmlProfilerApplication::run()
265 {
266     if (m_runMode == LaunchMode) {
267         m_process = new QProcess(this);
268         QStringList arguments;
269         arguments << QString::fromLatin1("-qmljsdebugger=port:%1,block").arg(m_port);
270         arguments << m_programArguments;
271
272         m_process->setProcessChannelMode(QProcess::MergedChannels);
273         connect(m_process, SIGNAL(readyRead()), this, SLOT(processHasOutput()));
274         connect(m_process, SIGNAL(finished(int,QProcess::ExitStatus)), this,
275                 SLOT(processFinished()));
276         logStatus(QString("Starting '%1 %2' ...").arg(m_programPath,
277                                                       arguments.join(" ")));
278         m_process->start(m_programPath, arguments);
279         if (!m_process->waitForStarted()) {
280             logError(QString("Could not run '%1': %2").arg(m_programPath,
281                                                            m_process->errorString()));
282             exit(1);
283         }
284
285     }
286     m_connectTimer.start();
287 }
288
289 void QmlProfilerApplication::tryToConnect()
290 {
291     Q_ASSERT(!m_connection.isConnected());
292     ++ m_connectionAttempts;
293
294     if (!m_verbose && !(m_connectionAttempts % 5)) {// print every 5 seconds
295         if (!m_verbose)
296             logError(QString("Could not connect to %1:%2 for %3 seconds ...").arg(
297                          m_hostName, QString::number(m_port),
298                          QString::number(m_connectionAttempts)));
299     }
300
301     if (m_connection.state() == QAbstractSocket::UnconnectedState) {
302         logStatus(QString("Connecting to %1:%2 ...").arg(m_hostName,
303                                                          QString::number(m_port)));
304         m_connection.connectToHost(m_hostName, m_port);
305     }
306 }
307
308 void QmlProfilerApplication::connected()
309 {
310     m_connectTimer.stop();
311     print(QString(QLatin1String("Connected to host:port %1:%2."
312                                 "Wait for profile data or type a command"
313                                 "(type 'help'' to show list of commands).")
314                   ).arg(m_hostName).arg((m_port)));
315     QString recordingStatus(QLatin1String("Recording Status: %1"));
316     if (!m_qmlProfilerClient.isRecording() &&
317             !m_v8profilerClient.isRecording())
318         recordingStatus = recordingStatus.arg(QLatin1String("Off"));
319     else
320         recordingStatus = recordingStatus.arg(QLatin1String("On"));
321     print(recordingStatus);
322 }
323
324 void QmlProfilerApplication::connectionStateChanged(
325         QAbstractSocket::SocketState state)
326 {
327     if (m_verbose)
328         qDebug() << state;
329 }
330
331 void QmlProfilerApplication::connectionError(QAbstractSocket::SocketError error)
332 {
333     if (m_verbose)
334         qDebug() << error;
335 }
336
337 void QmlProfilerApplication::processHasOutput()
338 {
339     Q_ASSERT(m_process);
340     while (m_process->bytesAvailable()) {
341         QTextStream out(stdout);
342         out << m_process->readAll();
343     }
344 }
345
346 void QmlProfilerApplication::processFinished()
347 {
348     Q_ASSERT(m_process);
349     if (m_process->exitStatus() == QProcess::NormalExit) {
350         logStatus(QString("Process exited (%1).").arg(m_process->exitCode()));
351
352         if (m_qmlProfilerClient.isRecording()) {
353             logError("Process exited while recording, last trace is lost!");
354             exit(2);
355         } else {
356             exit(0);
357         }
358     } else {
359         logError("Process crashed! Exiting ...");
360         exit(3);
361     }
362 }
363
364 void QmlProfilerApplication::traceClientEnabled()
365 {
366     logStatus("Trace client is attached.");
367     // blocked server is waiting for recording message from both clients
368     // once the last one is connected, both messages should be sent
369     m_qmlProfilerClient.sendRecordingStatus();
370     m_v8profilerClient.sendRecordingStatus();
371 }
372
373 void QmlProfilerApplication::profilerClientEnabled()
374 {
375     logStatus("Profiler client is attached.");
376
377     // blocked server is waiting for recording message from both clients
378     // once the last one is connected, both messages should be sent
379     m_qmlProfilerClient.sendRecordingStatus();
380     m_v8profilerClient.sendRecordingStatus();
381 }
382
383 void QmlProfilerApplication::traceFinished()
384 {
385     const QString fileName = traceFileName();
386
387     if (m_profilerData.save(fileName))
388         print(QString("Saving trace to %1.").arg(fileName));
389
390     if (m_quitAfterSave)
391         quit();
392 }
393
394 void QmlProfilerApplication::recordingChanged()
395 {
396     if (m_qmlProfilerClient.isRecording()) {
397         print(QLatin1String("Recording is on."));
398     } else {
399         print(QLatin1String("Recording is off."));
400     }
401 }
402
403 void QmlProfilerApplication::print(const QString &line)
404 {
405     QTextStream err(stderr);
406     err << line << endl;
407 }
408
409 void QmlProfilerApplication::logError(const QString &error)
410 {
411     QTextStream err(stderr);
412     err << "Error: " << error << endl;
413 }
414
415 void QmlProfilerApplication::logStatus(const QString &status)
416 {
417     if (!m_verbose)
418         return;
419     QTextStream err(stderr);
420     err << status << endl;
421 }
422
423 void QmlProfilerApplication::qmlComplete()
424 {
425     m_qmlDataReady = true;
426     if (m_v8profilerClient.state() != QQmlDebugClient::Enabled ||
427             m_v8DataReady) {
428         m_profilerData.complete();
429         // once complete is sent, reset the flag
430         m_qmlDataReady = false;
431     }
432 }
433
434 void QmlProfilerApplication::v8Complete()
435 {
436     m_v8DataReady = true;
437     if (m_qmlProfilerClient.state() != QQmlDebugClient::Enabled ||
438             m_qmlDataReady) {
439         m_profilerData.complete();
440         // once complete is sent, reset the flag
441         m_v8DataReady = false;
442     }
443 }