diff options
Diffstat (limited to 'src/plugins/tracing')
-rw-r--r-- | src/plugins/tracing/CMakeLists.txt | 32 | ||||
-rw-r--r-- | src/plugins/tracing/metadata_template.txt | 77 | ||||
-rw-r--r-- | src/plugins/tracing/qctflib.cpp | 447 | ||||
-rw-r--r-- | src/plugins/tracing/qctflib_p.h | 125 | ||||
-rw-r--r-- | src/plugins/tracing/qctfplugin.cpp | 63 | ||||
-rw-r--r-- | src/plugins/tracing/qctfplugin_p.h | 28 | ||||
-rw-r--r-- | src/plugins/tracing/qctfserver.cpp | 404 | ||||
-rw-r--r-- | src/plugins/tracing/qctfserver_p.h | 194 | ||||
-rw-r--r-- | src/plugins/tracing/trace.json | 3 |
9 files changed, 1373 insertions, 0 deletions
diff --git a/src/plugins/tracing/CMakeLists.txt b/src/plugins/tracing/CMakeLists.txt new file mode 100644 index 0000000000..823e11c174 --- /dev/null +++ b/src/plugins/tracing/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +function(make_includable input_file output_file) + get_filename_component(infile "${CMAKE_CURRENT_SOURCE_DIR}/${input_file}" ABSOLUTE) + set(outfile ${CMAKE_CURRENT_BINARY_DIR}/${output_file}) + file(READ ${infile} content) + set(content "R\"(${content})\"") + file(WRITE ${outfile} "${content}") +endfunction(make_includable) + +make_includable(metadata_template.txt metadata_template.h) + +qt_internal_add_plugin(QCtfTracePlugin + CLASS_NAME QCtfTracePlugin + PLUGIN_TYPE tracing + SOURCES + qctflib_p.h qctflib.cpp metadata_template.txt qctfplugin.cpp qctfplugin_p.h + qctfserver_p.h qctfserver.cpp + LIBRARIES + Qt::Core Qt::CorePrivate Qt::Network +) + +qt_internal_extend_target(QCtfTracePlugin CONDITION QT_FEATURE_zstd + LIBRARIES + WrapZSTD::WrapZSTD +) + +qt_internal_extend_target(QCtfTracePlugin CONDITION (QT_FEATURE_cxx17_filesystem) AND (GCC AND (QMAKE_GCC_MAJOR_VERSION LESS 9)) + LINK_OPTIONS + "-lstdc++fs" +) diff --git a/src/plugins/tracing/metadata_template.txt b/src/plugins/tracing/metadata_template.txt new file mode 100644 index 0000000000..5f27e79da5 --- /dev/null +++ b/src/plugins/tracing/metadata_template.txt @@ -0,0 +1,77 @@ +/* CTF 1.8 */ + +typealias integer { size = 8; align = 8; signed = false; } := uint8_t; +typealias integer { size = 16; align = 8; signed = false; } := uint16_t; +typealias integer { size = 32; align = 8; signed = false; } := uint32_t; +typealias integer { size = 64; align = 8; signed = false; } := uint64_t; +typealias integer { size = 8; align = 8; signed = true; } := int8_t; +typealias integer { size = 16; align = 8; signed = true; } := int16_t; +typealias integer { size = 32; align = 8; signed = true; } := int32_t; +typealias integer { size = 64; align = 8; signed = true; } := int64_t; +typealias integer { size = 32; align = 8; signed = true; base = 16; } := intptr32_t; +typealias integer { size = 64; align = 8; signed = true; base = 16; } := intptr64_t; +typealias floating_point { exp_dig = 8; mant_dig = 24; align = 8; byte_order = native; } := float; +typealias floating_point { exp_dig = 11; mant_dig = 53; align = 8; byte_order = native; } := double; + +typealias enum : integer { size = 8; } { + false, + true +} := Boolean; + +trace { + major = 1; + minor = 8; + uuid = "$TRACE_UUID"; + byte_order = $ENDIANNESS; + packet.header := struct { + uint32_t magic; + uint8_t uuid[16]; + uint32_t stream_id; + } align(8); +}; + +env { + domain = "ust"; + tracer_name = "qtctf"; + tracer_major = 1; + tracer_minor = 0; + architecture_bit_width = $ARC_BIT_WIDTH; + trace_name = "$SESSION_NAME"; + trace_creation_datetime = "$CREATION_TIME"; + hostname = "$HOST_NAME"; +}; + +clock { + name = "$CLOCK_NAME"; + uuid = "53836526-5a62-4de0-93c8-3a1970afab23"; + description = "$CLOCK_TYPE"; + freq = $CLOCK_FREQUENCY; + offset = $CLOCK_OFFSET; +}; + +typealias integer { + size = 64; align = 8; signed = false; + map = clock.monotonic.value; +} := uint64_clock_monotonic_t; + +struct packet_context { + uint64_clock_monotonic_t timestamp_begin; + uint64_clock_monotonic_t timestamp_end; + uint64_t content_size; + uint64_t packet_size; + uint64_t packet_seq_num; + uint64_t events_discarded; + uint32_t thread_id; + string thread_name; +} align(8); + +struct event_header { + uint32_t id; + uint64_clock_monotonic_t timestamp; +} align(8); + +stream { + id = 0; + event.header := struct event_header; + packet.context := struct packet_context; +}; diff --git a/src/plugins/tracing/qctflib.cpp b/src/plugins/tracing/qctflib.cpp new file mode 100644 index 0000000000..fe3946d27c --- /dev/null +++ b/src/plugins/tracing/qctflib.cpp @@ -0,0 +1,447 @@ +// Copyright (C) 2022 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 +#define BUILD_LIBRARY +#include <qstring.h> +#include <qthread.h> +#include <stdio.h> +#include <qjsondocument.h> +#include <qjsonarray.h> +#include <qjsonobject.h> +#include <qfileinfo.h> +#include <qrect.h> +#include <qsize.h> +#include <qmetaobject.h> +#include <qendian.h> +#include <qplatformdefs.h> +#include "qctflib_p.h" + +#if QT_CONFIG(cxx17_filesystem) +#include <filesystem> +#endif + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +Q_LOGGING_CATEGORY(lcDebugTrace, "qt.core.ctf", QtWarningMsg) + +static const size_t packetHeaderSize = 24 + 6 * 8 + 4; +static const size_t packetSize = 4096; + +static const char traceMetadataTemplate[] = +#include "metadata_template.h" +; +static const size_t traceMetadataSize = sizeof(traceMetadataTemplate); + +static inline QString allLiteral() { return QStringLiteral("all"); } +static inline QString defaultLiteral() { return QStringLiteral("default"); } + + +template <typename T> +static QByteArray &operator<<(QByteArray &arr, T val) +{ + static_assert(std::is_arithmetic_v<T>); + arr.append(reinterpret_cast<char *>(&val), sizeof(val)); + return arr; +} + +static FILE *openFile(const QString &filename, const QString &mode) +{ +#ifdef Q_OS_WINDOWS + return _wfopen(qUtf16Printable(filename), qUtf16Printable(mode)); +#else + return fopen(qPrintable(filename), qPrintable(mode)); +#endif +} + +QCtfLibImpl *QCtfLibImpl::s_instance = nullptr; + +QCtfLib *QCtfLibImpl::instance() +{ + if (!s_instance) + s_instance = new QCtfLibImpl(); + return s_instance; +} + +void QCtfLibImpl::cleanup() +{ + delete s_instance; + s_instance = nullptr; +} + +void QCtfLibImpl::handleSessionChange() +{ + m_sessionChanged = true; +} + +void QCtfLibImpl::handleStatusChange(QCtfServer::ServerStatus status) +{ + switch (status) { + case QCtfServer::Error: { + m_serverClosed = true; + } break; + default: + break; + } +} + +void QCtfLibImpl::buildMetadata() +{ + const QString mhn = QSysInfo::machineHostName(); + QString metadata = QString::fromUtf8(traceMetadataTemplate, traceMetadataSize); + metadata.replace(QStringLiteral("$TRACE_UUID"), s_TraceUuid.toString(QUuid::WithoutBraces)); + metadata.replace(QStringLiteral("$ARC_BIT_WIDTH"), QString::number(Q_PROCESSOR_WORDSIZE * 8)); + metadata.replace(QStringLiteral("$SESSION_NAME"), m_session.name); + metadata.replace(QStringLiteral("$CREATION_TIME"), m_datetime.toString(Qt::ISODate)); + metadata.replace(QStringLiteral("$HOST_NAME"), mhn); + metadata.replace(QStringLiteral("$CLOCK_FREQUENCY"), QStringLiteral("1000000000")); + metadata.replace(QStringLiteral("$CLOCK_NAME"), QStringLiteral("monotonic")); + metadata.replace(QStringLiteral("$CLOCK_TYPE"), QStringLiteral("Monotonic clock")); + metadata.replace(QStringLiteral("$CLOCK_OFFSET"), QString::number(m_datetime.toMSecsSinceEpoch() * 1000000)); + metadata.replace(QStringLiteral("$ENDIANNESS"), QSysInfo::ByteOrder == QSysInfo::BigEndian ? u"be"_s : u"le"_s); + writeMetadata(metadata, true); +} + +QCtfLibImpl::QCtfLibImpl() +{ + QString location = qEnvironmentVariable("QTRACE_LOCATION"); + if (location.isEmpty()) { + qCInfo(lcDebugTrace) << "QTRACE_LOCATION not set"; + return; + } + + if (location.startsWith(u"tcp")) { + QUrl url(location); + m_server.reset(new QCtfServer()); + m_server->setCallback(this); + m_server->setHost(url.host()); + m_server->setPort(url.port()); + m_server->startServer(); + m_streaming = true; + m_session.tracepoints.append(allLiteral()); + m_session.name = defaultLiteral(); + } else { +#if !QT_CONFIG(cxx17_filesystem) + qCWarning(lcDebugTrace) << "Unable to use filesystem"; + return; +#endif + // Check if the location is writable + if (QT_ACCESS(qPrintable(location), W_OK) != 0) { + qCWarning(lcDebugTrace) << "Unable to write to location"; + return; + } + const QString filename = location + u"/session.json"; + FILE *file = openFile(qPrintable(filename), "rb"_L1); + if (!file) { + qCWarning(lcDebugTrace) << "unable to open session file: " + << filename << ", " << qt_error_string(); + m_location = location; + m_session.tracepoints.append(allLiteral()); + m_session.name = defaultLiteral(); + } else { + QT_STATBUF stat; + if (QT_FSTAT(QT_FILENO(file), &stat) != 0) { + qCWarning(lcDebugTrace) << "Unable to stat session file, " << qt_error_string(); + return; + } + qsizetype filesize = qMin(stat.st_size, std::numeric_limits<qsizetype>::max()); + QByteArray data(filesize, Qt::Uninitialized); + qsizetype size = static_cast<qsizetype>(fread(data.data(), 1, filesize, file)); + fclose(file); + if (size != filesize) + return; + QJsonDocument json(QJsonDocument::fromJson(data)); + QJsonObject obj = json.object(); + bool valid = false; + if (!obj.isEmpty()) { + const auto it = obj.begin(); + if (it.value().isArray()) { + m_session.name = it.key(); + for (auto var : it.value().toArray()) + m_session.tracepoints.append(var.toString()); + valid = true; + } + } + if (!valid) { + qCWarning(lcDebugTrace) << "Session file is not valid"; + m_session.tracepoints.append(allLiteral()); + m_session.name = defaultLiteral(); + } + m_location = location + u"/ust"; +#if QT_CONFIG(cxx17_filesystem) + std::filesystem::create_directory(qPrintable(m_location), qPrintable(location)); +#endif + } + clearLocation(); + } + + m_session.all = m_session.tracepoints.contains(allLiteral()); + // Get datetime to when the timer was started to store the offset to epoch time for the traces + m_datetime = QDateTime::currentDateTime().toUTC(); + m_timer.start(); + if (!m_streaming) + buildMetadata(); +} + +void QCtfLibImpl::clearLocation() +{ +#if QT_CONFIG(cxx17_filesystem) + const std::filesystem::path location{qUtf16Printable(m_location)}; + for (auto const& dirEntry : std::filesystem::directory_iterator{location}) + { + const auto path = dirEntry.path(); +#if __cplusplus > 201703L + if (dirEntry.is_regular_file() + && path.filename().wstring().starts_with(std::wstring_view(L"channel_")) + && !path.has_extension()) { +#else + const auto strview = std::wstring_view(L"channel_"); + const auto sub = path.filename().wstring().substr(0, strview.length()); + if (dirEntry.is_regular_file() && sub.compare(strview) == 0 + && !path.has_extension()) { +#endif + if (!std::filesystem::remove(path)) { + qCInfo(lcDebugTrace) << "Unable to clear output location."; + break; + } + } + } +#endif +} + +void QCtfLibImpl::writeMetadata(const QString &metadata, bool overwrite) +{ + if (m_streaming) { + auto mt = metadata.toUtf8(); + mt.resize(mt.size() - 1); + m_server->bufferData(QStringLiteral("metadata"), mt, overwrite); + } else { + FILE *file = nullptr; + file = openFile(qPrintable(m_location + "/metadata"_L1), overwrite ? "w+b"_L1: "ab"_L1); + if (!file) + return; + + if (!overwrite) + fputs("\n", file); + + // data contains zero at the end, hence size - 1. + const QByteArray data = metadata.toUtf8(); + fwrite(data.data(), data.size() - 1, 1, file); + fclose(file); + } +} + +void QCtfLibImpl::writeCtfPacket(QCtfLibImpl::Channel &ch) +{ + FILE *file = nullptr; + if (!m_streaming) + file = openFile(ch.channelName, "ab"_L1); + if (file || m_streaming) { + /* Each packet contains header and context, which are defined in the metadata.txt */ + QByteArray packet; + packet << s_CtfHeaderMagic; + packet.append(QByteArrayView(s_TraceUuid.toBytes())); + + packet << quint32(0); + packet << ch.minTimestamp; + packet << ch.maxTimestamp; + packet << quint64(ch.data.size() + packetHeaderSize + ch.threadNameLength) * 8u; + packet << quint64(packetSize) * 8u; + packet << ch.seqnumber++; + packet << quint64(0); + packet << ch.threadIndex; + if (ch.threadName.size()) + packet.append(ch.threadName); + packet << (char)0; + + Q_ASSERT(ch.data.size() + packetHeaderSize + ch.threadNameLength <= packetSize); + Q_ASSERT(packet.size() == qsizetype(packetHeaderSize + ch.threadNameLength)); + if (m_streaming) { + ch.data.resize(packetSize - packet.size(), 0); + packet += ch.data; + m_server->bufferData(QString::fromLatin1(ch.channelName), packet, false); + } else { + fwrite(packet.data(), packet.size(), 1, file); + ch.data.resize(packetSize - packet.size(), 0); + fwrite(ch.data.data(), ch.data.size(), 1, file); + fclose(file); + } + } +} + +QCtfLibImpl::Channel::~Channel() +{ + impl->writeCtfPacket(*this); + impl->removeChannel(this); +} + +QCtfLibImpl::~QCtfLibImpl() +{ + if (!m_server.isNull()) + m_server->stopServer(); + qDeleteAll(m_eventPrivs); +} + +void QCtfLibImpl::removeChannel(Channel *ch) +{ + const QMutexLocker lock(&m_mutex); + m_channels.removeOne(ch); +} + +bool QCtfLibImpl::tracepointEnabled(const QCtfTracePointEvent &point) +{ + if (m_sessionChanged) { + const QMutexLocker lock(&m_mutex); + buildMetadata(); + m_session.name = m_server->sessionName(); + m_session.tracepoints = m_server->sessionTracepoints().split(';'); + m_session.all = m_session.tracepoints.contains(allLiteral()); + m_sessionChanged = false; + for (const auto &meta : m_additionalMetadata) + writeMetadata(meta->metadata); + for (auto *priv : m_eventPrivs) + writeMetadata(priv->metadata); + quint64 timestamp = m_timer.nsecsElapsed(); + for (auto *ch : m_channels) { + writeCtfPacket(*ch); + ch->data.clear(); + ch->minTimestamp = ch->maxTimestamp = timestamp; + } + } + if (m_streaming && (m_serverClosed || (!m_server->bufferOnIdle() && m_server->status() == QCtfServer::Idle))) + return false; + return m_session.all || m_session.tracepoints.contains(point.provider.provider); +} + +static QString toMetadata(const QString &provider, const QString &name, const QString &metadata, quint32 eventId) +{ +/* + generates event structure: +event { + name = provider:tracepoint_name; + id = eventId; + stream_id = 0; + loglevel = 13; + fields := struct { + metadata + }; +}; +*/ + return QStringView(u"event {\n name = \"") + provider + QLatin1Char(':') + name + u"\";\n" + + u" id = " + QString::number(eventId) + u";\n" + + u" stream_id = 0;\n loglevel = 13;\n fields := struct {\n " + + metadata + u"\n };\n};\n"; +} + +QCtfTracePointPrivate *QCtfLibImpl::initializeTracepoint(const QCtfTracePointEvent &point) +{ + QMutexLocker lock(&m_mutex); + QCtfTracePointPrivate *priv = point.d; + if (!point.d) { + if (const auto &it = m_eventPrivs.find(point.eventName); it != m_eventPrivs.end()) { + priv = *it; + } else { + priv = new QCtfTracePointPrivate(); + m_eventPrivs.insert(point.eventName, priv); + priv->id = eventId(); + priv->metadata = toMetadata(point.provider.provider, point.eventName, point.metadata, priv->id); + } + } + return priv; +} + +void QCtfLibImpl::doTracepoint(const QCtfTracePointEvent &point, const QByteArray &arr) +{ + QCtfTracePointPrivate *priv = point.d; + quint64 timestamp = 0; + QThread *thread = nullptr; + if (m_streaming && m_serverClosed) + return; + { + QMutexLocker lock(&m_mutex); + if (!priv->metadataWritten) { + priv->metadataWritten = true; + auto providerMetadata = point.provider.metadata; + while (providerMetadata) { + registerMetadata(*providerMetadata); + providerMetadata = providerMetadata->next; + } + if (m_newAdditionalMetadata.size()) { + for (const QString &name : m_newAdditionalMetadata) + writeMetadata(m_additionalMetadata[name]->metadata); + m_newAdditionalMetadata.clear(); + } + writeMetadata(priv->metadata); + } + timestamp = m_timer.nsecsElapsed(); + } + if (arr.size() != point.size) { + if (arr.size() < point.size) + return; + if (arr.size() > point.size && !point.variableSize && !point.metadata.isEmpty()) + return; + } + + thread = QThread::currentThread(); + if (thread == nullptr) + return; + + Channel &ch = m_threadData.localData(); + + if (ch.channelName[0] == 0) { + ch.impl = this; + m_channels.append(&ch); + m_threadIndices.insert(thread, m_threadIndices.size()); + sprintf(ch.channelName, "%s/channel_%d", qPrintable(m_location), m_threadIndices[thread]); + ch.minTimestamp = ch.maxTimestamp = timestamp; + ch.thread = thread; + ch.threadIndex = m_threadIndices[thread]; + ch.threadName = thread->objectName().toUtf8(); + if (ch.threadName.isEmpty()) { + const QMetaObject *obj = thread->metaObject(); + ch.threadName = QByteArray(obj->className()); + } + ch.threadNameLength = ch.threadName.size() + 1; + } + if (ch.locked) + return; + Q_ASSERT(ch.thread == thread); + ch.locked = true; + + QByteArray event; + event << priv->id << timestamp; + if (!point.metadata.isEmpty()) + event.append(arr); + + if (ch.threadNameLength + ch.data.size() + event.size() + packetHeaderSize >= packetSize) { + writeCtfPacket(ch); + ch.data = event; + ch.minTimestamp = ch.maxTimestamp = timestamp; + } else { + ch.data.append(event); + } + + ch.locked = false; + ch.maxTimestamp = timestamp; +} + +bool QCtfLibImpl::sessionEnabled() +{ + return !m_session.name.isEmpty(); +} + +int QCtfLibImpl::eventId() +{ + return m_eventId++; +} + +void QCtfLibImpl::registerMetadata(const QCtfTraceMetadata &metadata) +{ + if (m_additionalMetadata.contains(metadata.name)) + return; + + m_additionalMetadata.insert(metadata.name, &metadata); + m_newAdditionalMetadata.insert(metadata.name); +} + +QT_END_NAMESPACE diff --git a/src/plugins/tracing/qctflib_p.h b/src/plugins/tracing/qctflib_p.h new file mode 100644 index 0000000000..297d38ee50 --- /dev/null +++ b/src/plugins/tracing/qctflib_p.h @@ -0,0 +1,125 @@ +// Copyright (C) 2022 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 + +#ifndef QT_CTFLIB_H +#define QT_CTFLIB_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// +// + +#include <private/qctf_p.h> +#include "qctfplugin_p.h" +#include <qstring.h> +#include <qmutex.h> +#include <qelapsedtimer.h> +#include <qhash.h> +#include <qset.h> +#include <qthreadstorage.h> +#include <qthread.h> +#include <qloggingcategory.h> +#include "qctfserver_p.h" + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcDebugTrace) + +struct QCtfTracePointPrivate +{ + QString metadata; + quint32 id = 0; + quint32 payloadSize = 0; + bool metadataWritten = false; +}; + +class QCtfLibImpl : public QCtfLib, public QCtfServer::ServerCallback +{ + struct Session + { + QString name; + QStringList tracepoints; + bool all = false; + }; + struct Channel + { + char channelName[512]; + QByteArray data; + quint64 minTimestamp = 0; + quint64 maxTimestamp = 0; + quint64 seqnumber = 0; + QThread *thread = nullptr; + quint32 threadIndex = 0; + QByteArray threadName; + quint32 threadNameLength = 0; + bool locked = false; + QCtfLibImpl *impl = nullptr; + Channel() + { + memset(channelName, 0, sizeof(channelName)); + } + + ~Channel(); + }; + +public: + QCtfLibImpl(); + ~QCtfLibImpl(); + + bool tracepointEnabled(const QCtfTracePointEvent &point) override; + void doTracepoint(const QCtfTracePointEvent &point, const QByteArray &arr) override; + bool sessionEnabled() override; + QCtfTracePointPrivate *initializeTracepoint(const QCtfTracePointEvent &point) override; + void registerMetadata(const QCtfTraceMetadata &metadata); + int eventId(); + void shutdown(bool *) override + { + + } + + static QCtfLib *instance(); + static void cleanup(); +private: + static QCtfLibImpl *s_instance; + QHash<QString, QCtfTracePointPrivate *> m_eventPrivs; + void removeChannel(Channel *ch); + void updateMetadata(const QCtfTracePointEvent &point); + void writeMetadata(const QString &metadata, bool overwrite = false); + void clearLocation(); + void handleSessionChange() override; + void handleStatusChange(QCtfServer::ServerStatus status) override; + void writeCtfPacket(Channel &ch); + void buildMetadata(); + + static constexpr QUuid s_TraceUuid = QUuid(0x3e589c95, 0xed11, 0xc159, 0x42, 0x02, 0x6a, 0x9b, 0x02, 0x00, 0x12, 0xac); + static constexpr quint32 s_CtfHeaderMagic = 0xC1FC1FC1; + + QMutex m_mutex; + QElapsedTimer m_timer; + QString m_metadata; + QString m_location; + Session m_session; + QHash<QThread*, quint32> m_threadIndices; + QThreadStorage<Channel> m_threadData; + QList<Channel *> m_channels; + QHash<QString, const QCtfTraceMetadata *> m_additionalMetadata; + QSet<QString> m_newAdditionalMetadata; + QDateTime m_datetime; + int m_eventId = 0; + bool m_streaming = false; + std::atomic_bool m_sessionChanged = false; + std::atomic_bool m_serverClosed = false; + QScopedPointer<QCtfServer> m_server; + friend struct Channel; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/tracing/qctfplugin.cpp b/src/plugins/tracing/qctfplugin.cpp new file mode 100644 index 0000000000..93e508e199 --- /dev/null +++ b/src/plugins/tracing/qctfplugin.cpp @@ -0,0 +1,63 @@ +// Copyright (C) 2022 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 + +#define BUILD_LIBRARY +#include <qstring.h> +#include "qctfplugin_p.h" +#include "qctflib_p.h" + +QT_BEGIN_NAMESPACE + +class QCtfTracePlugin : public QCtfLib +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QCtfLib" FILE "trace.json") + Q_INTERFACES(QCtfLib) + +public: + QCtfTracePlugin() + { + + } + ~QCtfTracePlugin() + { + m_cleanup = true; + *m_shutdown = true; + QCtfLibImpl::cleanup(); + } + void shutdown(bool *shutdown) override + { + m_shutdown = shutdown; + } + bool tracepointEnabled(const QCtfTracePointEvent &point) override + { + if (m_cleanup) + return false; + return QCtfLibImpl::instance()->tracepointEnabled(point); + } + void doTracepoint(const QCtfTracePointEvent &point, const QByteArray &arr) override + { + if (m_cleanup) + return; + QCtfLibImpl::instance()->doTracepoint(point, arr); + } + bool sessionEnabled() override + { + if (m_cleanup) + return false; + return QCtfLibImpl::instance()->sessionEnabled(); + } + QCtfTracePointPrivate *initializeTracepoint(const QCtfTracePointEvent &point) override + { + if (m_cleanup) + return nullptr; + return QCtfLibImpl::instance()->initializeTracepoint(point); + } +private: + bool m_cleanup = false; + bool *m_shutdown = nullptr; +}; + +QT_END_NAMESPACE + +#include "qctfplugin.moc" diff --git a/src/plugins/tracing/qctfplugin_p.h b/src/plugins/tracing/qctfplugin_p.h new file mode 100644 index 0000000000..987c4d925f --- /dev/null +++ b/src/plugins/tracing/qctfplugin_p.h @@ -0,0 +1,28 @@ +// Copyright (C) 2022 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 + +#ifndef Q_CTFPLUGIN_P_H +#define Q_CTFPLUGIN_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// +// + +#include <private/qctf_p.h> +#include <qplugin.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_INTERFACE(QCtfLib, "org.qt-project.Qt.QCtfLib") + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/tracing/qctfserver.cpp b/src/plugins/tracing/qctfserver.cpp new file mode 100644 index 0000000000..d97c345e11 --- /dev/null +++ b/src/plugins/tracing/qctfserver.cpp @@ -0,0 +1,404 @@ +// Copyright (C) 2023 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 + +#include <qloggingcategory.h> +#include "qctfserver_p.h" + +#if QT_CONFIG(zstd) +#include <zstd.h> +#endif + +using namespace Qt::Literals::StringLiterals; + +Q_LOGGING_CATEGORY(lcCtfInfoTrace, "qt.core.ctfserver", QtWarningMsg) + +#if QT_CONFIG(zstd) +static QByteArray zstdCompress(ZSTD_CCtx *&context, const QByteArray &data, int compression) +{ + if (context == nullptr) + context = ZSTD_createCCtx(); + qsizetype size = data.size(); + size = ZSTD_COMPRESSBOUND(size); + QByteArray compressed(size, Qt::Uninitialized); + char *dst = compressed.data(); + size_t n = ZSTD_compressCCtx(context, dst, size, + data.constData(), data.size(), + compression); + if (ZSTD_isError(n)) { + qCWarning(lcCtfInfoTrace) << "Compression with zstd failed: " << QString::fromUtf8(ZSTD_getErrorName(n)); + return {}; + } + compressed.truncate(n); + return compressed; +} +#endif + +QCtfServer::QCtfServer(QObject *parent) + : QThread(parent) +{ + m_keySet << "cliendId"_L1 + << "clientVersion"_L1 + << "sessionName"_L1 + << "sessionTracepoints"_L1 + << "flags"_L1 + << "bufferSize"_L1 + << "compressionScheme"_L1; +} + +QCtfServer::~QCtfServer() +{ +#if QT_CONFIG(zstd) + ZSTD_freeCCtx(m_zstdCCtx); +#endif +} + +void QCtfServer::setHost(const QString &address) +{ + m_address = address; +} + +void QCtfServer::setPort(int port) +{ + m_port = port; +} + +void QCtfServer::setCallback(ServerCallback *cb) +{ + m_cb = cb; +} + +QString QCtfServer::sessionName() const +{ + return m_req.sessionName; +} + +QString QCtfServer::sessionTracepoints() const +{ + return m_req.sessionTracepoints; +} + +bool QCtfServer::bufferOnIdle() const +{ + return m_bufferOnIdle; +} + +QCtfServer::ServerStatus QCtfServer::status() const +{ + return m_status; +} + +void QCtfServer::setStatusAndNotify(ServerStatus status) +{ + m_status = status; + m_cb->handleStatusChange(status); +} + +void QCtfServer::bytesWritten(qint64 size) +{ + m_writtenSize += size; + if (m_writtenSize >= m_waitWriteSize && m_eventLoop) + m_eventLoop->exit(); +} + +void QCtfServer::initWrite() +{ + m_waitWriteSize = 0; + m_writtenSize = 0; +} + +bool QCtfServer::waitSocket() +{ + if (m_eventLoop) + m_eventLoop->exec(); + return m_socket->state() == QTcpSocket::ConnectedState; +} + +void QCtfServer::handleString(QCborStreamReader &cbor) +{ + const auto readString = [](QCborStreamReader &cbor) -> QString { + QString result; + auto r = cbor.readString(); + while (r.status == QCborStreamReader::Ok) { + result += r.data; + r = cbor.readString(); + } + + if (r.status == QCborStreamReader::Error) { + // handle error condition + result.clear(); + } + return result; + }; + do { + if (m_currentKey.isEmpty()) { + m_currentKey = readString(cbor); + } else { + switch (m_keySet.indexOf(m_currentKey)) { + case RequestSessionName: + m_req.sessionName = readString(cbor); + break; + case RequestSessionTracepoints: + m_req.sessionTracepoints = readString(cbor); + break; + case RequestCompressionScheme: + m_requestedCompressionScheme = readString(cbor); + break; + default: + // handle error + break; + } + m_currentKey.clear(); + } + if (cbor.lastError() == QCborError::EndOfFile) { + if (!waitSocket()) + return; + cbor.reparse(); + } + } while (cbor.lastError() == QCborError::EndOfFile); +} + +void QCtfServer::handleFixedWidth(QCborStreamReader &cbor) +{ + switch (m_keySet.indexOf(m_currentKey)) { + case RequestClientId: + if (!cbor.isUnsignedInteger()) + return; + m_req.clientId = cbor.toUnsignedInteger(); + break; + case RequestClientVersion: + if (!cbor.isUnsignedInteger()) + return; + m_req.clientVersion = cbor.toUnsignedInteger(); + break; + case RequestFlags: + if (!cbor.isUnsignedInteger()) + return; + m_req.flags = cbor.toUnsignedInteger(); + break; + case RequestBufferSize: + if (!cbor.isUnsignedInteger()) + return; + m_req.bufferSize = cbor.toUnsignedInteger(); + break; + default: + // handle error + break; + } + m_currentKey.clear(); +} + +void QCtfServer::readCbor(QCborStreamReader &cbor) +{ + switch (cbor.type()) { + case QCborStreamReader::UnsignedInteger: + case QCborStreamReader::NegativeInteger: + case QCborStreamReader::SimpleType: + case QCborStreamReader::Float16: + case QCborStreamReader::Float: + case QCborStreamReader::Double: + handleFixedWidth(cbor); + cbor.next(); + break; + case QCborStreamReader::ByteArray: + case QCborStreamReader::String: + handleString(cbor); + break; + case QCborStreamReader::Array: + case QCborStreamReader::Map: + cbor.enterContainer(); + while (cbor.lastError() == QCborError::NoError && cbor.hasNext()) + readCbor(cbor); + if (cbor.lastError() == QCborError::NoError) + cbor.leaveContainer(); + default: + break; + } +} + +void QCtfServer::writePacket(TracePacket &packet, QCborStreamWriter &cbor) +{ + cbor.startMap(4); + cbor.append("magic"_L1); + cbor.append(packet.PacketMagicNumber); + cbor.append("name"_L1); + cbor.append(QString::fromUtf8(packet.stream_name)); + cbor.append("flags"_L1); + cbor.append(packet.flags); + + cbor.append("data"_L1); + if (m_compression > 0) { + QByteArray compressed; +#if QT_CONFIG(zstd) + if (m_requestedCompressionScheme == QStringLiteral("zstd")) + compressed = zstdCompress(m_zstdCCtx, packet.stream_data, m_compression); + else +#endif + compressed = qCompress(packet.stream_data, m_compression); + + cbor.append(compressed); + } else { + cbor.append(packet.stream_data); + } + + cbor.endMap(); +} + +bool QCtfServer::recognizedCompressionScheme() const +{ + if (m_requestedCompressionScheme.isEmpty()) + return true; +#if QT_CONFIG(zstd) + if (m_requestedCompressionScheme == QStringLiteral("zstd")) + return true; +#endif + if (m_requestedCompressionScheme == QStringLiteral("zlib")) + return true; + return false; +} + +void QCtfServer::run() +{ + m_server = new QTcpServer(); + QHostAddress addr; + if (m_address.isEmpty()) + addr = QHostAddress(QHostAddress::Any); + else + addr = QHostAddress(m_address); + + qCInfo(lcCtfInfoTrace) << "Starting CTF server: " << m_address << ", port: " << m_port; + + while (m_stopping == 0) { + if (!m_server->isListening()) { + if (!m_server->listen(addr, m_port)) { + qCInfo(lcCtfInfoTrace) << "Unable to start server"; + m_stopping = 1; + setStatusAndNotify(Error); + } + } + setStatusAndNotify(Idle); + if (m_server->waitForNewConnection(-1)) { + qCInfo(lcCtfInfoTrace) << "client connection"; + m_eventLoop = new QEventLoop(); + m_socket = m_server->nextPendingConnection(); + + QObject::connect(m_socket, &QTcpSocket::readyRead, [&](){ + if (m_eventLoop) m_eventLoop->exit(); + }); + QObject::connect(m_socket, &QTcpSocket::bytesWritten, this, &QCtfServer::bytesWritten); + QObject::connect(m_socket, &QTcpSocket::disconnected, [&](){ + if (m_eventLoop) m_eventLoop->exit(); + }); + + m_server->close(); // Do not wait for more connections + setStatusAndNotify(Connected); + + if (waitSocket()) + { + QCborStreamReader cbor(m_socket); + + m_req = {}; + while (cbor.hasNext() && cbor.lastError() == QCborError::NoError) + readCbor(cbor); + + if (!m_req.isValid()) { + qCInfo(lcCtfInfoTrace) << "Invalid trace request."; + m_socket->close(); + } else { + m_compression = m_req.flags & CompressionMask; +#if QT_CONFIG(zstd) + m_compression = qMin(m_compression, ZSTD_maxCLevel()); +#else + m_compression = qMin(m_compression, 9); +#endif + m_bufferOnIdle = !(m_req.flags & DontBufferOnIdle); + + m_maxPackets = qMax(m_req.bufferSize / TracePacket::PacketSize, 16u); + + if (!recognizedCompressionScheme()) { + qCWarning(lcCtfInfoTrace) << "Client requested unrecognized compression scheme: " << m_requestedCompressionScheme; + m_requestedCompressionScheme.clear(); + m_compression = 0; + } + + qCInfo(lcCtfInfoTrace) << "request received: " << m_req.sessionName << ", " << m_req.sessionTracepoints; + + m_cb->handleSessionChange(); + { + TraceResponse resp; + resp.serverId = ServerId; + resp.serverVersion = 1; + resp.serverName = QStringLiteral("Ctf Server"); + + QCborStreamWriter cbor(m_socket); + cbor.startMap(m_compression ? 4 : 3); + cbor.append("serverId"_L1); + cbor.append(resp.serverId); + cbor.append("serverVersion"_L1); + cbor.append(resp.serverVersion); + cbor.append("serverName"_L1); + cbor.append(resp.serverName); + if (m_compression) { + cbor.append("compressionScheme"_L1); + cbor.append(m_requestedCompressionScheme); + } + cbor.endMap(); + } + + qCInfo(lcCtfInfoTrace) << "response sent, sending data"; + if (waitSocket()) { + while (m_socket->state() == QTcpSocket::ConnectedState) { + QList<TracePacket> packets; + { + QMutexLocker lock(&m_mutex); + while (m_packets.size() == 0) + m_bufferHasData.wait(&m_mutex); + packets = std::exchange(m_packets, {}); + } + + { + QCborStreamWriter cbor(m_socket); + for (TracePacket &packet : packets) { + writePacket(packet, cbor); + if (!waitSocket()) + break; + } + } + qCInfo(lcCtfInfoTrace) << packets.size() << " packets written"; + } + } + + qCInfo(lcCtfInfoTrace) << "client connection closed"; + } + } + delete m_eventLoop; + m_eventLoop = nullptr; + } else { + qCInfo(lcCtfInfoTrace) << "error: " << m_server->errorString(); + m_stopping = 1; + setStatusAndNotify(Error); + } + } +} + +void QCtfServer::startServer() +{ + start(); +} +void QCtfServer::stopServer() +{ + this->m_stopping = 1; + wait(); +} + +void QCtfServer::bufferData(const QString &stream, const QByteArray &data, quint32 flags) +{ + QMutexLocker lock(&m_mutex); + TracePacket packet; + packet.stream_name = stream.toUtf8(); + packet.stream_data = data; + packet.flags = flags; + m_packets.append(packet); + if (m_packets.size() > m_maxPackets) + m_packets.pop_front(); + m_bufferHasData.wakeOne(); +} diff --git a/src/plugins/tracing/qctfserver_p.h b/src/plugins/tracing/qctfserver_p.h new file mode 100644 index 0000000000..3660df7f09 --- /dev/null +++ b/src/plugins/tracing/qctfserver_p.h @@ -0,0 +1,194 @@ +// Copyright (C) 2023 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 + +#ifndef QT_CTFSERVER_H +#define QT_CTFSERVER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// +// + +#include <qbytearray.h> +#include <qdatastream.h> +#include <qthread.h> +#include <qmutex.h> +#include <qwaitcondition.h> +#include <qeventloop.h> +#include <QtNetwork/qtcpserver.h> +#include <QtNetwork/qtcpsocket.h> +#include <qcborstreamreader.h> +#include <qcborstreamwriter.h> +#include <qlist.h> + +typedef struct ZSTD_CCtx_s ZSTD_CCtx; + +QT_BEGIN_NAMESPACE + +class QCtfServer; +struct TracePacket +{ + static constexpr quint32 PacketMagicNumber = 0x100924da; + static constexpr quint32 PacketSize = 4096 + 9; + QByteArray stream_name; + QByteArray stream_data; + quint32 flags = 0; + + TracePacket() = default; + + TracePacket(const TracePacket &t) + { + stream_name = t.stream_name; + stream_data = t.stream_data; + flags = t.flags; + } + TracePacket &operator = (const TracePacket &t) + { + stream_name = t.stream_name; + stream_data = t.stream_data; + flags = t.flags; + return *this; + } + TracePacket(TracePacket &&t) + { + stream_name = std::move(t.stream_name); + stream_data = std::move(t.stream_data); + flags = t.flags; + } + TracePacket &operator = (TracePacket &&t) + { + stream_name = std::move(t.stream_name); + stream_data = std::move(t.stream_data); + flags = t.flags; + return *this; + } +}; + +auto constexpr operator""_MB(quint64 s) -> quint64 +{ + return s * 1024ul * 1024ul; +} + +struct TraceRequest +{ + quint32 clientId; + quint32 clientVersion; + quint32 flags; + quint32 bufferSize; + QString sessionName; + QString sessionTracepoints; + + static constexpr quint32 MaxBufferSize = 1024_MB; + + bool isValid() const + { + if (clientId != 0 && clientVersion != 0 && !sessionName.isEmpty() + && !sessionTracepoints.isEmpty() && bufferSize < MaxBufferSize) + return true; + return false; + } +}; + +struct TraceResponse +{ + quint32 serverId; + quint32 serverVersion; + QString serverName; +}; + +class QCtfServer : public QThread +{ + Q_OBJECT +public: + enum ServerStatus + { + Uninitialized, + Idle, + Connected, + Error, + }; + enum ServerFlags + { + CompressionMask = 255, + DontBufferOnIdle = 256, // not set -> the server is buffering even without client connection + // set -> the server is buffering only when client is connected + }; + enum RequestIds + { + RequestClientId = 0, + RequestClientVersion, + RequestSessionName, + RequestSessionTracepoints, + RequestFlags, + RequestBufferSize, + RequestCompressionScheme, + }; + + struct ServerCallback + { + virtual void handleSessionChange() = 0; + virtual void handleStatusChange(ServerStatus status) = 0; + }; + QCtfServer(QObject *parent = nullptr); + ~QCtfServer(); + void setCallback(ServerCallback *cb); + void setHost(const QString &address); + void setPort(int port); + void run() override; + void startServer(); + void stopServer(); + void bufferData(const QString &stream, const QByteArray &data, quint32 flags); + QString sessionName() const; + QString sessionTracepoints() const; + bool bufferOnIdle() const; + ServerStatus status() const; +private: + + void initWrite(); + void bytesWritten(qint64 size); + bool waitSocket(); + void readCbor(QCborStreamReader &cbor); + void handleString(QCborStreamReader &cbor); + void handleFixedWidth(QCborStreamReader &cbor); + bool recognizedCompressionScheme() const; + void setStatusAndNotify(ServerStatus status); + void writePacket(TracePacket &packet, QCborStreamWriter &cbor); + + QMutex m_mutex; + QWaitCondition m_bufferHasData; + QList<TracePacket> m_packets; + QString m_address; + QTcpServer *m_server = nullptr; + QTcpSocket *m_socket = nullptr; + QEventLoop *m_eventLoop = nullptr; + QList<QString> m_keySet; + TraceRequest m_req; + ServerCallback *m_cb = nullptr; + ServerStatus m_status = Uninitialized; + qint64 m_waitWriteSize = 0; + qint64 m_writtenSize = 0; + int m_port; + int m_compression = 0; + int m_maxPackets = DefaultMaxPackets; + QAtomicInt m_stopping; + bool m_bufferOnIdle = true; + QString m_currentKey; + QString m_requestedCompressionScheme; +#if QT_CONFIG(zstd) + ZSTD_CCtx *m_zstdCCtx = nullptr; +#endif + + static constexpr quint32 ServerId = 1; + static constexpr quint32 DefaultMaxPackets = 256; // 1 MB +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/tracing/trace.json b/src/plugins/tracing/trace.json new file mode 100644 index 0000000000..1b991122d4 --- /dev/null +++ b/src/plugins/tracing/trace.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "CTF" ] +} |