Skip to content

Commit 85b4be4

Browse files
sanchdaYun-Kimncybulnsrip-ddwconti27
authored
fix(profiling): avoid stack size limits on musl for stack v2 threads (#12174)
Co-authored-by: Yun Kim <[email protected]> Co-authored-by: Nicole Cybul <[email protected]> Co-authored-by: Nick Ripley <[email protected]> Co-authored-by: William Conti <[email protected]> Co-authored-by: Christophe Papazian <[email protected]> Co-authored-by: Munir Abdinur <[email protected]> Co-authored-by: Taegyun Kim <[email protected]>
1 parent e344189 commit 85b4be4

File tree

5 files changed

+77
-12
lines changed

5 files changed

+77
-12
lines changed

ddtrace/internal/datadog/profiling/stack_v2/include/sampler.hpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ class Sampler
2525
// Parameters
2626
uint64_t echion_frame_cache_size = g_default_echion_frame_cache_size;
2727

28-
// Helper function; implementation of the echion sampling thread
29-
void sampling_thread(const uint64_t seq_num);
30-
3128
// This is a singleton, so no public constructor
3229
Sampler();
3330

@@ -37,7 +34,7 @@ class Sampler
3734
public:
3835
// Singleton instance
3936
static Sampler& get();
40-
void start();
37+
bool start();
4138
void stop();
4239
void register_thread(uint64_t id, uint64_t native_id, const char* name);
4340
void unregister_thread(uint64_t id);
@@ -46,6 +43,7 @@ class Sampler
4643
PyObject* _asyncio_scheduled_tasks,
4744
PyObject* _asyncio_eager_tasks);
4845
void link_tasks(PyObject* parent, PyObject* child);
46+
void sampling_thread(const uint64_t seq_num);
4947

5048
// The Python side dynamically adjusts the sampling rate based on overhead, so we need to be able to update our
5149
// own intervals accordingly. Rather than a preemptive measure, we assume the rate is ~fairly stable and just

ddtrace/internal/datadog/profiling/stack_v2/src/sampler.cpp

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,52 @@
1010

1111
using namespace Datadog;
1212

13+
// Helper class for spawning a std::thread with control over its default stack size
14+
#ifdef __linux__
15+
#include <sys/resource.h>
16+
#include <unistd.h>
17+
18+
struct ThreadArgs
19+
{
20+
Sampler* sampler;
21+
uint64_t seq_num;
22+
};
23+
24+
void*
25+
call_sampling_thread(void* args)
26+
{
27+
ThreadArgs thread_args = *static_cast<ThreadArgs*>(args);
28+
delete static_cast<ThreadArgs*>(args); // no longer needed, dynamic alloc
29+
Sampler* sampler = thread_args.sampler;
30+
sampler->sampling_thread(thread_args.seq_num);
31+
return nullptr;
32+
}
33+
34+
pthread_t
35+
create_thread_with_stack(size_t stack_size, Sampler* sampler, uint64_t seq_num)
36+
{
37+
pthread_attr_t attr;
38+
if (pthread_attr_init(&attr) != 0) {
39+
return 0;
40+
}
41+
if (stack_size > 0) {
42+
pthread_attr_setstacksize(&attr, stack_size);
43+
}
44+
45+
pthread_t thread_id;
46+
ThreadArgs* thread_args = new ThreadArgs{ sampler, seq_num };
47+
int ret = pthread_create(&thread_id, &attr, call_sampling_thread, thread_args);
48+
49+
pthread_attr_destroy(&attr);
50+
51+
if (ret != 0) {
52+
delete thread_args; // usually deleted in the thread, but need to clean it up here
53+
return 0;
54+
}
55+
return thread_id;
56+
}
57+
#endif
58+
1359
void
1460
Sampler::sampling_thread(const uint64_t seq_num)
1561
{
@@ -134,7 +180,7 @@ Sampler::unregister_thread(uint64_t id)
134180
thread_info_map.erase(id);
135181
}
136182

137-
void
183+
bool
138184
Sampler::start()
139185
{
140186
static std::once_flag once;
@@ -143,8 +189,22 @@ Sampler::start()
143189
// Launch the sampling thread.
144190
// Thread lifetime is bounded by the value of the sequence number. When it is changed from the value the thread was
145191
// launched with, the thread will exit.
146-
std::thread t(&Sampler::sampling_thread, this, ++thread_seq_num);
147-
t.detach();
192+
#ifdef __linux__
193+
// We might as well get the default stack size and use that
194+
rlimit stack_sz = {};
195+
getrlimit(RLIMIT_STACK, &stack_sz);
196+
if (create_thread_with_stack(stack_sz.rlim_cur, this, ++thread_seq_num) == 0) {
197+
return false;
198+
}
199+
#else
200+
try {
201+
std::thread t(&Sampler::sampling_thread, this, ++thread_seq_num);
202+
t.detach();
203+
} catch (const std::exception& e) {
204+
return false;
205+
}
206+
#endif
207+
return true;
148208
}
149209

150210
void

ddtrace/internal/datadog/profiling/stack_v2/src/stack_v2.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ _stack_v2_start(PyObject* self, PyObject* args, PyObject* kwargs)
2121
}
2222

2323
Sampler::get().set_interval(min_interval_s);
24-
Sampler::get().start();
25-
Py_RETURN_NONE;
24+
if (Sampler::get().start()) {
25+
Py_RETURN_TRUE;
26+
}
27+
Py_RETURN_FALSE;
2628
}
2729

2830
// Bypasses the old-style cast warning with an unchecked helper function
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
Fixes an issue where Profiling native threads would respect the musl libc
5+
default stack size, which could cause stack overflows in certain
6+
configurations.

tests/smoke_test.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,8 @@ def emit(self, record):
8282
platform.system() == "Windows"
8383
# libdatadog x86_64-apple-darwin has not yet been integrated to dd-trace-py
8484
or (platform.system() == "Darwin" and platform.machine() == "x86_64")
85-
# echion crashes on musl linux with Python 3.12 for both x86_64 and
86-
# aarch64
87-
or (platform.system() == "Linux" and sys.version_info[:2] == (3, 12) and platform.libc_ver()[0] != "glibc")
85+
# echion only works with 3.8+
86+
or sys.version_info < (3, 8, 0)
8887
):
8988
orig_env = os.environ.copy()
9089
copied_env = copy.deepcopy(orig_env)

0 commit comments

Comments
 (0)