Skip to content

Commit 53c5793

Browse files
authored
chore(profiling): use absail instead of cwisstable (#15424)
## Description <!-- Provide an overview of the change and motivation for the change --> ## Testing <!-- Describe your testing strategy or note what tests are included --> ## Risks <!-- Note any risks associated with this change, or "None" if no risks --> ## Additional Notes <!-- Any other information that would be helpful for reviewers -->
1 parent 2cfb922 commit 53c5793

File tree

6 files changed

+254
-3525
lines changed

6 files changed

+254
-3525
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
cmake_minimum_required(VERSION 3.19)
2+
include(FetchContent)
3+
4+
project(_memalloc)
5+
6+
set(CMAKE_CXX_STANDARD 20)
7+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
8+
9+
# Add compile options
10+
add_compile_options(-fPIC -fvisibility=hidden -pthread -Wall -Wextra)
11+
12+
# Platform-specific compile definitions
13+
if(APPLE)
14+
# Fix for newer macOS SDKs that don't define BSD-style types These are needed by Abseil on macOS
15+
add_compile_definitions(_DARWIN_C_SOURCE)
16+
endif()
17+
18+
add_compile_definitions(_POSIX_C_SOURCE=200809L)
19+
20+
# Check the DD_COMPILE_ABSEIL environment variable and build type
21+
if(DEFINED ENV{DD_COMPILE_ABSEIL} AND ("$ENV{DD_COMPILE_ABSEIL}" STREQUAL "0" OR "$ENV{DD_COMPILE_ABSEIL}" STREQUAL
22+
"false"))
23+
message("==============================================================")
24+
message("WARNING: DD_COMPILE_ABSEIL set to 0 or false: not using abseil")
25+
message("==============================================================")
26+
add_definitions(-DDONT_COMPILE_ABSEIL)
27+
elseif(CMAKE_BUILD_TYPE STREQUAL "Debug")
28+
message("=====================================")
29+
message("WARNING: Debug mode: not using abseil")
30+
message("=====================================")
31+
add_definitions(-DDONT_COMPILE_ABSEIL)
32+
else()
33+
message("Release, RelWithDebInfo, or MinSizeRel mode: using abseil (DD_COMPILE_ABSEIL unset or not 0/false)")
34+
FetchContent_Declare(absl URL "https://github.com/abseil/abseil-cpp/archive/refs/tags/20250127.1.zip")
35+
FetchContent_MakeAvailable(absl)
36+
endif()
37+
38+
# Find Python (be flexible about what's available in build environments)
39+
find_package(Python3 COMPONENTS Interpreter Development)
40+
41+
# Make sure we have necessary Python variables
42+
if(NOT Python3_INCLUDE_DIRS)
43+
# Fallback to PYTHON_INCLUDE_DIRS if Python3_INCLUDE_DIRS not found
44+
if(PYTHON_INCLUDE_DIRS)
45+
set(Python3_INCLUDE_DIRS ${PYTHON_INCLUDE_DIRS})
46+
else()
47+
message(FATAL_ERROR "Python3_INCLUDE_DIRS not found")
48+
endif()
49+
endif()
50+
51+
# Python3::Python target might not exist in all build environments so we'll link using include dirs and let the linker
52+
# find Python dynamically
53+
if(NOT TARGET Python3::Python)
54+
message(STATUS "Python3::Python target not found, using include dirs only")
55+
endif()
56+
57+
# Source files for the extension
58+
set(SOURCE_FILES _memalloc.cpp _memalloc_tb.cpp _memalloc_heap.cpp _memalloc_reentrant.cpp _memalloc_heap_map.cpp)
59+
60+
# Get the extension name from setup.py or use default Note: EXTENSION_NAME from setup.py already includes the full
61+
# suffix
62+
if(DEFINED EXTENSION_NAME)
63+
set(FULL_EXTENSION_NAME "${EXTENSION_NAME}")
64+
else()
65+
set(FULL_EXTENSION_NAME "_memalloc.so")
66+
endif()
67+
68+
# Create the shared library with the full name
69+
add_library(${FULL_EXTENSION_NAME} SHARED ${SOURCE_FILES})
70+
71+
# Set properties to prevent CMake from adding any prefix or suffix
72+
set_target_properties(${FULL_EXTENSION_NAME} PROPERTIES PREFIX "" SUFFIX "")
73+
74+
# Set output directory if specified
75+
if(DEFINED LIB_INSTALL_DIR)
76+
set_target_properties(${FULL_EXTENSION_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${LIB_INSTALL_DIR})
77+
endif()
78+
79+
# Include directories
80+
target_include_directories(
81+
${FULL_EXTENSION_NAME} PRIVATE ${Python3_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}
82+
${CMAKE_CURRENT_SOURCE_DIR}/../../internal/datadog/profiling/dd_wrapper/include)
83+
84+
# Link libraries Python3::Python target might not exist in all build environments (e.g., manylinux) Python modules
85+
# should use -undefined dynamic_lookup on macOS and not link to libpython on Linux
86+
if(TARGET Python3::Python AND NOT APPLE)
87+
target_link_libraries(${FULL_EXTENSION_NAME} PRIVATE Python3::Python)
88+
endif()
89+
90+
# Link Abseil if available
91+
if(NOT (CMAKE_BUILD_TYPE STREQUAL "Debug"
92+
OR (DEFINED ENV{DD_COMPILE_ABSEIL} AND ("$ENV{DD_COMPILE_ABSEIL}" STREQUAL "0" OR "$ENV{DD_COMPILE_ABSEIL}"
93+
STREQUAL "false"))))
94+
target_link_libraries(${FULL_EXTENSION_NAME} PRIVATE absl::flat_hash_map)
95+
endif()
96+
97+
# Platform-specific settings
98+
if(APPLE)
99+
# macOS specific - set rpath for libdd_wrapper and use dynamic lookup for Python symbols
100+
set_target_properties(
101+
${FULL_EXTENSION_NAME}
102+
PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE
103+
INSTALL_RPATH "@loader_path/../../internal/datadog/profiling"
104+
LINK_FLAGS "-undefined dynamic_lookup")
105+
elseif(UNIX)
106+
# Linux specific
107+
target_link_libraries(${FULL_EXTENSION_NAME} PRIVATE atomic)
108+
set_target_properties(${FULL_EXTENSION_NAME} PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE
109+
INSTALL_RPATH "$ORIGIN/../../internal/datadog/profiling")
110+
endif()
111+
112+
# Link with libdd_wrapper if NATIVE_EXTENSION_LOCATION is defined
113+
if(DEFINED NATIVE_EXTENSION_LOCATION)
114+
# Find the libdd_wrapper shared library
115+
find_library(
116+
DD_WRAPPER_LIB
117+
NAMES libdd_wrapper${EXTENSION_SUFFIX}
118+
PATHS ${CMAKE_CURRENT_SOURCE_DIR}/../../internal/datadog/profiling
119+
${NATIVE_EXTENSION_LOCATION}/../../datadog/profiling
120+
NO_DEFAULT_PATH)
121+
122+
if(DD_WRAPPER_LIB)
123+
message(STATUS "Found libdd_wrapper: ${DD_WRAPPER_LIB}")
124+
target_link_libraries(${FULL_EXTENSION_NAME} PRIVATE ${DD_WRAPPER_LIB})
125+
else()
126+
message(WARNING "libdd_wrapper not found, extension may not link correctly")
127+
endif()
128+
endif()
129+
130+
# Add NDEBUG flag for release builds
131+
if(CMAKE_BUILD_TYPE STREQUAL "Release"
132+
OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo"
133+
OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
134+
target_compile_definitions(${FULL_EXTENSION_NAME} PRIVATE NDEBUG)
135+
else()
136+
target_compile_definitions(${FULL_EXTENSION_NAME} PRIVATE UNDEBUG)
137+
endif()
138+
139+
# Install the extension
140+
install(TARGETS ${FULL_EXTENSION_NAME} LIBRARY DESTINATION ${LIB_INSTALL_DIR})
141+
142+
# Optional: Build tests if BUILD_TESTING is ON
143+
option(BUILD_TESTING "Build tests" OFF)
144+
if(BUILD_TESTING)
145+
enable_testing()
146+
add_subdirectory(test)
147+
endif()
Lines changed: 36 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,71 @@
11
#include "_memalloc_heap_map.hpp"
22
#include "_memalloc_debug.h"
33

4-
/* Note that the HeapSample tables will, in general, never free their backing
5-
* memory unless we completely clear them. The table takes 17 bytes per entry: 8
6-
* for the void* keys, 8 for the traceback* values, and 1 byte per entry for
7-
* control metadata. Assuming a load factor target of ~50%, meaning our table
8-
* has roughly twice as many slots as actual entries, then for our default
9-
* maximum of 2^16 entries the table will be about 2MiB. A table this large
10-
* would correspond to a program with a ~65GiB live heap with a 1MiB default
11-
* sampling interval. Most of the memory usage of the profiler will come from
12-
* the tracebacks themselves, which we _do_ free when we're done with them.
4+
/* Note that the heap tracking tables will, in general, never free their backing
5+
* memory unless we completely clear them. Abseil's flat_hash_map uses approximately
6+
* 8 bytes for the key (void*), 8 bytes for the value (traceback_t*), plus metadata.
7+
* With a load factor of ~87.5% (Abseil's default), the table is quite efficient.
8+
* For our default maximum of 2^16 entries, the table will be roughly 2-3 MiB.
9+
* A table this large would correspond to a program with a ~65GiB live heap with
10+
* a 1MiB default sampling interval. Most of the memory usage of the profiler will
11+
* come from the tracebacks themselves, which we _do_ free when we're done with them.
1312
*/
1413

1514
// memalloc_heap_map implementation
16-
memalloc_heap_map::memalloc_heap_map()
17-
: map(HeapSamples_new(0))
18-
{
19-
}
20-
2115
memalloc_heap_map::~memalloc_heap_map()
2216
{
23-
HeapSamples_CIter it = HeapSamples_citer(&map);
24-
for (const HeapSamples_Entry* e = HeapSamples_CIter_get(&it); e != nullptr; e = HeapSamples_CIter_next(&it)) {
25-
delete e->val;
17+
// Delete all traceback objects before map is destroyed
18+
for (auto& [key, tb] : map) {
19+
delete tb;
2620
}
27-
HeapSamples_destroy(&map);
28-
}
29-
30-
size_t
31-
memalloc_heap_map::size() const
32-
{
33-
return HeapSamples_size(&map);
3421
}
3522

3623
traceback_t*
3724
memalloc_heap_map::insert(void* key, traceback_t* value)
3825
{
39-
HeapSamples_Entry k = { .key = key, .val = value };
40-
HeapSamples_Insert res = HeapSamples_insert(&map, &k);
41-
traceback_t* prev = nullptr;
42-
if (!res.inserted) {
26+
// Try to insert the new value
27+
auto [it, inserted] = map.insert({ key, value });
28+
29+
if (!inserted) {
30+
// Key already existed, replace the value
4331
/* This should not happen. It means we did not properly remove a previously-tracked
4432
* allocation from the map. This should probably be an assertion. Return the previous
4533
* entry as it is for an allocation that has been freed. */
46-
HeapSamples_Entry* e = HeapSamples_Iter_get(&res.iter);
47-
prev = e->val;
48-
e->val = value;
34+
traceback_t* prev = it->second;
35+
it->second = value;
36+
return prev;
4937
}
50-
return prev;
51-
}
5238

53-
bool
54-
memalloc_heap_map::contains(void* key) const
55-
{
56-
return HeapSamples_contains(&map, &key);
39+
return nullptr;
5740
}
5841

5942
traceback_t*
6043
memalloc_heap_map::remove(void* key)
6144
{
62-
traceback_t* res = nullptr;
63-
HeapSamples_Iter it = HeapSamples_find(&map, &key);
64-
HeapSamples_Entry* e = HeapSamples_Iter_get(&it);
65-
if (e != nullptr) {
66-
res = e->val;
67-
/* This erases the entry but won't shrink the table. */
68-
HeapSamples_erase_at(it);
45+
auto it = map.find(key);
46+
if (it == map.end()) {
47+
return nullptr;
6948
}
70-
return res;
49+
50+
traceback_t* result = it->second;
51+
map.erase(it);
52+
return result;
7153
}
7254

7355
PyObject*
7456
memalloc_heap_map::export_to_python() const
7557
{
76-
PyObject* heap_list = PyList_New(HeapSamples_size(&map));
58+
PyObject* heap_list = PyList_New(map.size());
7759
if (heap_list == nullptr) {
7860
return nullptr;
7961
}
8062

81-
int i = 0;
82-
HeapSamples_CIter it = HeapSamples_citer(&map);
83-
for (const HeapSamples_Entry* e = HeapSamples_CIter_get(&it); e != nullptr; e = HeapSamples_CIter_next(&it)) {
84-
traceback_t* tb = e->val;
85-
63+
size_t i = 0;
64+
for (const auto& [key, tb] : map) {
8665
PyObject* tb_and_size = PyTuple_New(2);
8766
PyTuple_SET_ITEM(tb_and_size, 0, tb->to_tuple());
8867
PyTuple_SET_ITEM(tb_and_size, 1, PyLong_FromSize_t(tb->size));
89-
PyList_SET_ITEM(heap_list, i, tb_and_size);
90-
i++;
68+
PyList_SET_ITEM(heap_list, i++, tb_and_size);
9169

9270
memalloc_debug_gil_release();
9371
}
@@ -97,79 +75,10 @@ memalloc_heap_map::export_to_python() const
9775
void
9876
memalloc_heap_map::destructive_copy_from(memalloc_heap_map& src)
9977
{
100-
HeapSamples_Iter it = HeapSamples_iter(&src.map);
101-
for (const HeapSamples_Entry* e = HeapSamples_Iter_get(&it); e != nullptr; e = HeapSamples_Iter_next(&it)) {
102-
HeapSamples_insert(&map, e);
103-
}
104-
/* Can't erase inside the loop or the iterator is invalidated */
105-
HeapSamples_clear(&src.map);
106-
}
107-
108-
// Iterator implementation
109-
memalloc_heap_map::iterator::iterator()
110-
: iter{}
111-
{
112-
}
78+
// Move all entries from src to this map using merge (C++17)
79+
// This efficiently transfers ownership without copying
80+
map.merge(src.map);
11381

114-
memalloc_heap_map::iterator::iterator(const memalloc_heap_map& map)
115-
: iter(HeapSamples_citer(&map.map))
116-
{
117-
}
118-
119-
memalloc_heap_map::iterator&
120-
memalloc_heap_map::iterator::operator++()
121-
{
122-
const HeapSamples_Entry* e = HeapSamples_CIter_get(&iter);
123-
if (!e) {
124-
return *this;
125-
}
126-
HeapSamples_CIter_next(&iter);
127-
return *this;
128-
}
129-
130-
memalloc_heap_map::iterator
131-
memalloc_heap_map::iterator::operator++(int)
132-
{
133-
iterator tmp = *this;
134-
++(*this);
135-
return tmp;
136-
}
137-
138-
memalloc_heap_map::iterator::value_type
139-
memalloc_heap_map::iterator::operator*() const
140-
{
141-
const HeapSamples_Entry* e = HeapSamples_CIter_get(&iter);
142-
if (!e) {
143-
return { nullptr, nullptr };
144-
}
145-
return { e->key, e->val };
146-
}
147-
148-
bool
149-
memalloc_heap_map::iterator::operator==(const iterator& other) const
150-
{
151-
// Compare underlying iterators by their current entry pointers
152-
// Note: HeapSamples_CIter doesn't have equality comparison, so we compare
153-
// the current entry pointers. Both end iterators will have nullptr entries.
154-
const HeapSamples_Entry* e1 = HeapSamples_CIter_get(&iter);
155-
const HeapSamples_Entry* e2 = HeapSamples_CIter_get(&other.iter);
156-
return e1 == e2;
157-
}
158-
159-
bool
160-
memalloc_heap_map::iterator::operator!=(const iterator& other) const
161-
{
162-
return !(*this == other);
163-
}
164-
165-
memalloc_heap_map::iterator
166-
memalloc_heap_map::begin() const
167-
{
168-
return iterator(*this);
169-
}
170-
171-
memalloc_heap_map::iterator
172-
memalloc_heap_map::end() const
173-
{
174-
return iterator();
82+
// Clear any remaining entries in src (shouldn't be any after merge)
83+
src.map.clear();
17584
}

0 commit comments

Comments
 (0)