blob: bae7d4c4d07c2d59fa0d9bf53bf28a7b227051e8 [file] [log] [blame]
//===-- xray_fdr_logging.cc ------------------------------------*- C++ -*-===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
//
// This file is a part of XRay, a dynamic runtime instruementation system.
//
// Here we implement the Flight Data Recorder mode for XRay, where we use
// compact structures to store records in memory as well as when writing out the
// data to files.
//
//===----------------------------------------------------------------------===//
#include "xray_fdr_logging.h"
#include <algorithm>
#include <bitset>
#include <cassert>
#include <cstring>
#include <memory>
#include <sys/syscall.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include <unordered_map>
#include "sanitizer_common/sanitizer_common.h"
#include "xray/xray_interface.h"
#include "xray/xray_records.h"
#include "xray_buffer_queue.h"
#include "xray_defs.h"
#include "xray_flags.h"
#include "xray_tsc.h"
#include "xray_utils.h"
namespace __xray {
// Global BufferQueue.
std::shared_ptr<BufferQueue> BQ;
std::atomic<XRayLogInitStatus> LoggingStatus{
XRayLogInitStatus::XRAY_LOG_UNINITIALIZED};
std::atomic<XRayLogFlushStatus> LogFlushStatus{
XRayLogFlushStatus::XRAY_LOG_NOT_FLUSHING};
std::unique_ptr<FDRLoggingOptions> FDROptions;
XRayLogInitStatus fdrLoggingInit(std::size_t BufferSize, std::size_t BufferMax,
void *Options,
size_t OptionsSize) XRAY_NEVER_INSTRUMENT {
assert(OptionsSize == sizeof(FDRLoggingOptions));
XRayLogInitStatus CurrentStatus = XRayLogInitStatus::XRAY_LOG_UNINITIALIZED;
if (!LoggingStatus.compare_exchange_strong(
CurrentStatus, XRayLogInitStatus::XRAY_LOG_INITIALIZING,
std::memory_order_release, std::memory_order_relaxed))
return CurrentStatus;
FDROptions.reset(new FDRLoggingOptions());
*FDROptions = *reinterpret_cast<FDRLoggingOptions *>(Options);
if (FDROptions->ReportErrors)
SetPrintfAndReportCallback(printToStdErr);
bool Success = false;
BQ = std::make_shared<BufferQueue>(BufferSize, BufferMax, Success);
if (!Success) {
Report("BufferQueue init failed.\n");
return XRayLogInitStatus::XRAY_LOG_UNINITIALIZED;
}
// Install the actual handleArg0 handler after initialising the buffers.
__xray_set_handler(fdrLoggingHandleArg0);
LoggingStatus.store(XRayLogInitStatus::XRAY_LOG_INITIALIZED,
std::memory_order_release);
return XRayLogInitStatus::XRAY_LOG_INITIALIZED;
}
// Must finalize before flushing.
XRayLogFlushStatus fdrLoggingFlush() XRAY_NEVER_INSTRUMENT {
if (LoggingStatus.load(std::memory_order_acquire) !=
XRayLogInitStatus::XRAY_LOG_FINALIZED)
return XRayLogFlushStatus::XRAY_LOG_NOT_FLUSHING;
XRayLogFlushStatus Result = XRayLogFlushStatus::XRAY_LOG_NOT_FLUSHING;
if (!LogFlushStatus.compare_exchange_strong(
Result, XRayLogFlushStatus::XRAY_LOG_FLUSHING,
std::memory_order_release, std::memory_order_relaxed))
return Result;
// Make a copy of the BufferQueue pointer to prevent other threads that may be
// resetting it from blowing away the queue prematurely while we're dealing
// with it.
auto LocalBQ = BQ;
// We write out the file in the following format:
//
// 1) We write down the XRay file header with version 1, type FDR_LOG.
// 2) Then we use the 'apply' member of the BufferQueue that's live, to
// ensure that at this point in time we write down the buffers that have
// been released (and marked "used") -- we dump the full buffer for now
// (fixed-sized) and let the tools reading the buffers deal with the data
// afterwards.
//
int Fd = FDROptions->Fd;
if (Fd == -1)
Fd = getLogFD();
if (Fd == -1) {
auto Result = XRayLogFlushStatus::XRAY_LOG_NOT_FLUSHING;
LogFlushStatus.store(Result, std::memory_order_release);
return Result;
}
XRayFileHeader Header;
Header.Version = 1;
Header.Type = FileTypes::FDR_LOG;
Header.CycleFrequency = getTSCFrequency();
// FIXME: Actually check whether we have 'constant_tsc' and 'nonstop_tsc'
// before setting the values in the header.
Header.ConstantTSC = 1;
Header.NonstopTSC = 1;
clock_gettime(CLOCK_REALTIME, &Header.TS);
retryingWriteAll(Fd, reinterpret_cast<char *>(&Header),
reinterpret_cast<char *>(&Header) + sizeof(Header));
LocalBQ->apply([&](const BufferQueue::Buffer &B) {
retryingWriteAll(Fd, reinterpret_cast<char *>(B.Buffer),
reinterpret_cast<char *>(B.Buffer) + B.Size);
});
LogFlushStatus.store(XRayLogFlushStatus::XRAY_LOG_FLUSHED,
std::memory_order_release);
return XRayLogFlushStatus::XRAY_LOG_FLUSHED;
}
XRayLogInitStatus fdrLoggingFinalize() XRAY_NEVER_INSTRUMENT {
XRayLogInitStatus CurrentStatus = XRayLogInitStatus::XRAY_LOG_INITIALIZED;
if (!LoggingStatus.compare_exchange_strong(
CurrentStatus, XRayLogInitStatus::XRAY_LOG_FINALIZING,
std::memory_order_release, std::memory_order_relaxed))
return CurrentStatus;
// Do special things to make the log finalize itself, and not allow any more
// operations to be performed until re-initialized.
BQ->finalize();
LoggingStatus.store(XRayLogInitStatus::XRAY_LOG_FINALIZED,
std::memory_order_release);
return XRayLogInitStatus::XRAY_LOG_FINALIZED;
}
XRayLogInitStatus fdrLoggingReset() XRAY_NEVER_INSTRUMENT {
XRayLogInitStatus CurrentStatus = XRayLogInitStatus::XRAY_LOG_FINALIZED;
if (!LoggingStatus.compare_exchange_strong(
CurrentStatus, XRayLogInitStatus::XRAY_LOG_UNINITIALIZED,
std::memory_order_release, std::memory_order_relaxed))
return CurrentStatus;
// Release the in-memory buffer queue.
BQ.reset();
// Spin until the flushing status is flushed.
XRayLogFlushStatus CurrentFlushingStatus =
XRayLogFlushStatus::XRAY_LOG_FLUSHED;
while (!LogFlushStatus.compare_exchange_weak(
CurrentFlushingStatus, XRayLogFlushStatus::XRAY_LOG_NOT_FLUSHING,
std::memory_order_release, std::memory_order_relaxed)) {
if (CurrentFlushingStatus == XRayLogFlushStatus::XRAY_LOG_NOT_FLUSHING)
break;
CurrentFlushingStatus = XRayLogFlushStatus::XRAY_LOG_FLUSHED;
}
// At this point, we know that the status is flushed, and that we can assume
return XRayLogInitStatus::XRAY_LOG_UNINITIALIZED;
}
namespace {
thread_local BufferQueue::Buffer Buffer;
thread_local char *RecordPtr = nullptr;
void setupNewBuffer(const BufferQueue::Buffer &Buffer) XRAY_NEVER_INSTRUMENT {
RecordPtr = static_cast<char *>(Buffer.Buffer);
static constexpr int InitRecordsCount = 2;
std::aligned_storage<sizeof(MetadataRecord)>::type Records[InitRecordsCount];
{
// Write out a MetadataRecord to signify that this is the start of a new
// buffer, associated with a particular thread, with a new CPU. For the
// data, we have 15 bytes to squeeze as much information as we can. At this
// point we only write down the following bytes:
// - Thread ID (pid_t, 4 bytes)
auto &NewBuffer = *reinterpret_cast<MetadataRecord *>(&Records[0]);
NewBuffer.Type = uint8_t(RecordType::Metadata);
NewBuffer.RecordKind = uint8_t(MetadataRecord::RecordKinds::NewBuffer);
pid_t Tid = syscall(SYS_gettid);
std::memcpy(&NewBuffer.Data, &Tid, sizeof(pid_t));
}
// Also write the WalltimeMarker record.
{
static_assert(sizeof(time_t) <= 8, "time_t needs to be at most 8 bytes");
auto &WalltimeMarker = *reinterpret_cast<MetadataRecord *>(&Records[1]);
WalltimeMarker.Type = uint8_t(RecordType::Metadata);
WalltimeMarker.RecordKind =
uint8_t(MetadataRecord::RecordKinds::WalltimeMarker);
timespec TS{0, 0};
clock_gettime(CLOCK_MONOTONIC, &TS);
// We only really need microsecond precision here, and enforce across
// platforms that we need 64-bit seconds and 32-bit microseconds encoded in
// the Metadata record.
int32_t Micros = TS.tv_nsec / 1000;
int64_t Seconds = TS.tv_sec;
std::memcpy(WalltimeMarker.Data, &Seconds, sizeof(Seconds));
std::memcpy(WalltimeMarker.Data + sizeof(Seconds), &Micros, sizeof(Micros));
}
std::memcpy(RecordPtr, Records, sizeof(MetadataRecord) * InitRecordsCount);
RecordPtr += sizeof(MetadataRecord) * InitRecordsCount;
}
void writeNewCPUIdMetadata(uint16_t CPU, uint64_t TSC) XRAY_NEVER_INSTRUMENT {
MetadataRecord NewCPUId;
NewCPUId.Type = uint8_t(RecordType::Metadata);
NewCPUId.RecordKind = uint8_t(MetadataRecord::RecordKinds::NewCPUId);
// The data for the New CPU will contain the following bytes:
// - CPU ID (uint16_t, 2 bytes)
// - Full TSC (uint64_t, 8 bytes)
// Total = 12 bytes.
std::memcpy(&NewCPUId.Data, &CPU, sizeof(CPU));
std::memcpy(&NewCPUId.Data[sizeof(CPU)], &TSC, sizeof(TSC));
std::memcpy(RecordPtr, &NewCPUId, sizeof(MetadataRecord));
RecordPtr += sizeof(MetadataRecord);
}
void writeEOBMetadata() XRAY_NEVER_INSTRUMENT {
MetadataRecord EOBMeta;
EOBMeta.Type = uint8_t(RecordType::Metadata);
EOBMeta.RecordKind = uint8_t(MetadataRecord::RecordKinds::EndOfBuffer);
// For now we don't write any bytes into the Data field.
std::memcpy(RecordPtr, &EOBMeta, sizeof(MetadataRecord));
RecordPtr += sizeof(MetadataRecord);
}
void writeTSCWrapMetadata(uint64_t TSC) XRAY_NEVER_INSTRUMENT {
MetadataRecord TSCWrap;
TSCWrap.Type = uint8_t(RecordType::Metadata);
TSCWrap.RecordKind = uint8_t(MetadataRecord::RecordKinds::TSCWrap);
// The data for the TSCWrap record contains the following bytes:
// - Full TSC (uint64_t, 8 bytes)
// Total = 8 bytes.
std::memcpy(&TSCWrap.Data, &TSC, sizeof(TSC));
std::memcpy(RecordPtr, &TSCWrap, sizeof(MetadataRecord));
RecordPtr += sizeof(MetadataRecord);
}
constexpr auto MetadataRecSize = sizeof(MetadataRecord);
constexpr auto FunctionRecSize = sizeof(FunctionRecord);
class ThreadExitBufferCleanup {
std::weak_ptr<BufferQueue> Buffers;
BufferQueue::Buffer &Buffer;
public:
explicit ThreadExitBufferCleanup(std::weak_ptr<BufferQueue> BQ,
BufferQueue::Buffer &Buffer)
XRAY_NEVER_INSTRUMENT : Buffers(BQ),
Buffer(Buffer) {}
~ThreadExitBufferCleanup() noexcept XRAY_NEVER_INSTRUMENT {
if (RecordPtr == nullptr)
return;
// We make sure that upon exit, a thread will write out the EOB
// MetadataRecord in the thread-local log, and also release the buffer to
// the queue.
assert((RecordPtr + MetadataRecSize) - static_cast<char *>(Buffer.Buffer) >=
static_cast<ptrdiff_t>(MetadataRecSize));
if (auto BQ = Buffers.lock()) {
writeEOBMetadata();
if (auto EC = BQ->releaseBuffer(Buffer))
Report("Failed to release buffer at %p; error=%s\n", Buffer.Buffer,
EC.message().c_str());
return;
}
}
};
class RecursionGuard {
bool &Running;
const bool Valid;
public:
explicit RecursionGuard(bool &R) : Running(R), Valid(!R) {
if (Valid)
Running = true;
}
RecursionGuard(const RecursionGuard &) = delete;
RecursionGuard(RecursionGuard &&) = delete;
RecursionGuard &operator=(const RecursionGuard &) = delete;
RecursionGuard &operator=(RecursionGuard &&) = delete;
explicit operator bool() const { return Valid; }
~RecursionGuard() noexcept {
if (Valid)
Running = false;
}
};
inline bool loggingInitialized() {
return LoggingStatus.load(std::memory_order_acquire) ==
XRayLogInitStatus::XRAY_LOG_INITIALIZED;
}
} // namespace
void fdrLoggingHandleArg0(int32_t FuncId,
XRayEntryType Entry) XRAY_NEVER_INSTRUMENT {
// We want to get the TSC as early as possible, so that we can check whether
// we've seen this CPU before. We also do it before we load anything else, to
// allow for forward progress with the scheduling.
unsigned char CPU;
uint64_t TSC = __xray::readTSC(CPU);
// Bail out right away if logging is not initialized yet.
if (LoggingStatus.load(std::memory_order_acquire) !=
XRayLogInitStatus::XRAY_LOG_INITIALIZED)
return;
// We use a thread_local variable to keep track of which CPUs we've already
// run, and the TSC times for these CPUs. This allows us to stop repeating the
// CPU field in the function records.
//
// We assume that we'll support only 65536 CPUs for x86_64.
thread_local uint16_t CurrentCPU = std::numeric_limits<uint16_t>::max();
thread_local uint64_t LastTSC = 0;
// Make sure a thread that's ever called handleArg0 has a thread-local
// live reference to the buffer queue for this particular instance of
// FDRLogging, and that we're going to clean it up when the thread exits.
thread_local auto LocalBQ = BQ;
thread_local ThreadExitBufferCleanup Cleanup(LocalBQ, Buffer);
// Prevent signal handler recursion, so in case we're already in a log writing
// mode and the signal handler comes in (and is also instrumented) then we
// don't want to be clobbering potentially partial writes already happening in
// the thread. We use a simple thread_local latch to only allow one on-going
// handleArg0 to happen at any given time.
thread_local bool Running = false;
RecursionGuard Guard{Running};
if (!Guard) {
assert(Running == true && "RecursionGuard is buggy!");
return;
}
if (!loggingInitialized() || LocalBQ->finalizing()) {
writeEOBMetadata();
if (auto EC = BQ->releaseBuffer(Buffer)) {
Report("Failed to release buffer at %p; error=%s\n", Buffer.Buffer,
EC.message().c_str());
return;
}
RecordPtr = nullptr;
}
if (Buffer.Buffer == nullptr) {
if (auto EC = LocalBQ->getBuffer(Buffer)) {
auto LS = LoggingStatus.load(std::memory_order_acquire);
if (LS != XRayLogInitStatus::XRAY_LOG_FINALIZING &&
LS != XRayLogInitStatus::XRAY_LOG_FINALIZED)
Report("Failed to acquire a buffer; error=%s\n", EC.message().c_str());
return;
}
setupNewBuffer(Buffer);
}
if (CurrentCPU == std::numeric_limits<uint16_t>::max()) {
// This means this is the first CPU this thread has ever run on. We set the
// current CPU and record this as the first TSC we've seen.
CurrentCPU = CPU;
writeNewCPUIdMetadata(CPU, TSC);
}
// Before we go setting up writing new function entries, we need to be really
// careful about the pointer math we're doing. This means we need to ensure
// that the record we are about to write is going to fit into the buffer,
// without overflowing the buffer.
//
// To do this properly, we use the following assumptions:
//
// - The least number of bytes we will ever write is 8
// (sizeof(FunctionRecord)) only if the delta between the previous entry
// and this entry is within 32 bits.
// - The most number of bytes we will ever write is 8 + 16 = 24. This is
// computed by:
//
// sizeof(FunctionRecord) + sizeof(MetadataRecord)
//
// These arise in the following cases:
//
// 1. When the delta between the TSC we get and the previous TSC for the
// same CPU is outside of the uint32_t range, we end up having to
// write a MetadataRecord to indicate a "tsc wrap" before the actual
// FunctionRecord.
// 2. When we learn that we've moved CPUs, we need to write a
// MetadataRecord to indicate a "cpu change", and thus write out the
// current TSC for that CPU before writing out the actual
// FunctionRecord.
// 3. When we learn about a new CPU ID, we need to write down a "new cpu
// id" MetadataRecord before writing out the actual FunctionRecord.
//
// - An End-of-Buffer (EOB) MetadataRecord is 16 bytes.
//
// So the math we need to do is to determine whether writing 24 bytes past the
// current pointer leaves us with enough bytes to write the EOB
// MetadataRecord. If we don't have enough space after writing as much as 24
// bytes in the end of the buffer, we need to write out the EOB, get a new
// Buffer, set it up properly before doing any further writing.
//
char *BufferStart = static_cast<char *>(Buffer.Buffer);
if ((RecordPtr + (MetadataRecSize + FunctionRecSize)) - BufferStart <
static_cast<ptrdiff_t>(MetadataRecSize)) {
writeEOBMetadata();
if (auto EC = LocalBQ->releaseBuffer(Buffer)) {
Report("Failed to release buffer at %p; error=%s\n", Buffer.Buffer,
EC.message().c_str());
return;
}
if (auto EC = LocalBQ->getBuffer(Buffer)) {
Report("Failed to acquire a buffer; error=%s\n", EC.message().c_str());
return;
}
setupNewBuffer(Buffer);
}
// By this point, we are now ready to write at most 24 bytes (one metadata
// record and one function record).
BufferStart = static_cast<char *>(Buffer.Buffer);
assert((RecordPtr + (MetadataRecSize + FunctionRecSize)) - BufferStart >=
static_cast<ptrdiff_t>(MetadataRecSize) &&
"Misconfigured BufferQueue provided; Buffer size not large enough.");
std::aligned_storage<sizeof(FunctionRecord), alignof(FunctionRecord)>::type
AlignedFuncRecordBuffer;
auto &FuncRecord =
*reinterpret_cast<FunctionRecord *>(&AlignedFuncRecordBuffer);
FuncRecord.Type = uint8_t(RecordType::Function);
// Only get the lower 28 bits of the function id.
FuncRecord.FuncId = FuncId & ~(0x0F << 28);
// Here we compute the TSC Delta. There are a few interesting situations we
// need to account for:
//
// - The thread has migrated to a different CPU. If this is the case, then
// we write down the following records:
//
// 1. A 'NewCPUId' Metadata record.
// 2. A FunctionRecord with a 0 for the TSCDelta field.
//
// - The TSC delta is greater than the 32 bits we can store in a
// FunctionRecord. In this case we write down the following records:
//
// 1. A 'TSCWrap' Metadata record.
// 2. A FunctionRecord with a 0 for the TSCDelta field.
//
// - The TSC delta is representable within the 32 bits we can store in a
// FunctionRecord. In this case we write down just a FunctionRecord with
// the correct TSC delta.
//
FuncRecord.TSCDelta = 0;
if (CPU != CurrentCPU) {
// We've moved to a new CPU.
writeNewCPUIdMetadata(CPU, TSC);
} else {
// If the delta is greater than the range for a uint32_t, then we write out
// the TSC wrap metadata entry with the full TSC, and the TSC for the
// function record be 0.
auto Delta = LastTSC - TSC;
if (Delta > (1ULL << 32) - 1)
writeTSCWrapMetadata(TSC);
else
FuncRecord.TSCDelta = Delta;
}
// We then update our "LastTSC" and "CurrentCPU" thread-local variables to aid
// us in future computations of this TSC delta value.
LastTSC = TSC;
CurrentCPU = CPU;
switch (Entry) {
case XRayEntryType::ENTRY:
case XRayEntryType::LOG_ARGS_ENTRY:
FuncRecord.RecordKind = uint8_t(FunctionRecord::RecordKinds::FunctionEnter);
break;
case XRayEntryType::EXIT:
FuncRecord.RecordKind = uint8_t(FunctionRecord::RecordKinds::FunctionExit);
break;
case XRayEntryType::TAIL:
FuncRecord.RecordKind =
uint8_t(FunctionRecord::RecordKinds::FunctionTailExit);
break;
}
std::memcpy(RecordPtr, &AlignedFuncRecordBuffer, sizeof(FunctionRecord));
RecordPtr += sizeof(FunctionRecord);
// If we've exhausted the buffer by this time, we then release the buffer to
// make sure that other threads may start using this buffer.
if ((RecordPtr + MetadataRecSize) - BufferStart == MetadataRecSize) {
writeEOBMetadata();
if (auto EC = LocalBQ->releaseBuffer(Buffer)) {
Report("Failed releasing buffer at %p; error=%s\n", Buffer.Buffer,
EC.message().c_str());
return;
}
RecordPtr = nullptr;
}
}
} // namespace __xray
static auto UNUSED Unused = [] {
using namespace __xray;
if (flags()->xray_fdr_log) {
XRayLogImpl Impl{
fdrLoggingInit, fdrLoggingFinalize, fdrLoggingHandleArg0,
fdrLoggingFlush,
};
__xray_set_log_impl(Impl);
}
return true;
}();