Skip to content

Commit 1b88ca8

Browse files
authored
Merge pull request #2913 from timbess/feature/heap-profiling
Heap Profiling Tooling
2 parents 69f2fdd + 9f96c66 commit 1b88ca8

File tree

6 files changed

+357
-8
lines changed

6 files changed

+357
-8
lines changed

cpp/perspective/CMakeLists.txt

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ option(PSP_CPP_BUILD "Build the C++ Project" OFF)
8686
option(PSP_PYTHON_BUILD "Build the Python Bindings" OFF)
8787
option(PSP_CPP_BUILD_STRICT "Build the C++ with strict warnings" OFF)
8888
option(PSP_SANITIZE "Build with sanitizers" OFF)
89+
option(PSP_HEAP_INSTRUMENTS "Build with heap inspection tooling" OFF)
8990

9091
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
9192
set(PSP_WASM_BUILD ON)
@@ -107,6 +108,12 @@ else()
107108
endif()
108109
endif()
109110

111+
if(DEFINED ENV{PSP_HEAP_INSTRUMENTS})
112+
set(PSP_HEAP_INSTRUMENTS ON)
113+
else()
114+
set(PSP_HEAP_INSTRUMENTS OFF)
115+
endif()
116+
110117
if(DEFINED ENV{PSP_MANYLINUX})
111118
set(MANYLINUX ON)
112119
else()
@@ -201,6 +208,11 @@ if(NOT DEFINED PSP_WASM_EXCEPTIONS AND NOT PSP_PYTHON_BUILD)
201208
set(PSP_WASM_EXCEPTIONS ON)
202209
endif()
203210

211+
set(DEBUG_LEVEL "0")
212+
if(PSP_HEAP_INSTRUMENTS)
213+
set(DEBUG_LEVEL "3")
214+
endif()
215+
204216
if(PSP_WASM_BUILD)
205217
####################
206218
# EMSCRIPTEN BUILD #
@@ -231,7 +243,7 @@ if(PSP_WASM_BUILD)
231243
")
232244
endif()
233245
else()
234-
set(OPT_FLAGS " -O3 -g0 ")
246+
set(OPT_FLAGS " -O3 -g${DEBUG_LEVEL} ")
235247
if (PSP_WASM_EXCEPTIONS)
236248
set(OPT_FLAGS "${OPT_FLAGS} -fwasm-exceptions -flto --emit-tsd=perspective-server.d.ts ")
237249
endif()
@@ -301,12 +313,12 @@ endif()
301313
if (PSP_WASM_EXCEPTIONS)
302314
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} \
303315
-O3 \
304-
-g0 \
316+
-g${DEBUG_LEVEL} \
305317
")
306318
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \
307319
-fwasm-exceptions \
308320
-O3 \
309-
-g0 \
321+
-g${DEBUG_LEVEL} \
310322
")
311323
else()
312324
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \
@@ -498,6 +510,11 @@ set(SOURCE_FILES
498510
${PSP_CPP_SRC}/src/cpp/binding_api.cpp
499511
)
500512

513+
if(PSP_HEAP_INSTRUMENTS)
514+
list(APPEND SOURCE_FILES ${PSP_CPP_SRC}/src/cpp/heap_instruments.cpp)
515+
add_compile_definitions(HEAP_INSTRUMENTS=1)
516+
endif()
517+
501518
set(PYTHON_SOURCE_FILES ${SOURCE_FILES})
502519
set(WASM_SOURCE_FILES ${SOURCE_FILES})
503520

@@ -509,11 +526,33 @@ else()
509526
# set(CMAKE_CXX_FLAGS " ${CMAKE_CXX_FLAGS}")
510527
endif()
511528

529+
set(PSP_EXPORTED_FUNCTIONS
530+
_psp_poll
531+
_psp_new_server
532+
_psp_free
533+
_psp_alloc
534+
_psp_handle_request
535+
_psp_new_session
536+
_psp_close_session
537+
_psp_delete_server
538+
_psp_is_memory64
539+
)
540+
541+
if(PSP_HEAP_INSTRUMENTS)
542+
list(APPEND PSP_EXPORTED_FUNCTIONS
543+
_psp_print_used_memory
544+
_psp_dump_stack_traces
545+
_psp_clear_stack_traces
546+
)
547+
endif()
548+
549+
string(JOIN "," PSP_EXPORTED_FUNCTIONS_JOINED ${PSP_EXPORTED_FUNCTIONS})
550+
512551
# Common flags for WASM/JS build and Pyodide
513552
if(PSP_PYODIDE)
514553
set(PSP_WASM_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} \
515554
--no-entry \
516-
-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 \
555+
-s EXPORTED_FUNCTIONS=${PSP_EXPORTED_FUNCTIONS_JOINED} \
517556
-s SIDE_MODULE=2 \
518557
")
519558
else()
@@ -537,7 +576,7 @@ else()
537576
-s NODEJS_CATCH_REJECTION=0 \
538577
-s USE_ES6_IMPORT_META=1 \
539578
-s EXPORT_ES6=1 \
540-
-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 \
579+
-s EXPORTED_FUNCTIONS=${PSP_EXPORTED_FUNCTIONS_JOINED} \
541580
")
542581

543582
if(PSP_WASM64)

cpp/perspective/build.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ try {
6161

6262
execSync(`cpy web/**/* ../web`, { cwd, stdio });
6363
execSync(`cpy node/**/* ../node`, { cwd, stdio });
64-
bootstrap(`../../cpp/perspective/dist/web/perspective-server.wasm`);
64+
if (!process.env.PSP_HEAP_INSTRUMENTS) {
65+
bootstrap(`../../cpp/perspective/dist/web/perspective-server.wasm`);
66+
}
6567
} catch (e) {
6668
console.error(e);
6769
process.exit(1);

cpp/perspective/src/cpp/binding_api.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@
1818
#include <string>
1919
#include <tsl/hopscotch_map.h>
2020

21+
#if HEAP_INSTRUMENTS
22+
#include <emscripten/heap.h>
23+
24+
#define UNINSTRUMENTED_MALLOC(x) emscripten_builtin_malloc(x)
25+
#define UNINSTRUMENTED_FREE(x) emscripten_builtin_free(x)
26+
#else
27+
#define UNINSTRUMENTED_MALLOC(x) malloc(x)
28+
#define UNINSTRUMENTED_FREE(x) free(x)
29+
#endif
30+
2131
using namespace perspective::server;
2232

2333
#pragma pack(push, 1)
@@ -102,14 +112,16 @@ psp_close_session(ProtoServer* server, std::uint32_t client_id) {
102112
PERSPECTIVE_EXPORT
103113
std::size_t
104114
psp_alloc(std::size_t size) {
105-
auto* mem = (char*)malloc(size);
115+
// We use this to allocate stack traces for instrumentation with heap
116+
// profiling builds.
117+
auto* mem = (char*)UNINSTRUMENTED_MALLOC(size);
106118
return (size_t)mem;
107119
}
108120

109121
PERSPECTIVE_EXPORT
110122
void
111123
psp_free(void* ptr) {
112-
free(ptr);
124+
UNINSTRUMENTED_FREE(ptr);
113125
}
114126

115127
PERSPECTIVE_EXPORT
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
10+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12+
13+
#include "perspective/base.h"
14+
#include "perspective/heap_instruments.h"
15+
#include <cstddef>
16+
#include <cstdio>
17+
#include <cstdlib>
18+
#include <emscripten/emscripten.h>
19+
#include <emscripten/heap.h>
20+
#include <emscripten/em_asm.h>
21+
#include <emscripten/stack.h>
22+
#include <string>
23+
24+
static std::uint64_t USED_MEMORY = 0;
25+
26+
static constexpr std::uint64_t MIN_RELEVANT_SIZE = 5 * 1024 * 1024;
27+
28+
extern "C" void
29+
psp_print_used_memory() {
30+
printf("Used memory: %llu\n", USED_MEMORY);
31+
}
32+
33+
struct Header {
34+
std::uint64_t size;
35+
};
36+
37+
using UnderlyingString =
38+
std::basic_string<char, std::char_traits<char>, UnderlyingAllocator<char>>;
39+
40+
using UnderlyingIStringStream = std::basic_istringstream<
41+
char,
42+
std::char_traits<char>,
43+
UnderlyingAllocator<char>>;
44+
45+
struct AllocMeta {
46+
Header header;
47+
const UnderlyingString* trace;
48+
std::uint64_t size;
49+
};
50+
51+
static std::unordered_map<
52+
UnderlyingString,
53+
AllocMeta,
54+
std::hash<UnderlyingString>,
55+
std::equal_to<>,
56+
UnderlyingAllocator<std::pair<UnderlyingString const, AllocMeta>>>
57+
stack_traces;
58+
59+
static UnderlyingString IRRELEVANT = "irrelevant";
60+
61+
static inline void
62+
record_stack_trace(Header* header, std::uint64_t size) {
63+
if (size >= MIN_RELEVANT_SIZE) {
64+
const char* stack_c_str = perspective::psp_stack_trace();
65+
UnderlyingIStringStream stack(stack_c_str);
66+
UnderlyingString line;
67+
UnderlyingString out;
68+
69+
while (std::getline(stack, line)) {
70+
line = line.substr(0, line.find_last_of(" ("));
71+
out += line + "\n";
72+
}
73+
74+
emscripten_builtin_free(const_cast<char*>(stack_c_str));
75+
76+
// stack_traces[ptr] = {.header = *header, .trace = out};
77+
if (stack_traces.find(out) == stack_traces.end()) {
78+
stack_traces[out] =
79+
AllocMeta{.header = *header, .trace = nullptr, .size = size};
80+
81+
stack_traces[out].trace = &stack_traces.find(out)->first;
82+
} else {
83+
stack_traces[out].size += size;
84+
}
85+
} else {
86+
if (stack_traces.find(IRRELEVANT) == stack_traces.end()) {
87+
stack_traces[IRRELEVANT] = AllocMeta{
88+
.header = *header, .trace = &IRRELEVANT, .size = size
89+
};
90+
} else {
91+
stack_traces[IRRELEVANT].size += size;
92+
}
93+
}
94+
}
95+
96+
extern "C" void
97+
psp_dump_stack_traces() {
98+
std::vector<AllocMeta, UnderlyingAllocator<AllocMeta>> metas;
99+
metas.reserve(stack_traces.size());
100+
for (const auto& [_, meta] : stack_traces) {
101+
metas.push_back(meta);
102+
}
103+
std::sort(
104+
metas.begin(),
105+
metas.end(),
106+
[](const AllocMeta& a, const AllocMeta& b) {
107+
return a.header.size > b.header.size;
108+
}
109+
);
110+
for (const auto& meta : metas) {
111+
printf("Allocated %llu bytes\n", meta.header.size);
112+
printf("Stacktrace:\n%s\n", meta.trace->c_str());
113+
}
114+
}
115+
116+
extern "C" void
117+
psp_clear_stack_traces() {
118+
stack_traces.clear();
119+
}
120+
121+
void*
122+
malloc(size_t size) {
123+
if (size > MIN_RELEVANT_SIZE) {
124+
printf("Allocating %zu bytes\n", size);
125+
}
126+
USED_MEMORY += size;
127+
const size_t total_size = size + sizeof(Header);
128+
auto* header = static_cast<Header*>(emscripten_builtin_malloc(total_size));
129+
if (header == nullptr) {
130+
fprintf(stderr, "Failed to allocate %zu bytes\n", size);
131+
}
132+
header->size = size;
133+
record_stack_trace(header, size);
134+
return header + 1;
135+
}
136+
137+
void*
138+
calloc(size_t nmemb, size_t size) {
139+
// printf("Allocating array: %zu elements of size %zu\n", nmemb, size);
140+
USED_MEMORY += nmemb * size;
141+
// return emscripten_builtin_calloc(nmemb, size);
142+
const size_t total_size = (nmemb * size) + sizeof(Header);
143+
auto* header = static_cast<Header*>(emscripten_builtin_malloc(total_size));
144+
if (header == nullptr) {
145+
fprintf(stderr, "Failed to allocate %zu bytes\n", size);
146+
}
147+
header->size = nmemb * size;
148+
memset(header + 1, 0, nmemb * size);
149+
record_stack_trace(header, size);
150+
return header + 1;
151+
}
152+
153+
void
154+
free(void* ptr) {
155+
// printf("Freeing memory at %p\n", ptr);
156+
157+
if (ptr == nullptr) {
158+
emscripten_builtin_free(ptr);
159+
} else {
160+
auto* header = static_cast<Header*>(ptr) - 1;
161+
auto old_memory = USED_MEMORY;
162+
USED_MEMORY -= header->size;
163+
if (USED_MEMORY > old_memory) {
164+
std::abort();
165+
}
166+
emscripten_builtin_free(header);
167+
}
168+
}
169+
170+
void*
171+
memalign(size_t alignment, size_t size) {
172+
const size_t total_size = size + sizeof(Header);
173+
auto* header =
174+
static_cast<Header*>(emscripten_builtin_memalign(alignment, total_size)
175+
);
176+
if (header == nullptr) {
177+
fprintf(stderr, "Failed to allocate %zu bytes\n", size);
178+
}
179+
header->size = size;
180+
record_stack_trace(header, size);
181+
return header + 1;
182+
}
183+
184+
int
185+
posix_memalign(void** memptr, size_t alignment, size_t size) {
186+
auto* header = static_cast<Header*>(
187+
emscripten_builtin_memalign(alignment, size + sizeof(Header))
188+
);
189+
if (header == nullptr) {
190+
fprintf(stderr, "Failed to allocate %zu bytes\n", size);
191+
}
192+
header->size = size;
193+
USED_MEMORY += size;
194+
record_stack_trace(header, size);
195+
*memptr = header + 1;
196+
return 0;
197+
}
198+
199+
void*
200+
realloc(void* ptr, size_t new_size) {
201+
if (ptr == nullptr) {
202+
// If ptr is nullptr, realloc behaves like malloc
203+
return malloc(new_size);
204+
}
205+
206+
if (new_size == 0) {
207+
// If new_size is 0, realloc behaves like free
208+
free(ptr);
209+
return nullptr;
210+
}
211+
212+
auto* header = static_cast<Header*>(ptr) - 1;
213+
const size_t old_size = header->size;
214+
215+
if (new_size <= old_size) {
216+
USED_MEMORY -= old_size - new_size;
217+
// If the new size is smaller or equal, we can potentially shrink the
218+
// block in place. For simplicity, we don't actually shrink the block
219+
// here.
220+
header->size = new_size; // Update the size in the header
221+
return ptr; // Return the same pointer
222+
}
223+
224+
// If the new size is larger, allocate a new block
225+
void* new_ptr = malloc(new_size);
226+
if (new_ptr == nullptr) {
227+
return nullptr;
228+
}
229+
230+
memcpy(new_ptr, ptr, old_size);
231+
free(ptr);
232+
233+
return new_ptr;
234+
}

0 commit comments

Comments
 (0)