Skip to content

Commit bfde5e5

Browse files
feat(prof): detect parallel threads (#3515)
* Detect parallel threads by reading the TLS runtime pointer * call public API function and fallback if not available
1 parent 6f3efe5 commit bfde5e5

4 files changed

Lines changed: 96 additions & 9 deletions

File tree

profiling/src/php_ffi.c

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,3 +624,83 @@ zval *ddog_php_prof_get_memoized_config(uint16_t config_id) {
624624
// dummy symbol for tests, so that they can be run without being linked into PHP
625625
__attribute__((weak)) zend_write_func_t zend_write;
626626
#endif
627+
628+
/**
629+
* Returns true if the thread was spawned by the parallel extension, false otherwise.
630+
*
631+
* This function is meant to be called in the GINIT phase of a PHP request, it is also safe to be
632+
* called in RINIT or during request processing, but its outcome won't change anymore after GINIT.
633+
* That being said, it being a costly function (module registry lookup, `dlsym()`,
634+
* `__tls_get_addr()` in the fallback branch), the best usage pattern is to call this in GINIT and
635+
* cache the result in a thread local.
636+
*/
637+
bool ddog_php_prof_is_parallel_thread() {
638+
// Check if parallel extension is loaded to retrieve it's dl handle
639+
zend_module_entry *parallel_module = zend_hash_str_find_ptr(&module_registry, ZEND_STRL("parallel"));
640+
641+
if (parallel_module == NULL || parallel_module->handle == NULL) {
642+
return false;
643+
}
644+
645+
// Try to find the new public API function first (available in parallel >= 1.2.9)
646+
zend_bool (*is_worker)(void) = DL_FETCH_SYMBOL(parallel_module->handle, "php_parallel_is_parallel_worker_thread");
647+
if (is_worker) {
648+
return is_worker();
649+
}
650+
651+
// Fallback: for older versions of parallel, we can access the TLS variable
652+
// `php_parallel_scheduler_context` and do a NULL check.
653+
654+
// Why not just `__attribute__((weak)) php_parallel_scheduler_context;`?
655+
// This would work if we could enforce the order of `dlopen()` calls for extensions (which we
656+
// can't). If the parallel extension is loaded before the profiler extension it works just nice
657+
// but if the profiler gets loaded first this will not resolve correct. Luckily `dlsym()`
658+
// behaves as a safe wrapper around this.
659+
void *tls_symbol = DL_FETCH_SYMBOL(parallel_module->handle, "php_parallel_scheduler_context");
660+
661+
if (tls_symbol == NULL) {
662+
return false;
663+
}
664+
665+
void **tls_ptr;
666+
#ifdef __APPLE__
667+
// On macOS TLS variables accessed via dlsym return a descriptor structure containing a
668+
// function pointer (thunk) that must be called to get the actual thread-local address.
669+
// see https://github.com/apple-oss-distributions/dyld/blob/637911768f664e38e7e50b4fbf17e303e14fdc01/libdyld/ThreadLocalVariables.h#L110-L115
670+
typedef struct {
671+
void* (*thunk)(void* desc);
672+
unsigned long key;
673+
unsigned long offset;
674+
} tls_descriptor;
675+
676+
tls_descriptor *desc = (tls_descriptor*)tls_symbol;
677+
678+
if (desc->thunk == NULL) {
679+
return false;
680+
}
681+
682+
tls_ptr = (void**)desc->thunk(desc);
683+
#else
684+
// Linux (musl and glibc) are nice to us, `dlsym()` detects that this symbol is a STT_TLS
685+
// (Symbol Table Type Thread-Local Storage) and calls `__tls_get_addr()` on it.
686+
//
687+
// musl implemenation at
688+
// https://git.musl-libc.org/cgit/musl/tree/ldso/dynlink.c?h=v1.2.5#n2287
689+
// glibc implementation at
690+
// https://github.com/bminor/glibc/blob/56d0e2cca1e5ac4a9ed9332c46c64d7021ab011f/elf/dl-sym.c#L162-L165
691+
//
692+
// So in the end it just returns a pointer to the correct TLS variable for `this` thread we are
693+
// in, which makes it an easy pointer deref to get the value.
694+
tls_ptr = (void**)tls_symbol;
695+
#endif
696+
697+
if (tls_ptr == NULL) {
698+
return false;
699+
}
700+
701+
// The parallel context is non-NULL when this is a parallel thread, see the
702+
// `php_parallel_scheduler_setup()` function in `src/scheduler.c` in the parallel extension.
703+
// This inits the `php_parallel_scheduler_context` TLS to point to a `php_parallel_runtime_t`
704+
// struct right before triggering `GINIT`.
705+
return (*tls_ptr != NULL);
706+
}

profiling/src/php_ffi.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,9 @@ void ddog_php_test_free_fake_zend_execute_data(zend_execute_data *execute_data);
161161

162162
void ddog_php_opcache_init_handle();
163163
bool ddog_php_jit_enabled();
164+
165+
/**
166+
* Detects if the current thread is a parallel extension thread.
167+
* Returns true if the thread was spawned by the parallel extension.
168+
*/
169+
bool ddog_php_prof_is_parallel_thread();

profiling/src/profiling/thread_utils.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::mem::MaybeUninit;
55
use std::thread::JoinHandle;
66
use std::time::{Duration, Instant};
77

8+
#[cfg(php_zts)]
9+
use crate::bindings::ddog_php_prof_is_parallel_thread;
810
#[cfg(php_zts)]
911
use crate::sapi::Sapi;
1012
#[cfg(php_zts)]
@@ -93,14 +95,16 @@ thread_local! {
9395

9496
pub fn get_current_thread_name() -> String {
9597
THREAD_NAME.with(|name| {
96-
name.get_or_init(|| {
97-
#[cfg(php_zts)]
98-
let mut thread_name = SAPI.to_string();
98+
name.get_or_init(|| -> String {
9999
#[cfg(not(php_zts))]
100-
let thread_name = SAPI.to_string();
100+
return SAPI.to_string();
101101

102102
#[cfg(php_zts)]
103103
{
104+
if unsafe { ddog_php_prof_is_parallel_thread() } {
105+
return "parallel worker".to_string();
106+
}
107+
let mut thread_name = SAPI.to_string();
104108
// So far, only FrankenPHP sets meaningful thread names
105109
if *SAPI == Sapi::FrankenPHP {
106110
let mut name = [0u8; 32];
@@ -124,9 +128,8 @@ pub fn get_current_thread_name() -> String {
124128
}
125129
}
126130
}
131+
thread_name
127132
}
128-
129-
thread_name
130133
})
131134
.clone()
132135
})

profiling/tests/correctness/exceptions_zts.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@
2525
},
2626
{
2727
"key": "thread name",
28-
"values": [
29-
"cli"
30-
]
28+
"values_regex": "(cli|parallel worker)"
3129
}
3230
]
3331
}

0 commit comments

Comments
 (0)