Revision control

Copy as Markdown

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant
#include "qqmlprofilerqtdwriter_p.h"
#include <private/qobject_p.h>
#include <QtCore/qfile.h>
#include <QtCore/qqueue.h>
#include <QtCore/qregularexpression.h>
#include <QtCore/qurl.h>
#include <QtCore/qxmlstream.h>
#include <QtCore/qxpfunctional.h>
#include <limits>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
const char PROFILER_FILE_VERSION[] = "1.02";
static const char *RANGE_TYPE_STRINGS[] = {
"Painting",
"Compiling",
"Creating",
"Binding",
"HandlingSignal",
"Javascript"
};
Q_STATIC_ASSERT(sizeof(RANGE_TYPE_STRINGS) == MaximumRangeType * sizeof(const char *));
static const char *MESSAGE_STRINGS[] = {
"Event",
"RangeStart",
"RangeData",
"RangeLocation",
"RangeEnd",
"Complete",
"PixmapCache",
"SceneGraph",
"MemoryAllocation",
"DebugMessage",
"Quick3D", // Not "Quick3DFrame" unfortunately. See precedence in Qt Creator.
};
Q_STATIC_ASSERT(sizeof(MESSAGE_STRINGS) == MaximumMessage * sizeof(const char *));
/////////////////////////////////////////////////////////////////
class QQmlProfilerQtdWriterPrivate : public QObjectPrivate
{
Q_DECLARE_PUBLIC(QQmlProfilerQtdWriter)
public:
enum State {
Empty,
AcquiringData,
ProcessingData,
Done
};
// data storage
QList<QQmlProfilerEventType> eventTypes;
QList<QQmlProfilerEvent> events;
qint64 traceStartTime = std::numeric_limits<qint64>::max();
qint64 traceEndTime = std::numeric_limits<qint64>::min();
// internal state while collecting events
qint64 qmlMeasuredTime = 0;
State state = Empty;
bool isEmpty() const { return events.isEmpty(); }
void sortStartTimes();
void computeQmlTime();
void setState(State state);
};
/////////////////////////////////////////////////////////////////
QQmlProfilerQtdWriter::QQmlProfilerQtdWriter(QObject *parent) :
QQmlProfilerEventReceiver(*new QQmlProfilerQtdWriterPrivate, parent)
{
}
QQmlProfilerQtdWriter::~QQmlProfilerQtdWriter() = default;
void QQmlProfilerQtdWriter::clear()
{
Q_D(QQmlProfilerQtdWriter);
// Do not clear the types. They persist for the whole session.
d->traceStartTime = std::numeric_limits<qint64>::max();
d->traceEndTime = std::numeric_limits<qint64>::min();
d->events.clear();
d->qmlMeasuredTime = 0;
d->setState(QQmlProfilerQtdWriterPrivate::Empty);
}
void QQmlProfilerQtdWriter::startTrace(qint64 time, const QList<int> &engineIds)
{
QQmlProfilerEventReceiver::startTrace(time, engineIds);
Q_D(QQmlProfilerQtdWriter);
if (time < d->traceStartTime)
d->traceStartTime = time;
}
void QQmlProfilerQtdWriter::endTrace(qint64 time, const QList<int> &engineIds)
{
QQmlProfilerEventReceiver::endTrace(time, engineIds);
Q_D(QQmlProfilerQtdWriter);
if (time > d->traceEndTime)
d->traceEndTime = time;
}
static QString qmlRangeTypeAsString(RangeType type)
{
if (type * sizeof(char *) < sizeof(RANGE_TYPE_STRINGS))
return QLatin1String(RANGE_TYPE_STRINGS[type]);
else
return QString::number(type);
}
static QString qmlMessageAsString(Message type)
{
if (type * sizeof(char *) < sizeof(MESSAGE_STRINGS))
return QLatin1String(MESSAGE_STRINGS[type]);
else
return QString::number(type);
}
void QQmlProfilerQtdWriter::addEvent(const QQmlProfilerEvent &event)
{
Q_D(QQmlProfilerQtdWriter);
d->setState(QQmlProfilerQtdWriterPrivate::AcquiringData);
d->events.append(event);
}
void QQmlProfilerQtdWriter::addEventType(const QQmlProfilerEventType &type)
{
QQmlProfilerEventType newType = type;
QString details;
// generate details string
if (!type.data().isEmpty()) {
details = type.data().simplified();
QRegularExpression rewrite(QStringLiteral("^\\(function \\$(\\w+)\\(\\) \\{ (return |)(.+) \\}\\)$"));
QRegularExpressionMatch match = rewrite.match(details);
if (match.hasMatch()) {
details = match.captured(1) +QLatin1String(": ") + match.captured(3);
}
if (details.startsWith(QLatin1String("file://")))
details = details.mid(details.lastIndexOf(QLatin1Char('/')) + 1);
}
newType.setData(details);
QString displayName;
switch (type.message()) {
case Event: {
switch (type.detailType()) {
case Mouse:
case Key:
displayName = QString::fromLatin1("Input:%1").arg(type.detailType());
break;
case AnimationFrame:
displayName = QString::fromLatin1("AnimationFrame");
break;
default:
displayName = QString::fromLatin1("Unknown");
}
break;
}
case RangeStart:
case RangeData:
case RangeLocation:
case RangeEnd:
case Complete:
Q_UNREACHABLE();
break;
case PixmapCacheEvent: {
const QString filePath = QUrl(type.location().filename()).path();
displayName = QStringView{filePath}.mid(filePath.lastIndexOf(QLatin1Char('/')) + 1)
+ QLatin1Char(':') + QString::number(type.detailType());
break;
}
case SceneGraphFrame:
displayName = QString::fromLatin1("SceneGraph:%1").arg(type.detailType());
break;
case MemoryAllocation:
displayName = QString::fromLatin1("MemoryAllocation:%1").arg(type.detailType());
break;
case DebugMessage:
displayName = QString::fromLatin1("DebugMessage:%1").arg(type.detailType());
break;
case Quick3DFrame:
displayName = QString::fromLatin1("Quick3DFrame:%1").arg(type.detailType());
break;
case MaximumMessage: {
const QQmlProfilerEventLocation eventLocation = type.location();
// generate hash
if (eventLocation.filename().isEmpty()) {
displayName = QString::fromLatin1("Unknown");
} else {
const QString filePath = QUrl(eventLocation.filename()).path();
displayName = QStringView{filePath}.mid(
filePath.lastIndexOf(QLatin1Char('/')) + 1) +
QLatin1Char(':') + QString::number(eventLocation.line());
}
break;
}
}
newType.setDisplayName(displayName);
d_func()->eventTypes.append(newType);
}
void QQmlProfilerQtdWriterPrivate::computeQmlTime()
{
// compute levels
qint64 level0Start = -1;
int level = 0;
for (const QQmlProfilerEvent &event : std::as_const(events)) {
const QQmlProfilerEventType &type = eventTypes.at(event.typeIndex());
if (type.message() != MaximumMessage)
continue;
switch (type.rangeType()) {
case Compiling:
case Creating:
case Binding:
case HandlingSignal:
case Javascript:
switch (event.rangeStage()) {
case RangeStart:
if (level++ == 0)
level0Start = event.timestamp();
break;
case RangeEnd:
if (--level == 0)
qmlMeasuredTime += event.timestamp() - level0Start;
break;
default:
break;
}
break;
default:
break;
}
}
}
bool compareStartTimes(const QQmlProfilerEvent &t1, const QQmlProfilerEvent &t2)
{
return t1.timestamp() < t2.timestamp();
}
void QQmlProfilerQtdWriterPrivate::sortStartTimes()
{
if (events.size() < 2)
return;
// assuming startTimes is partially sorted
// identify blocks of events and sort them with quicksort
QList<QQmlProfilerEvent>::iterator itFrom = events.end() - 2;
QList<QQmlProfilerEvent>::iterator itTo = events.end() - 1;
while (itFrom != events.begin() && itTo != events.begin()) {
// find block to sort
while (itFrom != events.begin() && itTo->timestamp() > itFrom->timestamp()) {
--itTo;
itFrom = itTo - 1;
}
// if we're at the end of the list
if (itFrom == events.begin())
break;
// find block length
while (itFrom != events.begin() && itTo->timestamp() <= itFrom->timestamp())
--itFrom;
if (itTo->timestamp() <= itFrom->timestamp())
std::sort(itFrom, itTo + 1, compareStartTimes);
else
std::sort(itFrom + 1, itTo + 1, compareStartTimes);
// move to next block
itTo = itFrom;
itFrom = itTo - 1;
}
}
void QQmlProfilerQtdWriter::complete(qint64 maximumTime)
{
Q_D(QQmlProfilerQtdWriter);
d->setState(QQmlProfilerQtdWriterPrivate::ProcessingData);
d->sortStartTimes();
d->computeQmlTime();
d->setState(QQmlProfilerQtdWriterPrivate::Done);
QQmlProfilerEventReceiver::complete(maximumTime);
}
struct StreamWriter {
QString error;
StreamWriter(const QString &filename)
{
if (!filename.isEmpty()) {
file.setFileName(filename);
if (!file.open(QIODevice::WriteOnly)) {
error = QQmlProfilerQtdWriter::tr("Could not open %1 for writing").arg(filename);
return;
}
} else {
if (!file.open(stdout, QIODevice::WriteOnly)) {
error = QQmlProfilerQtdWriter::tr("Could not open stdout for writing");
return;
}
}
stream.setDevice(&file);
stream.setAutoFormatting(true);
stream.writeStartDocument();
writeStartElement("trace");
}
~StreamWriter() {
if (!error.isEmpty())
return;
writeEndElement();
stream.writeEndDocument();
file.close();
}
template<typename Number>
void writeAttribute(const char *name, Number number)
{
stream.writeAttribute(QLatin1String(name), QString::number(number));
}
void writeAttribute(const char *name, const char *value)
{
stream.writeAttribute(QLatin1String(name), QLatin1String(value));
}
void writeAttribute(const char *name, const QQmlProfilerEvent &event, int i, bool printZero = true)
{
const qint64 number = event.number<qint64>(i);
if (printZero || number != 0)
writeAttribute(name, number);
}
template<typename Number>
void writeTextElement(const char *name, Number number)
{
writeTextElement(name, QString::number(number));
}
void writeTextElement(const char *name, const char *value)
{
stream.writeTextElement(QLatin1String(name), QLatin1String(value));
}
void writeTextElement(const char *name, const QString &value)
{
stream.writeTextElement(QLatin1String(name), value);
}
void writeStartElement(const char *name)
{
stream.writeStartElement(QLatin1String(name));
}
void writeEndElement()
{
stream.writeEndElement();
}
private:
QFile file;
QXmlStreamWriter stream;
};
struct DataIterator
{
DataIterator(
const QQmlProfilerQtdWriterPrivate *d,
qxp::function_ref<void(const QQmlProfilerEvent &, qint64)> &&sendEvent)
: d(d)
, sendEvent(std::move(sendEvent))
{}
void run();
private:
void handleRangeEvent(const QQmlProfilerEvent &event, const QQmlProfilerEventType &type);
void sendPending();
void endLevel0();
const QQmlProfilerQtdWriterPrivate *d = nullptr;
const qxp::function_ref<void(const QQmlProfilerEvent &, qint64)> sendEvent;
QQueue<QQmlProfilerEvent> pointEvents;
QList<QQmlProfilerEvent> rangeStarts[MaximumRangeType];
QList<qint64> rangeEnds[MaximumRangeType];
int level = 0;
};
void DataIterator::handleRangeEvent(
const QQmlProfilerEvent &event, const QQmlProfilerEventType &type)
{
QList<QQmlProfilerEvent> &starts = rangeStarts[type.rangeType()];
switch (event.rangeStage()) {
case RangeStart: {
++level;
starts.append(event);
break;
}
case RangeEnd: {
const qint64 invalidTimestamp = -1;
QList<qint64> &ends = rangeEnds[type.rangeType()];
// -1 because all valid timestamps are >= 0.
ends.resize(starts.size(), invalidTimestamp);
qsizetype i = starts.size();
while (ends[--i] != invalidTimestamp) {}
Q_ASSERT(i >= 0);
Q_ASSERT(starts[i].timestamp() <= event.timestamp());
ends[i] = event.timestamp();
if (--level == 0)
endLevel0();
break;
}
default:
break;
}
}
void DataIterator::sendPending()
{
// Send all pending events in the order of their start times.
qsizetype index[MaximumRangeType] = { 0, 0, 0, 0, 0, 0 };
while (true) {
// Find the range type with the minimum start time.
qsizetype minimum = MaximumRangeType;
qint64 minimumTime = std::numeric_limits<qint64>::max();
for (qsizetype i = 0; i < MaximumRangeType; ++i) {
const QList<QQmlProfilerEvent> &starts = rangeStarts[i];
if (starts.size() == index[i])
continue;
const qint64 timestamp = starts[index[i]].timestamp();
if (timestamp < minimumTime) {
minimumTime = timestamp;
minimum = i;
}
}
if (minimum == MaximumRangeType)
break;
// Send all point events that happened before the range we've found.
while (!pointEvents.isEmpty() && pointEvents.front().timestamp() < minimumTime)
sendEvent(pointEvents.dequeue(), 0);
// Send the range itself
sendEvent(rangeStarts[minimum][index[minimum]],
rangeEnds[minimum][index[minimum]] - minimumTime);
// Bump the index so that we don't send the same range again
++index[minimum];
}
}
void DataIterator::endLevel0()
{
sendPending();
for (qsizetype i = 0; i < MaximumRangeType; ++i) {
rangeStarts[i].clear();
rangeEnds[i].clear();
}
}
void DataIterator::run()
{
for (const QQmlProfilerEvent &event : std::as_const(d->events)) {
const QQmlProfilerEventType &type = d->eventTypes.at(event.typeIndex());
if (type.rangeType() != MaximumRangeType)
handleRangeEvent(event, type);
else if (level == 0)
sendEvent(event, 0);
else
pointEvents.enqueue(event);
}
for (qsizetype i = 0; i < MaximumRangeType; ++i) {
while (rangeEnds[i].size() < rangeStarts[i].size()) {
rangeEnds[i].append(d->traceEndTime);
--level;
}
}
sendPending();
}
bool QQmlProfilerQtdWriter::save(const QString &filename)
{
Q_D(QQmlProfilerQtdWriter);
if (isEmpty()) {
emit error(tr("No data to save"));
return false;
}
StreamWriter stream(filename);
if (!stream.error.isEmpty()) {
emit error(stream.error);
return false;
}
stream.writeAttribute("version", PROFILER_FILE_VERSION);
stream.writeAttribute("traceStart", d->traceStartTime);
stream.writeAttribute("traceEnd", d->traceEndTime);
stream.writeStartElement("eventData");
stream.writeAttribute("totalTime", d->qmlMeasuredTime);
for (int typeIndex = 0, end = d->eventTypes.size(); typeIndex < end; ++typeIndex) {
const QQmlProfilerEventType &eventData = d->eventTypes.at(typeIndex);
stream.writeStartElement("event");
stream.writeAttribute("index", typeIndex);
if (!eventData.displayName().isEmpty())
stream.writeTextElement("displayname", eventData.displayName());
stream.writeTextElement("type", eventData.rangeType() == MaximumRangeType
? qmlMessageAsString(eventData.message())
: qmlRangeTypeAsString(eventData.rangeType()));
const QQmlProfilerEventLocation location = eventData.location();
if (!location.filename().isEmpty())
stream.writeTextElement("filename", location.filename());
if (location.line() >= 0)
stream.writeTextElement("line", location.line());
if (location.column() >= 0)
stream.writeTextElement("column", location.column());
if (!eventData.data().isEmpty())
stream.writeTextElement("details", eventData.data());
if (eventData.rangeType() == Binding)
stream.writeTextElement("bindingType", eventData.detailType());
else if (eventData.message() == Event) {
switch (eventData.detailType()) {
case AnimationFrame:
stream.writeTextElement("animationFrame", eventData.detailType());
break;
case Key:
stream.writeTextElement("keyEvent", eventData.detailType());
break;
case Mouse:
stream.writeTextElement("mouseEvent", eventData.detailType());
break;
}
} else if (eventData.message() == PixmapCacheEvent)
stream.writeTextElement("cacheEventType", eventData.detailType());
else if (eventData.message() == SceneGraphFrame)
stream.writeTextElement("sgEventType", eventData.detailType());
else if (eventData.message() == MemoryAllocation)
stream.writeTextElement("memoryEventType", eventData.detailType());
stream.writeEndElement();
}
stream.writeEndElement(); // eventData
stream.writeStartElement("profilerDataModel");
auto sendEvent = [&](const QQmlProfilerEvent &event, qint64 duration = 0) {
Q_ASSERT(duration >= 0);
const QQmlProfilerEventType &type = d->eventTypes.at(event.typeIndex());
stream.writeStartElement("range");
stream.writeAttribute("startTime", event.timestamp());
if (duration != 0)
stream.writeAttribute("duration", duration);
stream.writeAttribute("eventIndex", event.typeIndex());
if (type.message() == Event) {
if (type.detailType() == AnimationFrame) {
// special: animation frame
stream.writeAttribute("framerate", event, 0);
stream.writeAttribute("animationcount", event, 1);
stream.writeAttribute("thread", event, 2);
} else if (type.detailType() == Key || type.detailType() == Mouse) {
// numerical value here, to keep the format a bit more compact
stream.writeAttribute("type", event, 0);
stream.writeAttribute("data1", event, 1);
stream.writeAttribute("data2", event, 2);
}
} else if (type.message() == PixmapCacheEvent) {
// special: pixmap cache event
if (type.detailType() == PixmapSizeKnown) {
stream.writeAttribute("width", event, 0);
stream.writeAttribute("height", event, 1);
} else if (type.detailType() == PixmapReferenceCountChanged
|| type.detailType() == PixmapCacheCountChanged) {
stream.writeAttribute("refCount", event, 1);
}
} else if (type.message() == SceneGraphFrame) {
stream.writeAttribute("timing1", event, 0, false);
stream.writeAttribute("timing2", event, 1, false);
stream.writeAttribute("timing3", event, 2, false);
stream.writeAttribute("timing4", event, 3, false);
stream.writeAttribute("timing5", event, 4, false);
} else if (type.message() == MemoryAllocation) {
stream.writeAttribute("amount", event, 0);
}
stream.writeEndElement();
};
DataIterator(d, std::move(sendEvent)).run();
stream.writeEndElement(); // profilerDataModel
return true;
}
void QQmlProfilerQtdWriterPrivate::setState(State newState)
{
// It's not an error, we are continuously calling "AcquiringData" for example
if (state == newState)
return;
Q_Q(QQmlProfilerQtdWriter);
switch (newState) {
case Empty:
// if it's not empty, complain but go on
if (!isEmpty())
emit q->error("Invalid qmlprofiler state change (Empty)"_L1);
break;
case AcquiringData:
// we're not supposed to receive new data while processing older data
if (state == ProcessingData)
emit q->error("Invalid qmlprofiler state change (AcquiringData)"_L1);
break;
case ProcessingData:
if (state != AcquiringData)
emit q->error("Invalid qmlprofiler state change (ProcessingData)"_L1);
break;
case Done:
if (state != ProcessingData && state != Empty)
emit q->error("Invalid qmlprofiler state change (Done)"_L1);
break;
default:
emit q->error("Trying to set unknown state in events list"_L1);
break;
}
state = newState;
// special: if we were done with an empty list, clean internal data and go back to empty
if (state == Done && isEmpty())
q->clear();
return;
}
qsizetype QQmlProfilerQtdWriter::numLoadedEventTypes() const
{
return d_func()->eventTypes.size();
}
qsizetype QQmlProfilerQtdWriter::numLoadedEvents() const
{
return d_func()->events.size();
}
QList<QQmlProfilerEventType> QQmlProfilerQtdWriter::eventTypes() const
{
return d_func()->eventTypes;
}
QList<QQmlProfilerEvent> QQmlProfilerQtdWriter::events() const
{
return d_func()->events;
}
QT_END_NAMESPACE
#include "moc_qqmlprofilerqtdwriter_p.cpp"