Files
revng-revng/lib/PipelineC/Tracing/Wrapper.h
2025-04-30 15:08:45 +02:00

309 lines
8.1 KiB
C++

#pragma once
//
// This file is distributed under the MIT License. See LICENSE.md for details.
//
#include <mutex>
#include <optional>
#include <thread>
#include "llvm/Support/Base64.h"
#include "llvm/Support/Process.h"
#include "llvm/Support/raw_ostream.h"
#include "revng/ADT/ConstexprString.h"
#include "revng/PipelineC/PipelineC.h"
#include "revng/PipelineC/Tracing/Common.h"
#include "revng/Support/Assert.h"
#include "revng/Support/Chrono.h"
#include "Types.h"
inline constexpr auto TracingEnv = "REVNG_C_API_TRACE_PATH";
inline auto PointerStyle = llvm::HexPrintStyle::PrefixLower;
// The opposite of a std::recursive_mutex, if locked by the same thread it will
// assert (this is to avoid a deadlock/malformed output when tracing)
class OncePerThreadMutex {
private:
std::optional<std::thread::id> ThreadId;
std::mutex Mutex;
public:
OncePerThreadMutex() {}
void lock() {
if (ThreadId.has_value())
revng_assert(std::this_thread::get_id() != *ThreadId,
"NonRecursiveMutex entered twice by the same thread!");
Mutex.lock();
ThreadId = std::this_thread::get_id();
}
void unlock() {
revng_assert(std::this_thread::get_id() == *ThreadId);
Mutex.unlock();
ThreadId.reset();
}
};
inline OncePerThreadMutex TraceMutex;
// Helper class for tracing, this will be used by the the argument/return
// handlers defined below to properly print the value of the arguments onto the
// YAML tracing file
class TraceWriter {
private:
llvm::raw_ostream &OS;
// If true it means we're outputting a function's arguments, whereas if it is
// false it means that we're outputting its return value.
// This is needed because sometimes, for the same data type, we want to output
// different things (e.g. Value vs Pointer) if it's an argument or a return
// value
bool OutputtingArguments = false;
// Integer used to compute the ID of the command
uint64_t ID = 0;
public:
TraceWriter(llvm::raw_ostream &OS) : OS(OS) {
OS.SetUnbuffered();
printHeader();
}
public:
void functionPrelude(const llvm::StringRef Name) {
OS << "- ID: " << ID++ << "\n";
OS << " StartTime: " << revng::getEpochInMilliseconds() << "\n";
OS << " Name: " << Name << "\n";
OS << " Arguments:\n";
OutputtingArguments = true;
OS.flush();
}
void newArgument() {
OS << " - ";
OS.flush();
}
// For integral types we still keep the template parameter. This is to avoid
// the overload selector doing an implicit conversion of unexpected types to
// these types
template<IntegerType T>
void printValue(const T &Int) {
OS << Int << "\n";
OS.flush();
}
template<typename T>
requires std::is_same_v<T, bool>
void printValue(const T &Bool) {
OS << (Bool ? "true" : "false") << "\n";
OS.flush();
}
template<typename T>
requires std::is_same_v<T, char>
void printValue(const T *String) {
if (OutputtingArguments) {
OS << reprString(String) << "\n";
OS.flush();
} else {
printPointer(String);
}
}
template<RPType T>
void printValue(const T *Ptr) {
printPointer(Ptr);
}
template<typename T>
void printPointer(const T *Ptr) {
// NOTE: if reading traces becomes a major task, it might be beneficial to
// switch to a representation with increasing indexes, e.g. object_1,
// object_2, ...
OS << PointerPrefix;
llvm::write_hex(OS, reinterpret_cast<uintptr_t>(Ptr), PointerStyle);
OS << "\n";
OS.flush();
}
void printBuffer(const llvm::StringRef Input) {
OS << llvm::encodeBase64(Input) << "\n";
OS.flush();
}
template<IntegerType T>
void printList(const T IntList[], uint64_t Length) {
using IntT = max_int<T>;
OS << "[";
for (uint64_t I = 0; I < Length; I++) {
OS << static_cast<IntT>(IntList[I]);
if (I < Length - 1) {
OS << ", ";
}
}
OS << "]\n";
OS.flush();
}
template<typename T>
requires std::is_same_v<T, char>
void printList(const T *StringList[], uint64_t Length) {
OS << "[";
for (uint64_t I = 0; I < Length; I++) {
OS << reprString(StringList[I]);
if (I < Length - 1) {
OS << ", ";
}
}
OS << "]\n";
OS.flush();
}
template<RPType T>
void printList(const T *PtrList[], uint64_t Length) {
OS << "[";
for (uint64_t I = 0; I < Length; I++) {
OS << PointerPrefix;
llvm::write_hex(OS,
reinterpret_cast<uintptr_t>(PtrList[I]),
PointerStyle);
if (I < Length - 1) {
OS << ", ";
}
}
OS << "]\n";
OS.flush();
}
template<typename... T>
requires(sizeof...(T) < 2)
void printReturn(T... ReturnValue) {
OutputtingArguments = false;
OS << " Result: ";
if constexpr (sizeof...(T) == 0) {
OS << "null\n";
} else {
printValue(ReturnValue...);
}
OS << " EndTime: " << revng::getEpochInMilliseconds() << "\n";
OS.flush();
}
private:
void printHeader() {
OS << "Version: 1\n";
OS << "Commands:\n";
OS.flush();
}
std::string reprString(const char *String) {
return '"' + llvm::yaml::escape(String) + '"';
}
};
class TracingRuntime {
private:
std::optional<TraceWriter> Writer;
std::optional<llvm::raw_fd_ostream> OS;
public:
TracingRuntime() {
if (auto Path = llvm::sys::Process::GetEnv(TracingEnv)) {
std::error_code EC;
OS.emplace(*Path, EC);
revng_assert(!EC);
Writer.emplace(*OS);
}
}
void swap(llvm::raw_ostream *NewOS = nullptr) {
Writer.reset();
OS.reset();
if (NewOS != nullptr) {
Writer.emplace(*NewOS);
}
}
bool isEnabled() const { return Writer.has_value(); }
TraceWriter &operator*() {
revng_assert(Writer.has_value());
return *Writer;
}
TraceWriter *operator->() {
revng_assert(Writer.has_value());
return &*Writer;
}
};
inline TracingRuntime Tracing;
template<ConstexprString Name, int I, int N, typename... T>
inline void handleArgument(std::tuple<T...> Args) {
Tracing->newArgument();
using ArgT = decltype(std::get<I>(Args));
using RArgT = std::remove_reference_t<ArgT>;
ArgT Argument = std::get<I>(Args);
constexpr int LengthHintValue = LengthHint<Name, I>;
if constexpr (LengthHintValue >= 0) {
using LengthT = decltype(std::get<LengthHintValue>(Args));
static_assert(isInteger<std::remove_reference_t<LengthT>>());
LengthT LengthArgument = std::get<LengthHintValue>(Args);
// Handle arguments with length hints
if constexpr (std::is_same_v<RArgT, const char *>) {
// Buffer
Tracing->printBuffer({ Argument, LengthArgument });
} else {
// Array-like
Tracing->printList(Argument, LengthArgument);
}
} else {
if constexpr (isDestroy<Name>()) {
// _destroy methods always take 1 argument and it's always a pointer
static_assert(N == 1);
Tracing->printPointer(Argument);
} else {
Tracing->printValue(Argument);
}
}
if constexpr (I + 1 < N)
handleArgument<Name, I + 1, N>(Args);
}
template<ConstexprString Name, typename... T>
inline void handleArguments(T &&...Args) {
if constexpr (sizeof...(T) > 0)
handleArgument<Name, 0, sizeof...(T)>(std::make_tuple(Args...));
}
// This function will be used in each PipelineC function we need to wrap
// For example:
// rp_initialize(...) { return wrap<"rp_initialize">(_rp_initialize, ...); }
template<ConstexprString Name, typename CalleeT, typename... ArgsT>
inline decltype(auto) wrap(CalleeT Callee, ArgsT... Args) {
using ReturnT = typename decltype(std::function{ Callee })::result_type;
if (Tracing.isEnabled()) {
// Special lock to avoid trace output being broken by multithreading or by
// calling a PipelineC function within PipelineC
std::lock_guard Guard(TraceMutex);
Tracing->functionPrelude(llvm::StringRef(Name));
handleArguments<Name>(Args...);
if constexpr (std::is_same_v<ReturnT, void>) {
Callee(std::forward<ArgsT>(Args)...);
Tracing->printReturn();
} else {
ReturnT Return = Callee(std::forward<ArgsT>(Args)...);
Tracing->printReturn(Return);
return Return;
}
} else {
return Callee(std::forward<ArgsT>(Args)...);
}
}