// 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 #include #include #include #include #include #include #include #include #include #include #include #include "qctflib_p.h" #if QT_CONFIG(cxx17_filesystem) #include #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 static QByteArray &operator<<(QByteArray &arr, T val) { static_assert(std::is_arithmetic_v); arr.append(reinterpret_cast(&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::max()); QByteArray data(filesize, Qt::Uninitialized); qsizetype size = static_cast(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