diff --git a/src/track/heaptrack_inject.cpp b/src/track/heaptrack_inject.cpp index b2589f5..b08e00a 100644 --- a/src/track/heaptrack_inject.cpp +++ b/src/track/heaptrack_inject.cpp @@ -1,308 +1,316 @@ /* * Copyright 2014-2017 Milian Wolff * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "libheaptrack.h" #include "util/config.h" #include #include +#include #include #include +#include #include #include /** * @file heaptrack_inject.cpp * * @brief Experimental support for symbol overloading after runtime injection. */ #if __WORDSIZE == 64 #define ELF_R_SYM(i) ELF64_R_SYM(i) #elif __WORDSIZE == 32 #define ELF_R_SYM(i) ELF32_R_SYM(i) #else #error unsupported word size #endif namespace { namespace Elf { using Addr = ElfW(Addr); using Dyn = ElfW(Dyn); using Rel = ElfW(Rel); using Rela = ElfW(Rela); using Sym = ElfW(Sym); using Sxword = ElfW(Sxword); using Xword = ElfW(Xword); } void overwrite_symbols() noexcept; namespace hooks { struct malloc { static constexpr auto name = "malloc"; static constexpr auto original = &::malloc; static void* hook(size_t size) noexcept { auto ptr = original(size); heaptrack_malloc(ptr, size); return ptr; } }; struct free { static constexpr auto name = "free"; static constexpr auto original = &::free; static void hook(void* ptr) noexcept { heaptrack_free(ptr); original(ptr); } }; struct realloc { static constexpr auto name = "realloc"; static constexpr auto original = &::realloc; static void* hook(void* ptr, size_t size) noexcept { auto ret = original(ptr, size); heaptrack_realloc(ptr, size, ret); return ret; } }; struct calloc { static constexpr auto name = "calloc"; static constexpr auto original = &::calloc; static void* hook(size_t num, size_t size) noexcept { auto ptr = original(num, size); heaptrack_malloc(ptr, num * size); return ptr; } }; #if HAVE_CFREE struct cfree { static constexpr auto name = "cfree"; static constexpr auto original = &::cfree; static void hook(void* ptr) noexcept { heaptrack_free(ptr); original(ptr); } }; #endif struct dlopen { static constexpr auto name = "dlopen"; static constexpr auto original = &::dlopen; static void* hook(const char* filename, int flag) noexcept { auto ret = original(filename, flag); if (ret) { heaptrack_invalidate_module_cache(); overwrite_symbols(); } return ret; } }; struct dlclose { static constexpr auto name = "dlclose"; static constexpr auto original = &::dlclose; static int hook(void* handle) noexcept { auto ret = original(handle); if (!ret) { heaptrack_invalidate_module_cache(); } return ret; } }; struct posix_memalign { static constexpr auto name = "posix_memalign"; static constexpr auto original = &::posix_memalign; static int hook(void** memptr, size_t alignment, size_t size) noexcept { auto ret = original(memptr, alignment, size); if (!ret) { heaptrack_malloc(*memptr, size); } return ret; } }; template bool hook(const char* symname, Elf::Addr addr, bool restore) { static_assert(std::is_convertible::value, "hook is not compatible to original function"); if (strcmp(Hook::name, symname) != 0) { return false; } // try to make the page read/write accessible, which is hackish // but apparently required for some shared libraries auto page = reinterpret_cast(addr & ~(0x1000 - 1)); mprotect(page, 0x1000, PROT_READ | PROT_WRITE); // now write to the address auto typedAddr = reinterpret_cast::type*>(addr); if (restore) { // restore the original address on shutdown *typedAddr = Hook::original; } else { // now actually inject our hook *typedAddr = &Hook::hook; } return true; } void apply(const char* symname, Elf::Addr addr, bool restore) { // TODO: use std::apply once we can rely on C++17 hook(symname, addr, restore) || hook(symname, addr, restore) || hook(symname, addr, restore) || hook(symname, addr, restore) #if HAVE_CFREE || hook(symname, addr, restore) #endif || hook(symname, addr, restore) || hook(symname, addr, restore) || hook(symname, addr, restore); } } template struct elftable { using type = T; T* table = nullptr; Elf::Xword size = {}; bool consume(const Elf::Dyn* dyn) noexcept { if (dyn->d_tag == AddrTag) { table = reinterpret_cast(dyn->d_un.d_ptr); return true; } else if (dyn->d_tag == SizeTag) { size = dyn->d_un.d_val; return true; } return false; } }; using elf_string_table = elftable; using elf_rel_table = elftable; using elf_rela_table = elftable; using elf_jmprel_table = elftable; using elf_symbol_table = elftable; template void try_overwrite_elftable(const Table& jumps, const elf_string_table& strings, const elf_symbol_table& symbols, const Elf::Addr base, const bool restore) noexcept { const auto rela_end = reinterpret_cast(reinterpret_cast(jumps.table) + jumps.size); for (auto rela = jumps.table; rela < rela_end; rela++) { const auto index = ELF_R_SYM(rela->r_info); if (index >= 0 && index < symbols.size) { const char* symname = strings.table + symbols.table[index].st_name; auto addr = rela->r_offset + base; hooks::apply(symname, addr, restore); } } } void try_overwrite_symbols(const Elf::Dyn* dyn, const Elf::Addr base, const bool restore) noexcept { elf_symbol_table symbols; elf_rel_table rels; elf_rela_table relas; elf_jmprel_table jmprels; elf_string_table strings; // initialize the elf tables for (; dyn->d_tag != DT_NULL; ++dyn) { symbols.consume(dyn) || strings.consume(dyn) || rels.consume(dyn) || relas.consume(dyn) || jmprels.consume(dyn); } // find symbols to overwrite try_overwrite_elftable(rels, strings, symbols, base, restore); try_overwrite_elftable(relas, strings, symbols, base, restore); try_overwrite_elftable(jmprels, strings, symbols, base, restore); } int iterate_phdrs(dl_phdr_info* info, size_t /*size*/, void* data) noexcept { if (strstr(info->dlpi_name, "/libheaptrack_inject.so")) { // prevent infinite recursion: do not overwrite our own symbols return 0; } else if (strstr(info->dlpi_name, "/ld-linux")) { // prevent strange crashes due to overwriting the free symbol in ld-linux return 0; } for (auto phdr = info->dlpi_phdr, end = phdr + info->dlpi_phnum; phdr != end; ++phdr) { if (phdr->p_type == PT_DYNAMIC) { try_overwrite_symbols(reinterpret_cast(phdr->p_vaddr + info->dlpi_addr), info->dlpi_addr, data != nullptr); } } return 0; } void overwrite_symbols() noexcept { dl_iterate_phdr(&iterate_phdrs, nullptr); } } extern "C" { void heaptrack_inject(const char* outputFileName) noexcept { - heaptrack_init(outputFileName, []() { overwrite_symbols(); }, [](FILE* out) { fprintf(out, "A\n"); }, + heaptrack_init(outputFileName, []() { overwrite_symbols(); }, + [](int fd) { + int ret = -1; + do { + ret = write(fd, "A\n", sizeof("A\n")); + } while (ret < 0 && errno == EINTR); + }, []() { bool do_shutdown = true; dl_iterate_phdr(&iterate_phdrs, &do_shutdown); }); } } diff --git a/src/track/libheaptrack.cpp b/src/track/libheaptrack.cpp index ade28fd..456b18f 100644 --- a/src/track/libheaptrack.cpp +++ b/src/track/libheaptrack.cpp @@ -1,827 +1,854 @@ /* * Copyright 2014-2017 Milian Wolff * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /** * @file libheaptrack.cpp * * @brief Collect raw heaptrack data by overloading heap allocation functions. */ #include "libheaptrack.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tracetree.h" #include "util/config.h" #include "util/libunwind_config.h" /** * uncomment to add helgrind annotations to the custom spinlock */ // #include /** * uncomment this to get extended debug code for known pointers * there are still some malloc functions I'm missing apparently, * related to TLS and such I guess */ // #define DEBUG_MALLOC_PTRS /** * uncomment this to use std::mutex for locking instead of a spinlock * * this makes it possible to use valgrind's helgrind/drd tools for error detection */ // #define DEBUG_USE_MUTEX using namespace std; namespace { using clock = chrono::steady_clock; chrono::time_point startTime() { static const chrono::time_point s_start = clock::now(); return s_start; } chrono::milliseconds elapsedTime() { return chrono::duration_cast(clock::now() - startTime()); } __pid_t gettid() { return syscall(SYS_gettid); } /** * A per-thread handle guard to prevent infinite recursion, which should be * acquired before doing any special symbol handling. */ struct RecursionGuard { RecursionGuard() : wasLocked(isActive) { isActive = true; } ~RecursionGuard() { isActive = wasLocked; } const bool wasLocked; static thread_local bool isActive; }; thread_local bool RecursionGuard::isActive = false; enum DebugVerbosity { NoDebugOutput, MinimalOutput, VerboseOutput, VeryVerboseOutput, }; // change this to add more debug output to stderr constexpr const DebugVerbosity s_debugVerbosity = NoDebugOutput; /** * Call this to optionally show debug information but give the compiler * a hand in removing it all if debug output is disabled. */ template inline void debugLog(const char fmt[], Args... args) { if (debugLevel <= s_debugVerbosity) { RecursionGuard guard; flockfile(stderr); fprintf(stderr, "heaptrack debug(%d) [%d:%d]@%lu ", static_cast(debugLevel), getpid(), gettid(), elapsedTime().count()); fprintf(stderr, fmt, args...); fputc('\n', stderr); funlockfile(stderr); } } void printBacktrace() { if (s_debugVerbosity == NoDebugOutput) return; #if LIBUNWIND_HAS_UNW_GETCONTEXT && LIBUNWIND_HAS_UNW_INIT_LOCAL RecursionGuard guard; unw_context_t context; unw_getcontext(&context); unw_cursor_t cursor; unw_init_local(&cursor, &context); int frameNr = 0; while (unw_step(&cursor)) { ++frameNr; unw_word_t ip = 0; unw_get_reg(&cursor, UNW_REG_IP, &ip); unw_word_t sp = 0; unw_get_reg(&cursor, UNW_REG_SP, &sp); char symbol[256] = {""}; unw_word_t offset = 0; unw_get_proc_name(&cursor, symbol, sizeof(symbol), &offset); fprintf(stderr, "#%-2d 0x%016" PRIxPTR " sp=0x%016" PRIxPTR " %s + 0x%" PRIxPTR "\n", frameNr, static_cast(ip), static_cast(sp), symbol, static_cast(offset)); } #endif } /** * Set to true in an atexit handler. In such conditions, the stop callback * will not be called. */ atomic s_atexit{false}; /** * Set to true in heaptrack_stop, when s_atexit was not yet set. In such conditions, * we always fully unload and cleanup behind ourselves */ atomic s_forceCleanup{false}; -void writeVersion(FILE* out) -{ - fprintf(out, "v %x %x\n", HEAPTRACK_VERSION, HEAPTRACK_FILE_FORMAT_VERSION); -} - -void writeExe(FILE* out) -{ - const int BUF_SIZE = 1023; - char buf[BUF_SIZE + 1]; - ssize_t size = readlink("/proc/self/exe", buf, BUF_SIZE); - if (size > 0 && size < BUF_SIZE) { - buf[size] = 0; - fprintf(out, "x %s\n", buf); - } -} - -void writeCommandLine(FILE* out) -{ - fputc('X', out); - const int BUF_SIZE = 4096; - char buf[BUF_SIZE + 1]; - auto fd = open("/proc/self/cmdline", O_RDONLY); - int bytesRead = read(fd, buf, BUF_SIZE); - char* end = buf + bytesRead; - for (char* p = buf; p < end;) { - fputc(' ', out); - fputs(p, out); - while (*p++) - ; // skip until start of next 0-terminated section - } - - close(fd); - fputc('\n', out); -} - -void writeSystemInfo(FILE* out) -{ - fprintf(out, "I %lx %lx\n", sysconf(_SC_PAGESIZE), sysconf(_SC_PHYS_PAGES)); -} - -FILE* createFile(const char* fileName) +int createFile(const char* fileName) { string outputFileName; if (fileName) { outputFileName.assign(fileName); } if (outputFileName == "-" || outputFileName == "stdout") { debugLog("%s", "will write to stdout"); - return stdout; + return fileno(stdout); } else if (outputFileName == "stderr") { debugLog("%s", "will write to stderr"); - return stderr; + return fileno(stderr); } if (outputFileName.empty()) { // env var might not be set when linked directly into an executable outputFileName = "heaptrack.$$"; } boost::replace_all(outputFileName, "$$", to_string(getpid())); - auto out = fopen(outputFileName.c_str(), "we"); + auto out = open(outputFileName.c_str(), O_CREAT | O_WRONLY | O_CLOEXEC, 0644); debugLog("will write to %s/%p\n", outputFileName.c_str(), out); // we do our own locking, this speeds up the writing significantly - if (out) { - __fsetlocking(out, FSETLOCKING_BYCALLER); - } else { + if (out == -1) { fprintf(stderr, "ERROR: failed to open heaptrack output file %s: %s (%d)\n", outputFileName.c_str(), strerror(errno), errno); - } - - if (flock(fileno(out), LOCK_EX | LOCK_NB) != 0) { + } else if (flock(out, LOCK_EX | LOCK_NB) != 0) { fprintf(stderr, "ERROR: failed to lock heaptrack output file %s: %s (%d)\n", outputFileName.c_str(), strerror(errno), errno); - fclose(out); - return nullptr; + close(out); + return -1; } return out; } class SpinLock { public: SpinLock() { #ifdef ANNOTATE_RWLOCK_CREATE ANNOTATE_RWLOCK_CREATE(this); #endif } ~SpinLock() { #ifdef ANNOTATE_RWLOCK_DESTROY ANNOTATE_RWLOCK_DESTROY(this); #endif } bool try_lock() { auto ret = m_locked.exchange(true, memory_order_acquire) == false; #ifdef ANNOTATE_RWLOCK_ACQUIRED if (ret) { ANNOTATE_RWLOCK_ACQUIRED(this, 1); } #endif return ret; } void unlock() { m_locked.store(false, memory_order_release); #ifdef ANNOTATE_RWLOCK_RELEASED ANNOTATE_RWLOCK_RELEASED(this, 1); #endif } private: atomic m_locked{false}; }; #ifdef DEBUG_USE_MUTEX using Lock = std::mutex; #else using Lock = SpinLock; #endif +const unsigned BUFFER_CAPACITY = PIPE_BUF; /** * Thread-Safe heaptrack API * * The only critical section in libheaptrack is the output of the data, * dl_iterate_phdr * calls, as well as initialization and shutdown. * * This uses a spinlock, instead of a std::mutex, as the latter can lead to - * deadlocks - * on destruction. The spinlock is "simple", and OK to only guard the small - * sections. + * deadlocks on destruction, when we try to join the timer thread which in + * turn is waiting to obtain the lock. + * The spinlock is "simple", and OK to only guard the small sections. */ class HeapTrack { public: HeapTrack(const RecursionGuard& /*recursionGuard*/) : HeapTrack([] { return true; }) { } ~HeapTrack() { debugLog("%s", "releasing lock"); s_lock.unlock(); } void initialize(const char* fileName, heaptrack_callback_t initBeforeCallback, heaptrack_callback_initialized_t initAfterCallback, heaptrack_callback_t stopCallback) { debugLog("initializing: %s", fileName); if (s_data) { debugLog("%s", "already initialized"); return; } if (initBeforeCallback) { debugLog("%s", "calling initBeforeCallback"); initBeforeCallback(); debugLog("%s", "done calling initBeforeCallback"); } // do some once-only initializations static once_flag once; call_once(once, [] { debugLog("%s", "doing once-only initialization"); // configure libunwind for better speed if (unw_set_caching_policy(unw_local_addr_space, UNW_CACHE_PER_THREAD)) { fprintf(stderr, "WARNING: Failed to enable per-thread libunwind caching.\n"); } #ifdef unw_set_cache_size if (unw_set_cache_size(unw_local_addr_space, 1024, 0)) { fprintf(stderr, "WARNING: Failed to set libunwind cache size.\n"); } #endif // do not trace forked child processes // TODO: make this configurable pthread_atfork(&prepare_fork, &parent_fork, &child_fork); atexit([]() { if (s_forceCleanup) { return; } debugLog("%s", "atexit()"); s_atexit.store(true); heaptrack_stop(); }); }); - FILE* out = createFile(fileName); + const auto out = createFile(fileName); - if (!out) { + if (out == -1) { if (stopCallback) { stopCallback(); } return; } - writeVersion(out); - writeExe(out); - writeCommandLine(out); - writeSystemInfo(out); - s_data = new LockedData(out, stopCallback); + writeVersion(); + writeExe(); + writeCommandLine(); + writeSystemInfo(); + if (initAfterCallback) { debugLog("%s", "calling initAfterCallback"); initAfterCallback(out); debugLog("%s", "calling initAfterCallback done"); } debugLog("%s", "initialization done"); } void shutdown() { if (!s_data) { return; } debugLog("%s", "shutdown()"); writeTimestamp(); writeRSS(); + sendBuffer(); // NOTE: we leak heaptrack data on exit, intentionally // This way, we can be sure to get all static deallocations. if (!s_atexit || s_forceCleanup) { delete s_data; s_data = nullptr; } debugLog("%s", "shutdown() done"); } void invalidateModuleCache() { if (!s_data) { return; } s_data->moduleCacheDirty = true; } void writeTimestamp() { - if (!s_data || !s_data->out) { + if (!s_data || s_data->out == -1) { return; } auto elapsed = elapsedTime(); debugLog("writeTimestamp(%" PRIx64 ")", elapsed.count()); writeLine("c %" PRIx64 "\n", elapsed.count()); } void writeRSS() { - if (!s_data || !s_data->out || !s_data->procStatm) { + if (!s_data || s_data->out == -1 || !s_data->procStatm) { return; } // read RSS in pages from statm, then rewind for next read size_t rss = 0; if (fscanf(s_data->procStatm, "%*x %zx", &rss) != 1) { fprintf(stderr, "WARNING: Failed to read RSS value from /proc/self/statm.\n"); fclose(s_data->procStatm); s_data->procStatm = nullptr; return; } rewind(s_data->procStatm); // TODO: compare to rusage.ru_maxrss (getrusage) to find "real" peak? // TODO: use custom allocators with known page sizes to prevent tainting // the RSS numbers with heaptrack-internal data writeLine("R %zx\n", rss); } + void writeVersion() + { + writeLine("v %x %x\n", HEAPTRACK_VERSION, HEAPTRACK_FILE_FORMAT_VERSION); + } + + void writeExe() + { + const int BUF_SIZE = 1023; + char buf[BUF_SIZE + 1]; + ssize_t size = readlink("/proc/self/exe", buf, BUF_SIZE); + if (size > 0 && size < BUF_SIZE) { + buf[size] = 0; + writeLine("x %s\n", buf); + } + } + + void writeCommandLine() + { + writeLine("X"); + const int BUF_SIZE = 4096; + char buf[BUF_SIZE + 1]; + auto fd = open("/proc/self/cmdline", O_RDONLY); + int bytesRead = read(fd, buf, BUF_SIZE); + char* end = buf + bytesRead; + for (char* p = buf; p < end;) { + writeLine(" %s", p); + while (*p++) + ; // skip until start of next 0-terminated section + } + + close(fd); + writeLine("\n"); + } + + void writeSystemInfo() + { + writeLine("I %lx %lx\n", sysconf(_SC_PAGESIZE), sysconf(_SC_PHYS_PAGES)); + } + void handleMalloc(void* ptr, size_t size, const Trace& trace) { - if (!s_data || !s_data->out) { + if (!s_data || s_data->out == -1) { return; } updateModuleCache(); const auto index = s_data->traceTree.index( trace, [this](uintptr_t ip, uint32_t index) { return writeLine("t %" PRIxPTR " %x\n", ip, index); }); #ifdef DEBUG_MALLOC_PTRS auto it = s_data->known.find(ptr); assert(it == s_data->known.end()); s_data->known.insert(ptr); #endif writeLine("+ %zx %x %" PRIxPTR "\n", size, index, reinterpret_cast(ptr)); } void handleFree(void* ptr) { - if (!s_data || !s_data->out) { + if (!s_data || s_data->out == -1) { return; } #ifdef DEBUG_MALLOC_PTRS auto it = s_data->known.find(ptr); assert(it != s_data->known.end()); s_data->known.erase(it); #endif writeLine("- %" PRIxPTR "\n", reinterpret_cast(ptr)); } private: static int dl_iterate_phdr_callback(struct dl_phdr_info* info, size_t /*size*/, void* data) { auto heaptrack = reinterpret_cast(data); const char* fileName = info->dlpi_name; if (!fileName || !fileName[0]) { fileName = "x"; } debugLog("dlopen_notify_callback: %s %zx", fileName, info->dlpi_addr); if (!heaptrack->writeLine("m %s %zx", fileName, info->dlpi_addr)) { return 1; } for (int i = 0; i < info->dlpi_phnum; i++) { const auto& phdr = info->dlpi_phdr[i]; if (phdr.p_type == PT_LOAD) { if (!heaptrack->writeLine(" %zx %zx", phdr.p_vaddr, phdr.p_memsz)) { return 1; } } } if (!heaptrack->writeLine("\n")) { return 1; } return 0; } static void prepare_fork() { debugLog("%s", "prepare_fork()"); // don't do any custom malloc handling while inside fork RecursionGuard::isActive = true; } static void parent_fork() { debugLog("%s", "parent_fork()"); // the parent process can now continue its custom malloc tracking RecursionGuard::isActive = false; } static void child_fork() { debugLog("%s", "child_fork()"); // but the forked child process cleans up itself // this is important to prevent two processes writing to the same file s_data = nullptr; RecursionGuard::isActive = true; } void updateModuleCache() { - if (!s_data || !s_data->out || !s_data->moduleCacheDirty) { + if (!s_data || s_data->out == -1 || !s_data->moduleCacheDirty) { return; } debugLog("%s", "updateModuleCache()"); if (!writeLine("m -\n")) { return; } dl_iterate_phdr(&dl_iterate_phdr_callback, this); s_data->moduleCacheDirty = false; } template - inline bool writeLine(const char* fmt, T... args) + bool writeLine(const char* fmt, T... args) { - int ret = 0; - do { - ret = fprintf(s_data->out, fmt, args...); - } while (ret < 0 && errno == EINTR); - - if (ret < 0) { - writeError(); + for (bool first_try : {true, false}) { + const auto available_buffer_size = BUFFER_CAPACITY - s_data->bufferSize; + int ret = snprintf(s_data->buffer.get() + s_data->bufferSize, available_buffer_size, fmt, args...); + if (ret < 0) { + writeError(); + return false; + } else if (static_cast(ret) < available_buffer_size) { + s_data->bufferSize += ret; + return true; + } else if (!first_try) { + fprintf(stderr, "failed to write line of length %d, available space: %d", ret, available_buffer_size); + return false; + } else if (static_cast(ret) >= BUFFER_CAPACITY) { + fprintf(stderr, "failed to write line of length %d, available space: %d", ret, BUFFER_CAPACITY); + return false; + } else if (!sendBuffer()) { + // buffer doesn't have enough space, send current buffer contents and clear buffer + return false; + } } - return ret >= 0; + return true; } - inline bool writeLine(const char* line) + bool writeLine(const char* line) { + // TODO: could be optimized to use strncpy or similar + return writeLine("%s", line); + } + + bool sendBuffer() + { + if (!s_data->bufferSize) + return 0; + int ret = 0; do { - ret = fputs(line, s_data->out); + ret = write(s_data->out, s_data->buffer.get(), s_data->bufferSize); } while (ret < 0 && errno == EINTR); if (ret < 0) { writeError(); + return false; } - - return ret >= 0; + s_data->bufferSize = 0; + memset(s_data->buffer.get(), 0, BUFFER_CAPACITY); + return true; } void writeError() { debugLog("write error %d/%s", errno, strerror(errno)); printBacktrace(); - s_data->out = nullptr; + close(s_data->out); + s_data->out = -1; shutdown(); } struct LockCheckFailed{}; template HeapTrack(AdditionalLockCheck lockCheck) { debugLog("%s", "acquiring lock"); while (!s_lock.try_lock()) { if (!lockCheck()) throw LockCheckFailed(); this_thread::sleep_for(chrono::microseconds(1)); } debugLog("%s", "lock acquired"); } struct LockedData { - LockedData(FILE* out, heaptrack_callback_t stopCallback) + LockedData(int out, heaptrack_callback_t stopCallback) : out(out) + , buffer(new char[BUFFER_CAPACITY]) , stopCallback(stopCallback) { + memset(buffer.get(), 0, BUFFER_CAPACITY); + debugLog("%s", "constructing LockedData"); procStatm = fopen("/proc/self/statm", "r"); if (!procStatm) { fprintf(stderr, "WARNING: Failed to open /proc/self/statm for reading: %s.\n", strerror(errno)); } else if (setvbuf(procStatm, nullptr, _IONBF, 0)) { // disable buffering to ensure we read the latest values fprintf(stderr, "WARNING: Failed to disable buffering for reading of /proc/self/statm: %s.\n", strerror(errno)); } // ensure this utility thread is not handling any signals // our host application may assume only one specific thread // will handle the threads, if that's not the case things // seemingly break in non-obvious ways. // see also: https://bugs.kde.org/show_bug.cgi?id=378494 sigset_t previousMask; sigset_t newMask; sigfillset(&newMask); if (pthread_sigmask(SIG_SETMASK, &newMask, &previousMask) != 0) { fprintf(stderr, "WARNING: Failed to block signals, disabling timer thread.\n"); return; } // the mask we set above will be inherited by the thread that we spawn below timerThread = thread([&]() { RecursionGuard::isActive = true; debugLog("%s", "timer thread started"); // now loop and repeatedly print the timestamp and RSS usage to the data stream while (!stopTimerThread) { // TODO: make interval customizable this_thread::sleep_for(chrono::milliseconds(10)); try { HeapTrack heaptrack([&] { return !stopTimerThread.load(); }); heaptrack.writeTimestamp(); heaptrack.writeRSS(); } catch (LockCheckFailed) { break; } } }); // now restore the previous mask as if nothing ever happened if (pthread_sigmask(SIG_SETMASK, &previousMask, nullptr) != 0) { fprintf(stderr, "WARNING: Failed to restore the signal mask.\n"); } } ~LockedData() { debugLog("%s", "destroying LockedData"); stopTimerThread = true; if (timerThread.joinable()) { try { timerThread.join(); } catch (std::system_error) { } } - if (out) { - fclose(out); + if (out != -1) { + close(out); } if (procStatm) { fclose(procStatm); } if (stopCallback && (!s_atexit || s_forceCleanup)) { stopCallback(); } debugLog("%s", "done destroying LockedData"); } /** - * Note: We use the C stdio API here for performance reasons. + * Note: We use the C API here for performance reasons. * Esp. in multi-threaded environments this is much faster * to produce non-per-line-interleaved output. + * Note: We use non-buffered API to workaround https://bugs.kde.org/show_bug.cgi?id=393387 */ - FILE* out = nullptr; + int out = -1; + unsigned bufferSize = 0; + unique_ptr buffer; /// /proc/self/statm file stream to read RSS value from FILE* procStatm = nullptr; /** * Calls to dlopen/dlclose mark the cache as dirty. * When this happened, all modules and their section addresses * must be found again via dl_iterate_phdr before we output the * next instruction pointer. Otherwise, heaptrack_interpret might * encounter IPs of an unknown/invalid module. */ bool moduleCacheDirty = true; TraceTree traceTree; atomic stopTimerThread{false}; thread timerThread; heaptrack_callback_t stopCallback = nullptr; #ifdef DEBUG_MALLOC_PTRS unordered_set known; #endif }; static Lock s_lock; static LockedData* s_data; }; Lock HeapTrack::s_lock; HeapTrack::LockedData* HeapTrack::s_data{nullptr}; } extern "C" { void heaptrack_init(const char* outputFileName, heaptrack_callback_t initBeforeCallback, heaptrack_callback_initialized_t initAfterCallback, heaptrack_callback_t stopCallback) { RecursionGuard guard; // initialize startTime(); debugLog("heaptrack_init(%s)", outputFileName); HeapTrack heaptrack(guard); heaptrack.initialize(outputFileName, initBeforeCallback, initAfterCallback, stopCallback); } void heaptrack_stop() { RecursionGuard guard; debugLog("%s", "heaptrack_stop()"); HeapTrack heaptrack(guard); if (!s_atexit) { s_forceCleanup.store(true); } heaptrack.shutdown(); } void heaptrack_malloc(void* ptr, size_t size) { if (ptr && !RecursionGuard::isActive) { RecursionGuard guard; debugLog("heaptrack_malloc(%p, %zu)", ptr, size); Trace trace; trace.fill(2 + HEAPTRACK_DEBUG_BUILD); HeapTrack heaptrack(guard); heaptrack.handleMalloc(ptr, size, trace); } } void heaptrack_free(void* ptr) { if (ptr && !RecursionGuard::isActive) { RecursionGuard guard; debugLog("heaptrack_free(%p)", ptr); HeapTrack heaptrack(guard); heaptrack.handleFree(ptr); } } void heaptrack_realloc(void* ptr_in, size_t size, void* ptr_out) { if (ptr_out && !RecursionGuard::isActive) { RecursionGuard guard; debugLog("heaptrack_realloc(%p, %zu, %p)", ptr_in, size, ptr_out); Trace trace; trace.fill(2 + HEAPTRACK_DEBUG_BUILD); HeapTrack heaptrack(guard); if (ptr_in) { heaptrack.handleFree(ptr_in); } heaptrack.handleMalloc(ptr_out, size, trace); } } void heaptrack_invalidate_module_cache() { RecursionGuard guard; debugLog("%s", "heaptrack_invalidate_module_cache()"); HeapTrack heaptrack(guard); heaptrack.invalidateModuleCache(); } } diff --git a/src/track/libheaptrack.h b/src/track/libheaptrack.h index dc9cbfa..fdae968 100644 --- a/src/track/libheaptrack.h +++ b/src/track/libheaptrack.h @@ -1,43 +1,43 @@ /* * Copyright 2014-2017 Milian Wolff * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include #ifdef __cplusplus extern "C" { #endif typedef void (*heaptrack_callback_t)(); -typedef void (*heaptrack_callback_initialized_t)(FILE*); +typedef void (*heaptrack_callback_initialized_t)(int); void heaptrack_init(const char* outputFileName, heaptrack_callback_t initCallbackBefore, heaptrack_callback_initialized_t initCallbackAfter, heaptrack_callback_t stopCallback); void heaptrack_stop(); void heaptrack_malloc(void* ptr, size_t size); void heaptrack_free(void* ptr); void heaptrack_realloc(void* ptr_in, size_t size, void* ptr_out); void heaptrack_invalidate_module_cache(); #ifdef __cplusplus } #endif diff --git a/tests/auto/tst_libheaptrack.cpp b/tests/auto/tst_libheaptrack.cpp index c18f1ab..064b7fd 100644 --- a/tests/auto/tst_libheaptrack.cpp +++ b/tests/auto/tst_libheaptrack.cpp @@ -1,136 +1,139 @@ /* * Copyright 2018 Milian Wolff * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "3rdparty/catch.hpp" #include "src/track/libheaptrack.h" #include #include #include #include #include #include #include bool initBeforeCalled = false; bool initAfterCalled = false; -FILE* out = nullptr; +int out = -1; bool stopCalled = false; using namespace std; struct TempFile { TempFile() : path(boost::filesystem::unique_path()) , fileName(path.native()) { } ~TempFile() { boost::filesystem::remove(path); } const boost::filesystem::path path; const string fileName; }; TEST_CASE ("api") { TempFile tmp; SECTION ("init") { - heaptrack_init(tmp.fileName.c_str(), [](){ - REQUIRE(!initBeforeCalled); - REQUIRE(!initAfterCalled); - REQUIRE(!stopCalled); - initBeforeCalled = true; - }, [](FILE* file) { - REQUIRE(initBeforeCalled); - REQUIRE(!initAfterCalled); - REQUIRE(!stopCalled); - REQUIRE(file); - out = file; - initAfterCalled = true; - }, []() { - REQUIRE(initBeforeCalled); - REQUIRE(initAfterCalled); - REQUIRE(!stopCalled); - stopCalled = true; - out = nullptr; - }); + heaptrack_init(tmp.fileName.c_str(), + []() { + REQUIRE(!initBeforeCalled); + REQUIRE(!initAfterCalled); + REQUIRE(!stopCalled); + initBeforeCalled = true; + }, + [](int fd) { + REQUIRE(initBeforeCalled); + REQUIRE(!initAfterCalled); + REQUIRE(!stopCalled); + REQUIRE(fd != -1); + out = fd; + initAfterCalled = true; + }, + []() { + REQUIRE(initBeforeCalled); + REQUIRE(initAfterCalled); + REQUIRE(!stopCalled); + stopCalled = true; + out = -1; + }); REQUIRE(initBeforeCalled); REQUIRE(initAfterCalled); REQUIRE(!stopCalled); int data[2] = {0}; SECTION ("no-op-malloc") { heaptrack_malloc(0, 0); } SECTION ("no-op-malloc-free") { heaptrack_free(0); } SECTION ("no-op-malloc-realloc") { heaptrack_realloc(data, 1, 0); } SECTION ("malloc-free") { heaptrack_malloc(data, 4); heaptrack_free(data); } SECTION ("realloc") { heaptrack_malloc(data, 4); heaptrack_realloc(data, 8, data); heaptrack_realloc(data, 16, data + 1); heaptrack_free(data + 1); } SECTION ("invalidate-cache") { heaptrack_invalidate_module_cache(); } SECTION ("multi-threaded") { const auto numThreads = min(4u, thread::hardware_concurrency()); cout << "start threads" << endl; { vector> futures; for (unsigned i = 0; i < numThreads; ++i) { futures.emplace_back(async(launch::async, [](){ for (int i = 0; i < 10000; ++i) { heaptrack_malloc(&i, i); heaptrack_realloc(&i, i + 1, &i); heaptrack_free(&i); if (i % 100 == 0) { heaptrack_invalidate_module_cache(); } } })); } } cout << "threads finished" << endl; } SECTION ("stop") { heaptrack_stop(); REQUIRE(stopCalled); } } }