#pragma once // // This file is distributed under the MIT License. See LICENSE.md for details. // #include #include #include #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 "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 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: " << getUnixMillis() << "\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 void printValue(const T &Int) { OS << Int << "\n"; OS.flush(); } template requires std::is_same_v void printValue(const T &Bool) { OS << (Bool ? "true" : "false") << "\n"; OS.flush(); } template requires std::is_same_v void printValue(const T *String) { if (OutputtingArguments) { OS << reprString(String) << "\n"; OS.flush(); } else { printPointer(String); } } template void printValue(const T *Ptr) { printPointer(Ptr); } template 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(Ptr), PointerStyle); OS << "\n"; OS.flush(); } void printBuffer(const llvm::StringRef Input) { OS << llvm::encodeBase64(Input) << "\n"; OS.flush(); } template void printList(const T IntList[], uint64_t Length) { using IntT = max_int; OS << "["; for (uint64_t I = 0; I < Length; I++) { OS << static_cast(IntList[I]); if (I < Length - 1) { OS << ", "; } } OS << "]\n"; OS.flush(); } template requires std::is_same_v 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 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(PtrList[I]), PointerStyle); if (I < Length - 1) { OS << ", "; } } OS << "]\n"; OS.flush(); } template 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: " << getUnixMillis() << "\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) + '"'; } // Returns the number of milliseconds since epoch static uint64_t getUnixMillis() { namespace sc = std::chrono; auto Now = sc::system_clock::now().time_since_epoch(); return sc::duration_cast(Now).count(); } }; class TracingRuntime { private: std::optional Writer; std::optional 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 inline void handleArgument(std::tuple Args) { Tracing->newArgument(); using ArgT = decltype(std::get(Args)); using RArgT = std::remove_reference_t; ArgT Argument = std::get(Args); constexpr int LengthHintValue = LengthHint; if constexpr (LengthHintValue >= 0) { using LengthT = decltype(std::get(Args)); static_assert(isInteger>()); LengthT LengthArgument = std::get(Args); // Handle arguments with length hints if constexpr (std::is_same_v) { // Buffer Tracing->printBuffer({ Argument, LengthArgument }); } else { // Array-like Tracing->printList(Argument, LengthArgument); } } else { if constexpr (isDestroy()) { // _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(Args); } template inline void handleArguments(T &&...Args) { if constexpr (sizeof...(T) > 0) handleArgument(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 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(std::string_view(Name)); handleArguments(Args...); if constexpr (std::is_same_v) { Callee(std::forward(Args)...); Tracing->printReturn(); } else { ReturnT Return = Callee(std::forward(Args)...); Tracing->printReturn(Return); return Return; } } else { return Callee(std::forward(Args)...); } }