diff options
-rw-r--r-- | README | 9 | ||||
-rw-r--r-- | examples/hover.qml | 79 | ||||
-rw-r--r-- | examples/traceHover.qml | 51 | ||||
-rw-r--r-- | examples/traces/flickable.qml (renamed from examples/flickable.qml) | 0 | ||||
-rw-r--r-- | examples/traces/mpta.qml (renamed from examples/mpta.qml) | 0 | ||||
-rw-r--r-- | imports/Diagrams/UmlSequence/Message.qml | 14 | ||||
-rw-r--r-- | imports/Diagrams/UmlSequence/ObjectInstance.qml | 2 | ||||
-rw-r--r-- | src/src.pro | 3 | ||||
-rw-r--r-- | src/trace/plugin.cpp | 55 | ||||
-rw-r--r-- | src/trace/qmldir | 2 | ||||
-rw-r--r-- | src/trace/qmlmessagetrace.cpp | 503 | ||||
-rw-r--r-- | src/trace/qmlmessagetrace_p.h | 121 | ||||
-rw-r--r-- | src/trace/trace.pro | 24 |
13 files changed, 852 insertions, 11 deletions
@@ -1,15 +1,12 @@ The initial goal of the UmlQuick project is to automatically generate -message trace diagrams from a declarative QML form by instrumenting the +message trace diagrams via a declarative QML form by instrumenting the code being traced. Perhaps other types of diagrams could be created in QML too. It was inspired by the UMLGraph project http://www.umlgraph.org/doc/seq-intro.html but rewritten because I wanted to try to use QML instead of gnu pic macros. -The code here is only for viewing the generated QML, and there are a couple -of examples. Generating QML inside core parts of Qt depends on -https://codereview.qt-project.org/#change,43201 -and there is an example patch for qtbase to trace touch events here -https://codereview.qt-project.org/#change,65307 +There are some components for viewing the generated QML, and a few +examples: how to generate the trace, and examples of complete traces. It should work with Qt 5.7 or newer (due to some limited usage of QtQuick.Controls 2). diff --git a/examples/hover.qml b/examples/hover.qml new file mode 100644 index 0000000..a807d50 --- /dev/null +++ b/examples/hover.qml @@ -0,0 +1,79 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Labs UmlQuick project. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +import QtQuick 2.8 +import Qt.labs.UmlQuick.Trace 1.0 + +Rectangle { + color: outerMA.containsMouse ? "lightsteelblue" : "gray" + width: 100; height: 400 + objectName: "outerRect" + + MessageTrace { + category: "qt.quick.hover.trace" + outputPrefix: "hoverSequence-" + } + + MouseArea { + id: outerMA + objectName: "outerMA" + anchors.fill: parent + hoverEnabled: true + } + + Rectangle { + color: "blue" + border.color: innerMA.containsMouse ? "white" : "transparent" + width: 80; height: 80; x: 10; y: 10 + objectName: "innerRect" + + MouseArea { + id: innerMA + objectName: "innerMA" + anchors.fill: parent +// propagateHoverEvents: true // proposed in https://codereview.qt-project.org/#/c/160688/ + hoverEnabled: true + onContainsMouseChanged: Qt.quit() // stop after this to avoid an excessive message trace + } + } + Shortcut { + sequence: StandardKey.Quit + onActivated: Qt.quit() // control-Q will quit at any time, and write the message trace + } +} diff --git a/examples/traceHover.qml b/examples/traceHover.qml new file mode 100644 index 0000000..5a04ded --- /dev/null +++ b/examples/traceHover.qml @@ -0,0 +1,51 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Labs UmlQuick project. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +import Qt.labs.UmlQuick.Trace 1.0 + +/* + This is a means of instrumenting any pure-QML application to generate a + sequence diagram showing how hover events are propagated. Run it like this: + + qml traceHover.qml testApplication.qml +*/ +MessageTrace { + category: "qt.quick.hover.trace" + outputPrefix: "hoverSequence-" +} diff --git a/examples/flickable.qml b/examples/traces/flickable.qml index 2079fbd..2079fbd 100644 --- a/examples/flickable.qml +++ b/examples/traces/flickable.qml diff --git a/examples/mpta.qml b/examples/traces/mpta.qml index a0932c0..a0932c0 100644 --- a/examples/mpta.qml +++ b/examples/traces/mpta.qml diff --git a/imports/Diagrams/UmlSequence/Message.qml b/imports/Diagrams/UmlSequence/Message.qml index 19d38cf..92b441f 100644 --- a/imports/Diagrams/UmlSequence/Message.qml +++ b/imports/Diagrams/UmlSequence/Message.qml @@ -52,8 +52,14 @@ MouseArea { hoverEnabled: true z: 1 property bool toSelf: from && from === to - property bool backwards: from ? from.x > to.x : false + property bool backwards: from && to ? from.x > to.x : false property real timestamp: 0 + Text { + x: -root.x + text: root.timestamp + color: "darkblue" + font.pointSize: 8 + } Rectangle { width: 150 height: 20 @@ -89,10 +95,10 @@ MouseArea { x: backwards || toSelf ? 0 : parent.width - width } ToolTip { - text: root.methodSignature + "\n" + root.backtrace + "\n\nparams:\n" + root.params + text: root.methodSignature + "\n" + root.backtrace + "\n\nparams:\n" + root.params + "\n\ntimestamp: " + root.timestamp visible: root.containsMouse } - anchors.left: backwards ? to.horizontalCenter : from ? from.horizontalCenter : undefined - anchors.right: backwards ? from.horizontalCenter : toSelf ? undefined : to.horizontalCenter + anchors.left: backwards && to ? to.horizontalCenter : from ? from.horizontalCenter : undefined + anchors.right: backwards && from ? from.horizontalCenter : toSelf ? undefined : to ? to.horizontalCenter : undefined } diff --git a/imports/Diagrams/UmlSequence/ObjectInstance.qml b/imports/Diagrams/UmlSequence/ObjectInstance.qml index b8dea17..a2b45f3 100644 --- a/imports/Diagrams/UmlSequence/ObjectInstance.qml +++ b/imports/Diagrams/UmlSequence/ObjectInstance.qml @@ -96,7 +96,7 @@ Rectangle { Text { id: rotatedLabel color: "darkblue" - text: objectClassLabel.text + text: objectClassLabel.text + " " + objectNameLabel.text anchors.centerIn: parent } } diff --git a/src/src.pro b/src/src.pro new file mode 100644 index 0000000..2018864 --- /dev/null +++ b/src/src.pro @@ -0,0 +1,3 @@ +TEMPLATE = subdirs +SUBDIRS += \ + trace diff --git a/src/trace/plugin.cpp b/src/trace/plugin.cpp new file mode 100644 index 0000000..0ae90b2 --- /dev/null +++ b/src/trace/plugin.cpp @@ -0,0 +1,55 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Shawn Rutledge +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Labs UmlQuick project. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include <QtQml> + +#include "qmlmessagetrace_p.h" + +class TracePlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) + +public: + void registerTypes(const char *uri) + { + qmlRegisterType<QmlMessageTrace>(uri, 1, 0, "MessageTrace"); + } +}; + +#include "plugin.moc" diff --git a/src/trace/qmldir b/src/trace/qmldir new file mode 100644 index 0000000..7cbebbb --- /dev/null +++ b/src/trace/qmldir @@ -0,0 +1,2 @@ +module Qt.labs.UmlQuick.Trace +plugin trace diff --git a/src/trace/qmlmessagetrace.cpp b/src/trace/qmlmessagetrace.cpp new file mode 100644 index 0000000..fe6e72d --- /dev/null +++ b/src/trace/qmlmessagetrace.cpp @@ -0,0 +1,503 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Shawn Rutledge +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Labs UmlQuick project. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include "qmlmessagetrace_p.h" +#include <QCoreApplication> +#include <QDateTime> +#include <QDebug> +#include <QFile> +#include <QStringList> +#include <private/qobject_p.h> + +/*! + \class QmlMessageTrace + \inmodule UmlQuick + + \brief The QmlMessageTrace class provides a QML output stream for rendering + a UML \l {https://en.wikipedia.org/wiki/Sequence_diagram}{Sequence diagram} + (also known as a Message Trace diagram). + + \section1 Basic Use + + Any QML application can instantiate a trace object. It will immediately + begin capturing categorized log output. It induces each line of output from + the given category to also include a backtrace; then it tries to + reconstruct sequences of method calls by splicing the backtraces together. + The result is that often a few qCDebug calls in strategic "destination" + functions (such as functions which handle events, make state changes and + emit signals) will be enough to reconstruct a fairly complete UML Message + Trace diagram. + + When the trace is completed, simply run the qml output file with the qml + runtime tool. It will import components from Diagrams.UmlSequence, which + should be installed already if you have built this module properly. + + For example, to trace hover events in a QML application, create a file + traceHover.qml which looks like the following: + + \code + import Qt.labs.UmlQuick.Trace 1.0 + + MessageTrace { + category: "qt.quick.hover.trace" + outputPrefix: "hoverSequence-" + } + \endcode + + Then start up the qml runtime, loading traceHover.qml first, and then your + application. After exiting, you can view the sequence diagram via the + qml output. + + \code + $ qml traceHover.qml testApplication.qml + + $ ls hoverSequence* + hoverSequence-20160601-17.24.45.357.qml + + $ qml hoverSequence-20160601-17.24.45.357.qml + \endcode + + Alternatively, your application can be instrumented: + import Qt.labs.UmlQuick.Trace, declare a MessageTrace somewhere (perhaps as + a child of the root Window or Item, or in some component where the tracing + is relevant), and use the \l enabled property to start and stop tracing. + Each time \l enabled is set to false, a new qml file will be written. Its + filename will begin with the given outputPrefix, to which will be appended + the timestamp when the trace was started. In this way it's possible to view + traces more dynamically: if you want to trace the series of method calls + which led to a particular behavior, enable the trace when the user begins a + specific action, and disable it when that action is done. + + In order to generate a valid sequence diagram, the \l category should + have a maximum of one log output per call. If it has more than one, the + diagram will show that the method called itself. That would be incorrect: + the self-call should be reserved to indicate recursion. + + The category debug output should not include Q_FUNC_INFO, because that + information is already available in every log message, so including it + again would be redundant. Every qCDebug for tracing class methods should + include \c this as the first parameter. If you include extra information, e.g. + \code + qCDebug(QC_CATEGORY) << this << parameter1 << parameter2 + \endcode + it will appear in tooltips (along with the backtraces) over the arrows on + the diagram. Typically you should include information about any interesting + method parameters; but too much information will make the diagram hard to + read. +*/ + +/*! + \fn QDebug::QDebug(QIODevice *device) + + Constructs a debug stream that writes to the given \a device. +*/ + +QT_BEGIN_NAMESPACE + +//#define MT_DEBUG_ENABLED +#ifdef MT_DEBUG_ENABLED +#define MT_DEBUG printf +#else +#define MT_DEBUG(format, args...) ((void)0) +#endif + +QHash<QByteArray, QList<QmlMessageTrace*> > QmlMessageTrace::m_categoryInstances; +QtMessageHandler QmlMessageTrace::m_parentMessageHandler(nullptr); + +// Regex for stuff like QQuickMouseArea(0x16b9cf0, name="outerMA", parent=0x16fc070, geometry=0,0 100x400) +// Captures only the part outside parentheses and the part inside +QRegularExpression QmlMessageTrace::m_regexObjectFormatted(QStringLiteral( + "([\\w_]+)\\((.+)\\)")); + +// Regex for pointers like 0x16ec750 +QRegularExpression QmlMessageTrace::m_regexPointer(QStringLiteral( + "0[xX]([0-9a-fA-F]+)")); + +int QmlMessageTrace::m_refCount(0); + +static QString pointerHash(void* ptr) +{ + QByteArray ret; + ret.setNum((qulonglong)ptr, 16); + // qml doesn't like an ID to start with a digit + // so, shift 0-9 to g-p + for (int i = 0; i < ret.length(); ++i) { + QChar ch(ret[i]); + if (ch.isDigit()) + ret[i] = (char)(ret[i] + 55); + } + if (ret.length() == 1) + return QStringLiteral("null"); + return QLatin1String(ret); +} + +void QmlMessageTrace::messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &text) +{ + bool consumed = false; + if (m_categoryInstances.contains(context.category)) { + QList<QmlMessageTrace *> instances = m_categoryInstances.value(context.category); + QString formatted = qFormatLogMessage(type, context, text); + for (QmlMessageTrace *tracer : instances) { + tracer->log(type, context, formatted); + consumed = true; + } + } +// qDebug() << Q_FUNC_INFO << context.category << text << "found tracers?" << m_categoryInstances.value(context.category); + if (!consumed) + m_parentMessageHandler(type, context, text); +} + +QmlMessageTrace::QmlMessageTrace() + : QObject() + , m_previousTimestamp(0) + , m_enabled(true) + , m_category(nullptr) + , m_outputPrefix("messagetrace") +{ + ++m_refCount; + if (qEnvironmentVariableIsSet("QT_MESSAGE_PATTERN")) { + qFatal("please unset the QT_MESSAGE_PATTERN env variable before trying to use QmlMessageTrace"); + QCoreApplication::exit(-1); + } + qSetMessagePattern(QStringLiteral("%{time process}|%{backtrace depth=20}|%{message}")); +} + +QmlMessageTrace::~QmlMessageTrace() +{ + if (--m_refCount == 0) + qInstallMessageHandler(nullptr); + if (!m_messages.isEmpty()) + writeQml(); +} + +void QmlMessageTrace::setCategory(QString cat) +{ + QByteArray category = cat.toUtf8(); + if (m_category == category) + return; + + if (m_categoryInstances.isEmpty()) { + // first-use init + QLoggingCategory::installFilter(&QmlMessageTrace::categoryFilter); + } + + if (m_categoryInstances.contains(m_category)) + m_categoryInstances[m_category].removeAll(this); + if (m_categoryInstances.contains(category)) + m_categoryInstances[category].append(this); + else + m_categoryInstances.insert(category, QList<QmlMessageTrace *>() << this); + if (!m_parentMessageHandler) { + m_parentMessageHandler = qInstallMessageHandler(messageHandler); + } + + m_category = category; + emit categoryChanged(); +} + +void QmlMessageTrace::setOutputPrefix(QString outputPrefix) +{ + if (m_outputPrefix == outputPrefix) + return; + + m_outputPrefix = outputPrefix; + emit outputPrefixChanged(); +} + +void QmlMessageTrace::setEnabled(bool enabled) +{ + if (m_enabled == enabled) + return; + + m_enabled = enabled; + if (!enabled) + writeQml(); + emit enabledChanged(); +} + +void QmlMessageTrace::categoryFilter(QLoggingCategory *cat) +{ + if (m_categoryInstances.contains(cat->categoryName())) { + cat->setEnabled(QtDebugMsg, true); + // TODO warning, fatal + } else { + cat->setEnabled(QtDebugMsg, false); + } +//MT_DEBUG("%s %d\n", cat->categoryName(), cat->isDebugEnabled()); +} + +QString QmlMessageTrace::category() const +{ + return QString::fromUtf8(m_category); +} + +// A utility function to make QRegularExpression if/else matches possible +static bool hasMatch(const QRegularExpression ®ex, QRegularExpressionMatch &match, const QString &text) { + match = regex.match(text); + return match.hasMatch(); +} + +static void * stringToPointer(const QString &str) { + bool ptrOk = false; + void *ret = (void *)(str.toULongLong(&ptrOk, 16)); + if (!ptrOk) + ret = nullptr; + return ret; +} + +void QmlMessageTrace::parseClassAndMethod(const QString &classAndMethod, QString &className, QString &methodName) +{ + int scopingIdx = classAndMethod.indexOf(QStringLiteral("::")); + if (scopingIdx >= 0) { + className = classAndMethod.left(scopingIdx).split(' ', QString::SkipEmptyParts).last(); + int leftParenIdx = classAndMethod.indexOf('(', scopingIdx); + scopingIdx += 2; // get past it + methodName = leftParenIdx >= 0 ? classAndMethod.mid(scopingIdx, leftParenIdx - scopingIdx) : classAndMethod.mid(scopingIdx); + if (className.startsWith('Q') && className.endsWith(QStringLiteral("Application"))) + className = QStringLiteral("QGuiApplication"); + int qmlSuffixIdx = className.indexOf(QStringLiteral("_QML")); + if (qmlSuffixIdx > 0) + className = className.left(qmlSuffixIdx); + } +} + +void QmlMessageTrace::addObjectInstance(void *obj, const QString &objClass) +{ + QString id = obj ? pointerHash(obj) : QStringLiteral("ufo_") + objClass; + m_tracedObjectsById.insert(id, objClass); + m_recentObjectsByClass.insert(objClass, obj); + if (objClass == QStringLiteral("QQuickWindow")) + m_recentObjectsByClass.insert(QStringLiteral("QWindow"), obj); + if (obj && objClass.startsWith('Q') && !objClass.endsWith("Private")) { + QObject * q = static_cast<QObject*>(obj); + m_objects.insert(obj, q); + m_objects.insert(QObjectPrivate::get(q), q); + } +} + +void QmlMessageTrace::writeObjectInstance(QFile &f, QObject *o) +{ + if (!o) + return; + QString className = o->metaObject()->className(); + int qmlSuffixIdx = className.indexOf(QStringLiteral("_QML")); + if (qmlSuffixIdx > 0) + className = className.left(qmlSuffixIdx); + QString oName = o->objectName(); + if (oName.isEmpty()) + oName = QStringLiteral("0x%1").arg(qulonglong(o), 0, 16); + f.write(QStringLiteral(" ObjectInstance { id: %1; objectName: \"%2\"; objectClass: \"%3\" }\n") + .arg(pointerHash(o)).arg(oName).arg(className).toUtf8()); +} + +void QmlMessageTrace::logBacktrace(QStringList trace) +{ + if (trace.length() < 2) + return; + Message m; + m.timestamp = m_previousTimestamp; + m.callerPointer = nullptr; + m.calleePointer = nullptr; + QString calleeClass; + parseClassAndMethod(trace.takeFirst(), calleeClass, m.calleeMethod); + if (m.calleeMethod.isEmpty()) + return; + parseClassAndMethod(trace.first(), m.callerClass, m.callerMethod); + m.backtrace = trace; + logBacktrace(trace); + m_messages.append(m); +} + +void QmlMessageTrace::log(QtMsgType type, const QMessageLogContext &context, const QString &rawText) +{ + if (type != QtDebugMsg) + return; // so far we don't handle warning and critical + + MT_DEBUG("\n%s\n", qPrintable(rawText)); + Message m; + + // Extract info from pipe-separated stuff, and clean up the backtrace + m.backtrace = rawText.split('|'); + m.params = m.backtrace.takeLast(); // %{message} + m.timestamp = m.backtrace.takeFirst().trimmed().toDouble(); // %{time process} + { + int idx = m.backtrace.indexOf(QStringLiteral("QDebug::~QDebug")); + for (int i = 0; i <= idx; ++i) + m.backtrace.removeFirst(); + } + m.backtrace.removeFirst(); // the callee + + // -------- + // Callee + QString calleeClassAndMethod = QLatin1String(context.function); + QString calleeClassName; + + parseClassAndMethod(calleeClassAndMethod, calleeClassName, m.calleeMethod); + + QString callee0xPointerStr; + m.calleePointer = nullptr; + QRegularExpressionMatch match; + if (hasMatch(m_regexObjectFormatted, match, m.params) && match.capturedStart() == 0 && match.lastCapturedIndex() == 2) { + // Got formatted info about "this" which was supposed to be the first parameter to qCDebug + // something like QQuickMouseArea(0x16b9cf0, name="outerMA", parent=0x16fc070, geometry=0,0 100x400) +// calleeClassName = match.captured(1).toUtf8(); // strictly unnecessary: we got it via parseClassAndMethod above + QString calleeInfo = match.captured(2); + match = m_regexPointer.match(calleeInfo); + callee0xPointerStr = match.captured(0); + m.calleePointer = stringToPointer(match.captured(1)); + if (m.calleePointer) { + calleeInfo.remove(0, match.capturedEnd(1)); + if (calleeInfo.startsWith(',')) + calleeInfo.remove(0, 1); + calleeInfo = calleeInfo.trimmed(); + } +// MT_DEBUG("callee %s::%s %p guts %s from %s\n", qPrintable(calleeClassName), qPrintable(m.calleeMethod), m.calleePointer, qPrintable(calleeInfo), qPrintable(m.params)); + } else if (hasMatch(m_regexPointer, match, m.params) && match.capturedStart() == 0) { + // Got just a plain pointer to "this" which was supposed to be the first parameter to qCDebug +// MT_DEBUG("captured pointer %s @%d from %s\n", qPrintable(match.captured(1)), match.capturedStart(), qPrintable(m.params)); + callee0xPointerStr = match.captured(0); + m.calleePointer = stringToPointer(match.captured(1)); + } else { + printf("ignoring call to unknown object instance: %s %s:%s\n", context.category, context.function, qPrintable(m.params)); + } + MT_DEBUG(" callee %s::%s %p\n", qPrintable(calleeClassName), qPrintable(m.calleeMethod), m.calleePointer); + + if (m.calleePointer) { + if (!m_tracedObjectsById.contains(pointerHash(m.calleePointer))) + addObjectInstance(m.calleePointer, calleeClassName); + } else if (m_recentObjectsByClass.contains(calleeClassName)) { + // callee object is unknown; assume it's the last one with the same classname that we've seen + m.calleePointer = m_recentObjectsByClass.value(calleeClassName); + } else if (!calleeClassName.isEmpty()) { + // totally unknown object, but we do know the classname + addObjectInstance(m.calleePointer, calleeClassName); + } + if (calleeClassName.isEmpty()) + printf("failed to find callee class name in %s\n", qPrintable(rawText)); + + // -------- + // Caller + m.callerPointer = nullptr; + + // Guess the caller object ID; the m.backtrace can't tell us the caller's "this" pointer, unfortunately + if (!m.backtrace.isEmpty()) { + QString callerClassAndMethod = m.backtrace.first(); + parseClassAndMethod(callerClassAndMethod, m.callerClass, m.callerMethod); + if (m_recentObjectsByClass.contains(m.callerClass)) { + m.callerPointer = m_recentObjectsByClass.value(calleeClassName); + } else if (!m.callerClass.isEmpty()) { + addObjectInstance(nullptr, m.callerClass); + } + MT_DEBUG(" caller %p %s :: %s from %s\n", m.callerPointer, qPrintable(m.callerClass), qPrintable(m.callerMethod), qPrintable(callerClassAndMethod)); + // TODO deduce where we are in the message trace from caller method + + QStringList leadingBacktrace = m.backtrace; + leadingBacktrace.removeFirst(); + + m_previousTimestamp = m.timestamp; + + logBacktrace(leadingBacktrace); + } + +// QString rawLog = rawText; +// rawLog.replace(QStringLiteral("|"), QStringLiteral("\\n")); +// rawLog.replace(QStringLiteral("\""), QStringLiteral("\\\"")); + + m.params.replace(QStringLiteral("\""), QStringLiteral("\\\"")); + // Now that both objects exist we can declare the arrow +// if (isReturn) +// m_messageElement.append(" Return { \n from: " + m_fromObjectId + "\n to: " + +// m_toObjectId + "\n fromMethod: \"" + m_fromMethod + "\"\n"); +// else + + m.calleeSignature = QLatin1String(context.function); + m_messages.append(m); +} + +void QmlMessageTrace::writeQml() +{ + QString filePath = QString::fromLocal8Bit("%1%2.qml").arg(m_outputPrefix).arg(QDateTime::currentDateTime().toString(Qt::ISODate)); + MT_DEBUG("-> %s\n", qPrintable(filePath)); + QFile f(filePath); + if (f.open(QFile::WriteOnly)) { + f.write("import QtQuick 2.0\nimport Diagrams.UmlSequence 1.0\n\nUmlSequenceDiagram {\n"); + QSet<void *> objectsWritten; + QSet<QString> ufosWritten; + for (Message &m : m_messages) { + QObject *callee = m_objects.value(m.calleePointer); + m.calleePointer = callee; + if (callee && !objectsWritten.contains(callee)) { + writeObjectInstance(f, callee); + objectsWritten.insert(callee); + } + QObject *caller = m_objects.value(m.callerPointer); + if (!caller) { + void *p = m_recentObjectsByClass.value(m.callerClass); + caller = m_objects.value(p); + } + m.callerPointer = caller; + if (caller) { + if (!objectsWritten.contains(caller)) { + writeObjectInstance(f, caller); + objectsWritten.insert(caller); + } + } else if (!ufosWritten.contains(m.callerClass)) { + f.write(QStringLiteral(" ObjectInstance { id: %1; objectName: \"%1\"; objectClass: \"%2\" }\n") + .arg(QStringLiteral("ufo_") + m.callerClass).arg(m.callerClass).toUtf8()); + ufosWritten.insert(m.callerClass); + } + f.write(m.toQml().toUtf8()); + } + f.write("}\n"); + f.close(); + } else { + printf("failed to write %s", qPrintable(filePath)); + } +} + +QString QmlMessageTrace::Message::toQml() const +{ + QString callerId = callerPointer ? pointerHash(callerPointer) : QStringLiteral("ufo_") + callerClass; + return QStringLiteral( + " Message { \n from: %1\n to: %2\n method: \"%3\"\n methodSignature: \"%4\"\n" + " fromMethod: \"%5\"\n timestamp: %6\n backtrace: \"%7\"\n params: \"%8\"\n }\n") + .arg(callerId).arg(pointerHash(calleePointer)).arg(calleeMethod).arg(calleeSignature) + .arg(callerMethod).arg(timestamp).arg(backtrace.join(QStringLiteral("\\n"))).arg(params); +} + +QT_END_NAMESPACE diff --git a/src/trace/qmlmessagetrace_p.h b/src/trace/qmlmessagetrace_p.h new file mode 100644 index 0000000..737b20d --- /dev/null +++ b/src/trace/qmlmessagetrace_p.h @@ -0,0 +1,121 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Shawn Rutledge +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Labs UmlQuick project. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#ifndef QMLMESSAGETRACE_H +#define QMLMESSAGETRACE_H + +#include <QFile> +#include <QHash> +#include <QLoggingCategory> +#include <QRegularExpression> + +QT_BEGIN_NAMESPACE + +class QmlMessageTrace : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString category READ category WRITE setCategory NOTIFY categoryChanged) + Q_PROPERTY(QString outputPrefix READ outputPrefix WRITE setOutputPrefix NOTIFY outputPrefixChanged) + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + +public: + QmlMessageTrace(); + ~QmlMessageTrace(); + + QString category() const; + void setCategory(QString category); + + QString outputPrefix() const { return m_outputPrefix; } + void setOutputPrefix(QString outputPrefix); + + bool enabled() const { return m_enabled; } + void setEnabled(bool enabled); + +signals: + void categoryChanged(); + void outputPrefixChanged(); + void enabledChanged(); + +private: + static void categoryFilter(QLoggingCategory *cat); + static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &text); + void log(QtMsgType type, const QMessageLogContext &context, const QString &rawText); + void logBacktrace(QStringList trace); + void parseClassAndMethod(const QString &classAndMethod, QString &className, QString &methodName); + void addObjectInstance(void *obj, const QString &objClass); + void writeObjectInstance(QFile &f, QObject *o); + void writeQml(); + +private: + struct Message { + double timestamp; + QtMsgType type; + void *callerPointer; + QString callerClass; + QString callerMethod; + void *calleePointer; + QString calleeMethod; + QString calleeSignature; + QStringList backtrace; + QString params; + QString toQml() const; + }; + + static QHash<QByteArray, QList<QmlMessageTrace*> > m_categoryInstances; + static QtMessageHandler m_parentMessageHandler; + + static QRegularExpression m_regexObjectFormatted; + static QRegularExpression m_regexPointer; + + static int m_refCount; + + QList<Message> m_messages; + double m_previousTimestamp; + + QHash<QString, QString> m_tracedObjectsById; + QHash<QString, void*> m_recentObjectsByClass; + QHash<void*, QObject*> m_objects; // both QObjects and QObjectPrivates have entries here + + bool m_enabled; + QByteArray m_category; + QString m_outputPrefix; +}; + +QT_END_NAMESPACE + +#endif // QMLMESSAGETRACE_H diff --git a/src/trace/trace.pro b/src/trace/trace.pro new file mode 100644 index 0000000..fac422e --- /dev/null +++ b/src/trace/trace.pro @@ -0,0 +1,24 @@ +TEMPLATE = lib +TARGET = trace + +QT += quick core_private + +CONFIG += qt plugin + +SOURCES += \ + plugin.cpp \ + qmlmessagetrace.cpp + +HEADERS += \ + qmlmessagetrace_p.h + +TARGETPATH = Qt/labs/UmlQuick/Trace + +OTHER_FILES = qmldir + +target.path = $$[QT_INSTALL_QML]/$$TARGETPATH + +qmldir.files = qmldir +qmldir.path = $$[QT_INSTALL_QML]/$$TARGETPATH + +INSTALLS = target qmldir |