diff --git a/CMakeLists.txt b/CMakeLists.txt index 810cd9d44..2c9fcfa1a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,7 @@ cmake_dependent_option(ENABLE_THREADS "Enable multithreading" ON "Threads_FOUND" option(ENABLE_ZLIB "Enable zlib" ON) option(ENABLE_SYSTEM_YYJSON "Use system provided (instead of fastfetch embedded) yyjson library" OFF) option(ENABLE_ASAN "Build fastfetch with ASAN (address sanitizer)" OFF) +option(ENABLE_TRACER "Build fastfetch with function tracing" OFF) option(ENABLE_LTO "Enable link-time optimization in release mode if supported" ON) option(BUILD_FLASHFETCH "Build flashfetch" ON) # Also build the flashfetch binary option(BUILD_TESTS "Build tests" OFF) # Also create test executables @@ -1365,6 +1366,22 @@ elseif(GNU) ) endif() +if(ENABLE_TRACER) + message(STATUS "Tracer is enabled") + list(APPEND LIBFASTFETCH_SRC src/common/impl/tracer.c) + if(CMAKE_C_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -finstrument-functions-after-inlining") + else() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -finstrument-functions -Dinline=\"__attribute__((no_instrument_function)) inline\"") + endif() + if(NOT WIN32) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -rdynamic") + elseif(CMAKE_C_COMPILER_ID MATCHES "Clang") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -gcodeview -fuse-ld=lld") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--pdb=${CMAKE_BINARY_DIR}/fastfetch.pdb") + endif() +endif() + # Proprietary GPU driver APIs if(LINUX OR FreeBSD OR WIN32) list(APPEND LIBFASTFETCH_SRC src/detection/gpu/gpu_nvidia.c) @@ -1889,6 +1906,11 @@ elseif(WIN32) PRIVATE "runtimeobject" ) endif() + if(ENABLE_TRACER) + target_link_libraries(libfastfetch + PRIVATE "dbghelp" + ) + endif() elseif(FreeBSD) target_link_libraries(libfastfetch PRIVATE "m" diff --git a/src/common/impl/tracer.c b/src/common/impl/tracer.c new file mode 100644 index 000000000..a2951598f --- /dev/null +++ b/src/common/impl/tracer.c @@ -0,0 +1,144 @@ +// DON'T CALL ANY EXTERNAL FUNCTION IN THIS FILE + +#include +#include +#include +#include +#include +#include +#include +#include + +#if !_WIN32 + #include + #include + #include +#else + #include + #include +#endif + +#if _WIN32 +static LARGE_INTEGER freq; + #define NtCurrentProcess() ((HANDLE) (-1)) +#endif + +static struct trace_event { + uint64_t ts; + uint64_t tid; + void* func; +} events[4 * 1024 * 1024]; // 4M events, 96 MiB memory usage +static _Atomic uint32_t event_count; + +__attribute__((no_instrument_function, always_inline)) static inline uint64_t get_time_us() { +#if !_WIN32 + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t) (ts.tv_sec * 1000000 + ts.tv_nsec / 1000); +#else + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + return (uint64_t) (now.QuadPart * 1000000 / freq.QuadPart); +#endif +} + +#if _WIN32 +__attribute__((constructor, no_instrument_function)) void trace_init() { + QueryPerformanceFrequency(&freq); +} +#endif + +__attribute__((destructor, no_instrument_function)) void trace_fini() { +#if _WIN32 + uint32_t pid = (uint32_t) GetCurrentProcessId(); +#else + uint32_t pid = (uint32_t) getpid(); +#endif + + char fileName[64]; + snprintf(fileName, sizeof(fileName), "trace_%d.json", pid); + FILE* trace_file = fopen(fileName, "wb"); + if (!trace_file) { + fprintf(stderr, "Failed to open trace file %s for writing\n", fileName); + abort(); + } + + fputs("[\n", trace_file); + +#if _WIN32 + SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_UNDNAME); + SymInitialize(NtCurrentProcess(), NULL, TRUE); +#endif + + const char* fnName = NULL; + + uint32_t count = atomic_load_explicit(&event_count, memory_order_acquire); + for (uint32_t i = 0; i < count; ++i) { + void* fn = events[i].func; + uint64_t ts = events[i].ts; + bool is_exit = (ts >> 63) != 0; + ts &= ~(1ULL << 63); // clear the highest bit + uint64_t tid = events[i].tid; + +#if _WIN32 + char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)]; + PSYMBOL_INFO pSymbol = (PSYMBOL_INFO) buffer; + pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO); + pSymbol->MaxNameLen = MAX_SYM_NAME; + + DWORD64 displacement = 0; + if (SymFromAddr(NtCurrentProcess(), (DWORD64) fn, &displacement, pSymbol)) { + fnName = pSymbol->Name; + } +#else + char buffer[32]; + Dl_info info; + if (dladdr(fn, &info) && info.dli_sname) { + fnName = info.dli_sname; + } +#endif + + else { + snprintf(buffer, sizeof(buffer), "%p", fn); + fnName = buffer; + } + + fprintf(trace_file, "{\"name\":\"%s\",\"ph\":\"%c\",\"pid\":%" PRIu32 ",\"tid\":%" PRIu64 ",\"ts\":%" PRIu64 "}%c\n", fnName, is_exit ? 'E' : 'B', pid, tid, ts, i < count - 1 ? ',' : ' '); + } + + fputs("\n]\n", trace_file); + fclose(trace_file); + fprintf(stderr, "Trace written to trace_%d.json with %u events. Use https://ui.perfetto.dev/ to view it.\n", pid, count); + +#if _WIN32 + SymCleanup(NtCurrentProcess()); +#endif +} + +__attribute__((no_instrument_function)) static void write_event(void* this_fn, bool is_exit) { + uint32_t idx = atomic_fetch_add_explicit(&event_count, 1, memory_order_relaxed); + if (__builtin_expect(idx >= sizeof(events) / sizeof(events[0]), false)) { + abort(); + } + + events[idx].ts = get_time_us(); +#if _WIN32 + events[idx].tid = (uint64_t) GetCurrentThreadId(); +#else + events[idx].tid = (uint64_t) (uintptr_t) pthread_self(); +#endif + events[idx].func = this_fn; + if (is_exit) { + events[idx].ts |= (1ULL << 63); // set the highest bit to indicate function exit + } +} + +__attribute__((no_instrument_function)) void __cyg_profile_func_enter(void* this_fn, void* call_site) { + (void) call_site; + write_event(this_fn, false); +} + +__attribute__((no_instrument_function)) void __cyg_profile_func_exit(void* this_fn, void* call_site) { + (void) call_site; + write_event(this_fn, true); +}