@@ -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+ }
0 commit comments