summaryrefslogtreecommitdiffstats
path: root/src/plugins/tracing
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/tracing')
-rw-r--r--src/plugins/tracing/CMakeLists.txt32
-rw-r--r--src/plugins/tracing/metadata_template.txt77
-rw-r--r--src/plugins/tracing/qctflib.cpp447
-rw-r--r--src/plugins/tracing/qctflib_p.h125
-rw-r--r--src/plugins/tracing/qctfplugin.cpp63
-rw-r--r--src/plugins/tracing/qctfplugin_p.h28
-rw-r--r--src/plugins/tracing/qctfserver.cpp404
-rw-r--r--src/plugins/tracing/qctfserver_p.h194
-rw-r--r--src/plugins/tracing/trace.json3
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" ]
+}