From 9f96c66ddd42e3dd7aafc1ec727ecd6100f0cea2 Mon Sep 17 00:00:00 2001 From: Timothy Bess Date: Mon, 3 Feb 2025 16:51:08 -0500 Subject: [PATCH] Conditionally compile in heap instrumentation to find allocation hotspots. Signed-off-by: Timothy Bess --- cpp/perspective/CMakeLists.txt | 49 +++- cpp/perspective/build.js | 4 +- cpp/perspective/src/cpp/binding_api.cpp | 16 +- cpp/perspective/src/cpp/heap_instruments.cpp | 234 ++++++++++++++++++ .../include/perspective/heap_instruments.h | 59 +++++ .../src/ts/wasm/emscripten_api.ts | 3 + 6 files changed, 357 insertions(+), 8 deletions(-) create mode 100644 cpp/perspective/src/cpp/heap_instruments.cpp create mode 100644 cpp/perspective/src/include/perspective/heap_instruments.h diff --git a/cpp/perspective/CMakeLists.txt b/cpp/perspective/CMakeLists.txt index 9d17846a7b..fca91b04fb 100644 --- a/cpp/perspective/CMakeLists.txt +++ b/cpp/perspective/CMakeLists.txt @@ -86,6 +86,7 @@ option(PSP_CPP_BUILD "Build the C++ Project" OFF) option(PSP_PYTHON_BUILD "Build the Python Bindings" OFF) option(PSP_CPP_BUILD_STRICT "Build the C++ with strict warnings" OFF) option(PSP_SANITIZE "Build with sanitizers" OFF) +option(PSP_HEAP_INSTRUMENTS "Build with heap inspection tooling" OFF) if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") set(PSP_WASM_BUILD ON) @@ -107,6 +108,12 @@ else() endif() endif() +if(DEFINED ENV{PSP_HEAP_INSTRUMENTS}) + set(PSP_HEAP_INSTRUMENTS ON) +else() + set(PSP_HEAP_INSTRUMENTS OFF) +endif() + if(DEFINED ENV{PSP_MANYLINUX}) set(MANYLINUX ON) else() @@ -201,6 +208,11 @@ if(NOT DEFINED PSP_WASM_EXCEPTIONS AND NOT PSP_PYTHON_BUILD) set(PSP_WASM_EXCEPTIONS ON) endif() +set(DEBUG_LEVEL "0") +if(PSP_HEAP_INSTRUMENTS) + set(DEBUG_LEVEL "3") +endif() + if(PSP_WASM_BUILD) #################### # EMSCRIPTEN BUILD # @@ -231,7 +243,7 @@ if(PSP_WASM_BUILD) ") endif() else() - set(OPT_FLAGS " -O3 -g0 ") + set(OPT_FLAGS " -O3 -g${DEBUG_LEVEL} ") if (PSP_WASM_EXCEPTIONS) set(OPT_FLAGS "${OPT_FLAGS} -fwasm-exceptions -flto --emit-tsd=perspective-server.d.ts ") endif() @@ -301,12 +313,12 @@ endif() if (PSP_WASM_EXCEPTIONS) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} \ -O3 \ - -g0 \ + -g${DEBUG_LEVEL} \ ") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ -fwasm-exceptions \ -O3 \ - -g0 \ + -g${DEBUG_LEVEL} \ ") else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ @@ -498,6 +510,11 @@ set(SOURCE_FILES ${PSP_CPP_SRC}/src/cpp/binding_api.cpp ) +if(PSP_HEAP_INSTRUMENTS) + list(APPEND SOURCE_FILES ${PSP_CPP_SRC}/src/cpp/heap_instruments.cpp) + add_compile_definitions(HEAP_INSTRUMENTS=1) +endif() + set(PYTHON_SOURCE_FILES ${SOURCE_FILES}) set(WASM_SOURCE_FILES ${SOURCE_FILES}) @@ -509,11 +526,33 @@ else() # set(CMAKE_CXX_FLAGS " ${CMAKE_CXX_FLAGS}") endif() +set(PSP_EXPORTED_FUNCTIONS + _psp_poll + _psp_new_server + _psp_free + _psp_alloc + _psp_handle_request + _psp_new_session + _psp_close_session + _psp_delete_server + _psp_is_memory64 +) + +if(PSP_HEAP_INSTRUMENTS) + list(APPEND PSP_EXPORTED_FUNCTIONS + _psp_print_used_memory + _psp_dump_stack_traces + _psp_clear_stack_traces + ) +endif() + +string(JOIN "," PSP_EXPORTED_FUNCTIONS_JOINED ${PSP_EXPORTED_FUNCTIONS}) + # Common flags for WASM/JS build and Pyodide if(PSP_PYODIDE) set(PSP_WASM_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} \ --no-entry \ - -s EXPORTED_FUNCTIONS=_psp_poll,_psp_new_server,_psp_free,_psp_alloc,_psp_handle_request,_psp_new_session,_psp_close_session,_psp_delete_server,_psp_is_memory64 \ + -s EXPORTED_FUNCTIONS=${PSP_EXPORTED_FUNCTIONS_JOINED} \ -s SIDE_MODULE=2 \ ") else() @@ -537,7 +576,7 @@ else() -s NODEJS_CATCH_REJECTION=0 \ -s USE_ES6_IMPORT_META=1 \ -s EXPORT_ES6=1 \ - -s EXPORTED_FUNCTIONS=_psp_poll,_psp_new_server,_psp_free,_psp_alloc,_psp_handle_request,_psp_new_session,_psp_close_session,_psp_delete_server,_psp_is_memory64 \ + -s EXPORTED_FUNCTIONS=${PSP_EXPORTED_FUNCTIONS_JOINED} \ ") if(PSP_WASM64) diff --git a/cpp/perspective/build.js b/cpp/perspective/build.js index 2092acba68..c42bd601ea 100644 --- a/cpp/perspective/build.js +++ b/cpp/perspective/build.js @@ -61,7 +61,9 @@ try { execSync(`cpy web/**/* ../web`, { cwd, stdio }); execSync(`cpy node/**/* ../node`, { cwd, stdio }); - bootstrap(`../../cpp/perspective/dist/web/perspective-server.wasm`); + if (!process.env.PSP_HEAP_INSTRUMENTS) { + bootstrap(`../../cpp/perspective/dist/web/perspective-server.wasm`); + } } catch (e) { console.error(e); process.exit(1); diff --git a/cpp/perspective/src/cpp/binding_api.cpp b/cpp/perspective/src/cpp/binding_api.cpp index 46f791363c..fd189440ca 100644 --- a/cpp/perspective/src/cpp/binding_api.cpp +++ b/cpp/perspective/src/cpp/binding_api.cpp @@ -18,6 +18,16 @@ #include #include +#if HEAP_INSTRUMENTS +#include + +#define UNINSTRUMENTED_MALLOC(x) emscripten_builtin_malloc(x) +#define UNINSTRUMENTED_FREE(x) emscripten_builtin_free(x) +#else +#define UNINSTRUMENTED_MALLOC(x) malloc(x) +#define UNINSTRUMENTED_FREE(x) free(x) +#endif + using namespace perspective::server; #pragma pack(push, 1) @@ -102,14 +112,16 @@ psp_close_session(ProtoServer* server, std::uint32_t client_id) { PERSPECTIVE_EXPORT std::size_t psp_alloc(std::size_t size) { - auto* mem = (char*)malloc(size); + // We use this to allocate stack traces for instrumentation with heap + // profiling builds. + auto* mem = (char*)UNINSTRUMENTED_MALLOC(size); return (size_t)mem; } PERSPECTIVE_EXPORT void psp_free(void* ptr) { - free(ptr); + UNINSTRUMENTED_FREE(ptr); } PERSPECTIVE_EXPORT diff --git a/cpp/perspective/src/cpp/heap_instruments.cpp b/cpp/perspective/src/cpp/heap_instruments.cpp new file mode 100644 index 0000000000..9c54ae9fe2 --- /dev/null +++ b/cpp/perspective/src/cpp/heap_instruments.cpp @@ -0,0 +1,234 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +#include "perspective/base.h" +#include "perspective/heap_instruments.h" +#include +#include +#include +#include +#include +#include +#include +#include + +static std::uint64_t USED_MEMORY = 0; + +static constexpr std::uint64_t MIN_RELEVANT_SIZE = 5 * 1024 * 1024; + +extern "C" void +psp_print_used_memory() { + printf("Used memory: %llu\n", USED_MEMORY); +} + +struct Header { + std::uint64_t size; +}; + +using UnderlyingString = + std::basic_string, UnderlyingAllocator>; + +using UnderlyingIStringStream = std::basic_istringstream< + char, + std::char_traits, + UnderlyingAllocator>; + +struct AllocMeta { + Header header; + const UnderlyingString* trace; + std::uint64_t size; +}; + +static std::unordered_map< + UnderlyingString, + AllocMeta, + std::hash, + std::equal_to<>, + UnderlyingAllocator>> + stack_traces; + +static UnderlyingString IRRELEVANT = "irrelevant"; + +static inline void +record_stack_trace(Header* header, std::uint64_t size) { + if (size >= MIN_RELEVANT_SIZE) { + const char* stack_c_str = perspective::psp_stack_trace(); + UnderlyingIStringStream stack(stack_c_str); + UnderlyingString line; + UnderlyingString out; + + while (std::getline(stack, line)) { + line = line.substr(0, line.find_last_of(" (")); + out += line + "\n"; + } + + emscripten_builtin_free(const_cast(stack_c_str)); + + // stack_traces[ptr] = {.header = *header, .trace = out}; + if (stack_traces.find(out) == stack_traces.end()) { + stack_traces[out] = + AllocMeta{.header = *header, .trace = nullptr, .size = size}; + + stack_traces[out].trace = &stack_traces.find(out)->first; + } else { + stack_traces[out].size += size; + } + } else { + if (stack_traces.find(IRRELEVANT) == stack_traces.end()) { + stack_traces[IRRELEVANT] = AllocMeta{ + .header = *header, .trace = &IRRELEVANT, .size = size + }; + } else { + stack_traces[IRRELEVANT].size += size; + } + } +} + +extern "C" void +psp_dump_stack_traces() { + std::vector> metas; + metas.reserve(stack_traces.size()); + for (const auto& [_, meta] : stack_traces) { + metas.push_back(meta); + } + std::sort( + metas.begin(), + metas.end(), + [](const AllocMeta& a, const AllocMeta& b) { + return a.header.size > b.header.size; + } + ); + for (const auto& meta : metas) { + printf("Allocated %llu bytes\n", meta.header.size); + printf("Stacktrace:\n%s\n", meta.trace->c_str()); + } +} + +extern "C" void +psp_clear_stack_traces() { + stack_traces.clear(); +} + +void* +malloc(size_t size) { + if (size > MIN_RELEVANT_SIZE) { + printf("Allocating %zu bytes\n", size); + } + USED_MEMORY += size; + const size_t total_size = size + sizeof(Header); + auto* header = static_cast(emscripten_builtin_malloc(total_size)); + if (header == nullptr) { + fprintf(stderr, "Failed to allocate %zu bytes\n", size); + } + header->size = size; + record_stack_trace(header, size); + return header + 1; +} + +void* +calloc(size_t nmemb, size_t size) { + // printf("Allocating array: %zu elements of size %zu\n", nmemb, size); + USED_MEMORY += nmemb * size; + // return emscripten_builtin_calloc(nmemb, size); + const size_t total_size = (nmemb * size) + sizeof(Header); + auto* header = static_cast(emscripten_builtin_malloc(total_size)); + if (header == nullptr) { + fprintf(stderr, "Failed to allocate %zu bytes\n", size); + } + header->size = nmemb * size; + memset(header + 1, 0, nmemb * size); + record_stack_trace(header, size); + return header + 1; +} + +void +free(void* ptr) { + // printf("Freeing memory at %p\n", ptr); + + if (ptr == nullptr) { + emscripten_builtin_free(ptr); + } else { + auto* header = static_cast(ptr) - 1; + auto old_memory = USED_MEMORY; + USED_MEMORY -= header->size; + if (USED_MEMORY > old_memory) { + std::abort(); + } + emscripten_builtin_free(header); + } +} + +void* +memalign(size_t alignment, size_t size) { + const size_t total_size = size + sizeof(Header); + auto* header = + static_cast(emscripten_builtin_memalign(alignment, total_size) + ); + if (header == nullptr) { + fprintf(stderr, "Failed to allocate %zu bytes\n", size); + } + header->size = size; + record_stack_trace(header, size); + return header + 1; +} + +int +posix_memalign(void** memptr, size_t alignment, size_t size) { + auto* header = static_cast( + emscripten_builtin_memalign(alignment, size + sizeof(Header)) + ); + if (header == nullptr) { + fprintf(stderr, "Failed to allocate %zu bytes\n", size); + } + header->size = size; + USED_MEMORY += size; + record_stack_trace(header, size); + *memptr = header + 1; + return 0; +} + +void* +realloc(void* ptr, size_t new_size) { + if (ptr == nullptr) { + // If ptr is nullptr, realloc behaves like malloc + return malloc(new_size); + } + + if (new_size == 0) { + // If new_size is 0, realloc behaves like free + free(ptr); + return nullptr; + } + + auto* header = static_cast(ptr) - 1; + const size_t old_size = header->size; + + if (new_size <= old_size) { + USED_MEMORY -= old_size - new_size; + // If the new size is smaller or equal, we can potentially shrink the + // block in place. For simplicity, we don't actually shrink the block + // here. + header->size = new_size; // Update the size in the header + return ptr; // Return the same pointer + } + + // If the new size is larger, allocate a new block + void* new_ptr = malloc(new_size); + if (new_ptr == nullptr) { + return nullptr; + } + + memcpy(new_ptr, ptr, old_size); + free(ptr); + + return new_ptr; +} \ No newline at end of file diff --git a/cpp/perspective/src/include/perspective/heap_instruments.h b/cpp/perspective/src/include/perspective/heap_instruments.h new file mode 100644 index 0000000000..d90df7bb51 --- /dev/null +++ b/cpp/perspective/src/include/perspective/heap_instruments.h @@ -0,0 +1,59 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +#include +#include +#include +#include + +// Don't track our own memory usage +template +class UnderlyingAllocator : std::allocator { +public: + using value_type = T; + + UnderlyingAllocator() = default; + + template + UnderlyingAllocator(const UnderlyingAllocator& /*unused*/) noexcept {} + + T* + allocate(std::size_t n) { + if (n > std::numeric_limits::max() / sizeof(T)) { + throw std::bad_alloc(); + } + void* ptr = emscripten_builtin_malloc(n * sizeof(T)); + if (ptr == nullptr) { + throw std::bad_alloc(); + } + return static_cast(ptr); + } + + // Deallocate memory + void + deallocate(T* ptr, std::size_t /*unused*/) noexcept { + emscripten_builtin_free(ptr); + } + + // Equality comparison (required for allocators) + template + bool + operator==(const UnderlyingAllocator& /*unused*/) const noexcept { + return true; + } + + template + bool + operator!=(const UnderlyingAllocator& other) const noexcept { + return !(*this == other); + } +}; diff --git a/rust/perspective-js/src/ts/wasm/emscripten_api.ts b/rust/perspective-js/src/ts/wasm/emscripten_api.ts index aa222f514b..df1d4b33ae 100644 --- a/rust/perspective-js/src/ts/wasm/emscripten_api.ts +++ b/rust/perspective-js/src/ts/wasm/emscripten_api.ts @@ -31,7 +31,10 @@ export async function compile_perspective( imports["env"] = { ...imports["env"], psp_stack_trace() { + const old = Error.stackTraceLimit; + Error.stackTraceLimit = 1000; const str = Error().stack || ""; + Error.stackTraceLimit = old; const textEncoder = new TextEncoder(); const bytes = textEncoder.encode(str); const ptr = module._psp_alloc(