diff --git a/profiling/build.rs b/profiling/build.rs index c14c4bc9bd..836de45af7 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -389,8 +389,11 @@ fn cfg_frameless(vernum: u64) -> bool { } fn cfg_php_feature_flags(vernum: u64) { - println!("cargo::rustc-check-cfg=cfg(php_gc_status, php_zend_compile_string_has_position, php_gc_status_extended, php_frameless, php_opcache_restart_hook, php_zend_mm_set_custom_handlers_ex)"); + println!("cargo::rustc-check-cfg=cfg(php_gc_status, php_zend_compile_string_has_position, php_gc_status_extended, php_frameless, php_opcache_restart_hook, php_zend_mm_set_custom_handlers_ex, php_zts_fast_globals)"); + if vernum >= 70400 { + println!("cargo:rustc-cfg=php_zts_fast_globals"); + } if vernum >= 70300 { println!("cargo:rustc-cfg=php_gc_status"); } @@ -630,6 +633,7 @@ fn apple_linker_flags() { "_sapi_module", // TSRM (ZTS builds only; harmless to list on NTS — they simply // won't appear as undefined) + "_executor_globals_id", "_tsrm_get_ls_cache", "_tsrm_set_new_thread_end_handler", // ZTS globals offsets (replace direct globals on ZTS) diff --git a/profiling/src/allocation/allocation_ge84.rs b/profiling/src/allocation/allocation_ge84.rs index 9ffd83ecd5..bfc1d4d2ba 100644 --- a/profiling/src/allocation/allocation_ge84.rs +++ b/profiling/src/allocation/allocation_ge84.rs @@ -1,8 +1,6 @@ -use crate::allocation::{ - allocation_profiling_stats_should_collect, collect_allocation, current_execute_data, - untrack_allocation, -}; +use crate::allocation::{collect_allocation, untrack_allocation}; use crate::bindings as zend; +use crate::module_globals::{self, ProfilerGlobals}; use crate::PROFILER_NAME; use core::ptr; use libc::{c_char, c_int, c_void, size_t}; @@ -10,6 +8,9 @@ use log::{debug, trace, warn}; use std::sync::atomic::Ordering::Relaxed; use std::sync::LazyLock; +#[cfg(php_zts)] +use crate::allocation::current_execute_data_from_cache; + #[cfg(php_debug)] use libc::c_uint; @@ -322,7 +323,14 @@ unsafe fn alloc_prof_malloc_impl(len: size_t) -> *mut c_void #[cfg(feature = "debug_stats")] ALLOCATION_PROFILING_SIZE.fetch_add(len as u64, Relaxed); - let state = tls_zend_mm_state_copy!(); + #[cfg(php_zts)] + let ls_cache = module_globals::get_tsrm_ls_cache(); + #[cfg(php_zts)] + let globals = module_globals::get_profiler_globals_from_cache(ls_cache); + #[cfg(not(php_zts))] + let globals = module_globals::get_profiler_globals(); + let state = (*globals).zend_mm_state.get(); + let ptr = if CUSTOM { let alloc = state.prev_custom_mm_alloc.unwrap(); #[cfg(php_debug)] @@ -343,12 +351,21 @@ unsafe fn alloc_prof_malloc_impl(len: size_t) -> *mut c_void // during startup, minit, rinit, ... current_execute_data is null // we are only interested in allocations during userland operations - if current_execute_data().is_null() { + #[cfg(php_zts)] + let execute_data = current_execute_data_from_cache(ls_cache); + #[cfg(not(php_zts))] + let execute_data = ptr::addr_of!(zend::executor_globals.current_execute_data).read(); + if execute_data.is_null() { return ptr; } - if allocation_profiling_stats_should_collect(len) { - collect_allocation(ptr, len); + if ProfilerGlobals::should_collect(globals, len) { + collect_allocation( + unsafe { &(*globals).interrupt_count }, + execute_data, + ptr, + len, + ); } ptr @@ -494,7 +511,14 @@ unsafe fn alloc_prof_realloc_impl( #[cfg(feature = "debug_stats")] ALLOCATION_PROFILING_SIZE.fetch_add(len as u64, Relaxed); - let state = tls_zend_mm_state_copy!(); + #[cfg(php_zts)] + let ls_cache = module_globals::get_tsrm_ls_cache(); + #[cfg(php_zts)] + let globals = module_globals::get_profiler_globals_from_cache(ls_cache); + #[cfg(not(php_zts))] + let globals = module_globals::get_profiler_globals(); + let state = (*globals).zend_mm_state.get(); + let ptr = if CUSTOM { let realloc = state.prev_custom_mm_realloc.unwrap(); #[cfg(php_debug)] @@ -521,23 +545,24 @@ unsafe fn alloc_prof_realloc_impl( untrack_allocation(prev_ptr); } - alloc_prof_realloc_sample(ptr, len) -} + #[cfg(php_zts)] + let execute_data = current_execute_data_from_cache(ls_cache); + #[cfg(not(php_zts))] + let execute_data = ptr::addr_of!(zend::executor_globals.current_execute_data).read(); -#[inline(always)] -unsafe fn alloc_prof_realloc_sample(ptr: *mut c_void, len: size_t) -> *mut c_void { // during startup, minit, rinit, ... current_execute_data is null // we are only interested in allocations during userland operations - if current_execute_data().is_null() { - return ptr; - } - - if ptr.is_null() { + if execute_data.is_null() || ptr.is_null() { return ptr; } - if allocation_profiling_stats_should_collect(len) { - collect_allocation(ptr, len); + if ProfilerGlobals::should_collect(globals, len) { + collect_allocation( + unsafe { &(*globals).interrupt_count }, + execute_data, + ptr, + len, + ); } ptr diff --git a/profiling/src/allocation/allocation_le83.rs b/profiling/src/allocation/allocation_le83.rs index e10b83b1c8..14637a2384 100644 --- a/profiling/src/allocation/allocation_le83.rs +++ b/profiling/src/allocation/allocation_le83.rs @@ -1,11 +1,9 @@ -use crate::allocation::{ - allocation_profiling_stats_should_collect, collect_allocation, current_execute_data, - untrack_allocation, -}; +use crate::allocation::{collect_allocation, untrack_allocation}; use crate::bindings::{ self as zend, datadog_php_install_handler, datadog_php_zif_handler, ddog_php_prof_copy_long_into_zval, }; +use crate::module_globals::{self, ProfilerGlobals}; use crate::{RefCellExt, PROFILER_NAME, REQUEST_LOCALS}; use core::ptr; use libc::{c_char, c_int, c_void, size_t}; @@ -13,6 +11,9 @@ use log::{debug, trace, warn}; use std::sync::atomic::Ordering::Relaxed; use std::sync::LazyLock; +#[cfg(php_zts)] +use crate::allocation::current_execute_data_from_cache; + #[cfg(feature = "debug_stats")] use crate::allocation::{ALLOCATION_PROFILING_COUNT, ALLOCATION_PROFILING_SIZE}; @@ -308,7 +309,14 @@ unsafe fn alloc_prof_malloc_impl(len: size_t) -> *mut c_void #[cfg(feature = "debug_stats")] ALLOCATION_PROFILING_SIZE.fetch_add(len as u64, Relaxed); - let state = tls_zend_mm_state_copy!(); + #[cfg(php_zts)] + let ls_cache = module_globals::get_tsrm_ls_cache(); + #[cfg(php_zts)] + let globals = module_globals::get_profiler_globals_from_cache(ls_cache); + #[cfg(not(php_zts))] + let globals = module_globals::get_profiler_globals(); + let state = (*globals).zend_mm_state.get(); + let ptr = if CUSTOM { state.prev_custom_mm_alloc.unwrap()(len) } else { @@ -327,12 +335,21 @@ unsafe fn alloc_prof_malloc_impl(len: size_t) -> *mut c_void // during startup, minit, rinit, ... current_execute_data is null // we are only interested in allocations during userland operations - if current_execute_data().is_null() { + #[cfg(php_zts)] + let execute_data = current_execute_data_from_cache(ls_cache); + #[cfg(not(php_zts))] + let execute_data = ptr::addr_of!(zend::executor_globals.current_execute_data).read(); + if execute_data.is_null() { return ptr; } - if allocation_profiling_stats_should_collect(len) { - collect_allocation(ptr, len); + if ProfilerGlobals::should_collect(globals, len) { + collect_allocation( + unsafe { &(*globals).interrupt_count }, + execute_data, + ptr, + len, + ); } ptr @@ -422,7 +439,14 @@ unsafe fn alloc_prof_realloc_impl( #[cfg(feature = "debug_stats")] ALLOCATION_PROFILING_SIZE.fetch_add(len as u64, Relaxed); - let state = tls_zend_mm_state_copy!(); + #[cfg(php_zts)] + let ls_cache = module_globals::get_tsrm_ls_cache(); + #[cfg(php_zts)] + let globals = module_globals::get_profiler_globals_from_cache(ls_cache); + #[cfg(not(php_zts))] + let globals = module_globals::get_profiler_globals(); + let state = (*globals).zend_mm_state.get(); + let ptr = if CUSTOM { state.prev_custom_mm_realloc.unwrap()(prev_ptr, len) } else { @@ -447,23 +471,24 @@ unsafe fn alloc_prof_realloc_impl( untrack_allocation(prev_ptr); } - alloc_prof_realloc_sample(ptr, len) -} + #[cfg(php_zts)] + let execute_data = current_execute_data_from_cache(ls_cache); + #[cfg(not(php_zts))] + let execute_data = ptr::addr_of!(zend::executor_globals.current_execute_data).read(); -#[inline(always)] -unsafe fn alloc_prof_realloc_sample(ptr: *mut c_void, len: size_t) -> *mut c_void { // during startup, minit, rinit, ... current_execute_data is null // we are only interested in allocations during userland operations - if current_execute_data().is_null() { - return ptr; - } - - if ptr.is_null() { + if execute_data.is_null() || ptr.is_null() { return ptr; } - if allocation_profiling_stats_should_collect(len) { - collect_allocation(ptr, len); + if ProfilerGlobals::should_collect(globals, len) { + collect_allocation( + unsafe { &(*globals).interrupt_count }, + execute_data, + ptr, + len, + ); } ptr diff --git a/profiling/src/allocation/mod.rs b/profiling/src/allocation/mod.rs index bb6a762f26..4766331dfb 100644 --- a/profiling/src/allocation/mod.rs +++ b/profiling/src/allocation/mod.rs @@ -14,7 +14,7 @@ use log::{debug, trace}; use rand_distr::{Distribution, Poisson}; use std::ffi::c_void; use std::num::{NonZero, NonZeroU32, NonZeroU64}; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; #[cfg(not(php_zts))] use rand::rngs::StdRng; @@ -39,13 +39,26 @@ pub(crate) unsafe fn get_zend_mm_state() -> *mut Cell { ptr::addr_of_mut!((*globals).zend_mm_state) } +#[cfg(php_zts)] #[inline(always)] -pub(crate) unsafe fn current_execute_data() -> *mut zend::zend_execute_data { - #[cfg(not(php_zts))] - return ptr::addr_of!(zend::executor_globals.current_execute_data).read(); - - #[cfg(php_zts)] - zend::ddog_php_prof_get_current_execute_data() +pub(crate) unsafe fn current_execute_data_from_cache( + ls_cache: *mut c_void, +) -> *mut zend::zend_execute_data { + // PHP 7.4 introduced fast globals offsets. Older versions use the TSRM resource ID. + #[cfg(php_zts_fast_globals)] + let globals = { + let offset = ptr::addr_of!(zend::executor_globals_offset).read(); + ls_cache + .byte_add(offset) + .cast::() + }; + #[cfg(not(php_zts_fast_globals))] + let globals = { + let id = ptr::addr_of!(zend::executor_globals_id).read(); + module_globals::get_tsrm_resource_from_cache(ls_cache, id) + .cast::() + }; + ptr::addr_of!((*globals).current_execute_data).read() } /// Macros for accessing ZendMMState from PHP globals. @@ -146,7 +159,7 @@ pub static ALLOCATION_PROFILING_COUNT: AtomicU64 = AtomicU64::new(0); pub static ALLOCATION_PROFILING_SIZE: AtomicU64 = AtomicU64::new(0); pub struct AllocationProfilingStats { - /// number of bytes until next sample collection + /// Number of bytes remaining until the next sample collection. next_sample: i64, poisson: Poisson, #[cfg(php_zts)] @@ -177,42 +190,46 @@ impl AllocationProfilingStats { fn should_collect_allocation(&mut self, len: size_t) -> bool { self.next_sample -= len as i64; - if self.next_sample > 0 { return false; } self.next_sampling_interval(); - true } } /// Collect an allocation sample and optionally track it for live heap profiling. /// +/// # Safety +/// `execute_data` must be null or a valid pointer provided by the engine. The +/// profiler may walk the execution frames reachable through it. +/// /// # Arguments /// * `ptr` - The pointer returned by the allocator (used for live heap tracking) /// * `len` - The size of the allocation in bytes #[cold] -pub fn collect_allocation(ptr: *mut c_void, len: size_t) { +pub unsafe fn collect_allocation( + interrupt_count: &AtomicU32, + execute_data: *mut zend::zend_execute_data, + ptr: *mut c_void, + len: size_t, +) { if let Some(profiler) = Profiler::get() { // Check if there's a pending time interrupt that we can handle now // instead of waiting for an interrupt handler. This is slightly more // accurate and efficient, win-win. - // SAFETY: allocation samples are collected on an initialized PHP request thread. - let globals = unsafe { module_globals::get_profiler_globals() }; - // SAFETY: the current thread's module globals are valid through GSHUTDOWN. - let interrupt_count = unsafe { (*globals).interrupt_count.swap(0, Ordering::Relaxed) }; + let pending_interrupts = interrupt_count.swap(0, Ordering::Relaxed); // SAFETY: execute_data was provided by the engine, and the profiler - // doesn't mutate it. + // only reads the execution frames reachable through it. unsafe { profiler.collect_allocations( - zend::ddog_php_prof_get_current_execute_data(), + execute_data, ptr, 1_i64, len as i64, - (interrupt_count > 0).then_some(interrupt_count), + (pending_interrupts > 0).then_some(pending_interrupts), ) }; } diff --git a/profiling/src/allocation/profiling_stats.rs b/profiling/src/allocation/profiling_stats.rs index b2c0e30d26..8cb6c44000 100644 --- a/profiling/src/allocation/profiling_stats.rs +++ b/profiling/src/allocation/profiling_stats.rs @@ -1,124 +1,49 @@ -//! The thread-local allocation profiling stats are held in this module. -//! The stats are used on the hot-path of allocation, so this code is -//! performance sensitive. It is encapsulated so that some unsafe techniques -//! can be used but expose a relatively safe API. +//! Per-thread allocation profiling stats stored in PHP module globals. +//! The stats are used on the allocation hot path, so callers should thread +//! through an already-resolved [`ProfilerGlobals`] pointer whenever possible. use super::{AllocationProfilingStats, ALLOCATION_PROFILING_INTERVAL}; +use crate::module_globals::{self, ProfilerGlobals}; use libc::size_t; -use std::mem::MaybeUninit; +use std::num::NonZeroU64; +use std::sync::atomic::Ordering; #[cfg(php_zend_mm_set_custom_handlers_ex)] use super::allocation_ge84; #[cfg(not(php_zend_mm_set_custom_handlers_ex))] use super::allocation_le83; -#[cfg(php_zts)] -use std::cell::UnsafeCell; -use std::num::NonZeroU64; -use std::sync::atomic::Ordering; -#[cfg(php_zts)] -thread_local! { - /// This is initialized in ginit, before any memory allocator hooks are - /// installed. During a request, all accesses will be initialized. +impl ProfilerGlobals { + /// Updates the allocation sampling state from the PHP globals. /// - /// This is not pub so that unsafe code can be contained to this module. - static ALLOCATION_PROFILING_STATS: UnsafeCell> = - const { UnsafeCell::new(MaybeUninit::uninit()) }; -} - -#[cfg(not(php_zts))] -static mut ALLOCATION_PROFILING_STATS: MaybeUninit = - const { MaybeUninit::uninit() }; - -/// Accesses the thread-local [`AllocationProfilingStats`], passing a mutable -/// reference to the contained `MaybeUninit` to `F`. -/// -/// # Safety -/// -/// 1. There should not be any active borrows to the thread-local variable -/// [`AllocationProfilingStats`] when this function is called. -/// 2. Function `F` should not do anything which causes a new borrow on -/// [`AllocationProfilingStats`]. -/// 3. Do not call this function in ALLOCATION_PROFILING_STATS's destructor, -/// as it assumes that [`std::thread::LocalKey::try_with`] cannot fail. -/// -/// This is not pub to limit caller's ability to violate these conditions. -unsafe fn allocation_profiling_stats_mut(f: F) -> R -where - F: FnOnce(&mut MaybeUninit) -> R, -{ - #[cfg(php_zts)] - { - let result = ALLOCATION_PROFILING_STATS.try_with(|cell| { - let ptr: *mut MaybeUninit = cell.get(); - // SAFETY: the cell is statically initialized to [`MaybeUninit::uninit`] so the - // _cell_ is valid and initialized memory. As required by this own - // function's safety requirements, there should not be any active borrows - // to [`ALLOCATION_PROFILING_STATS`], so this mutable dereference is sound. - let uninit = unsafe { &mut *ptr }; - f(uninit) - }); - // SAFETY: this function is not called in a destructor, therefore it - // cannot return an AccessError: - // > If the key has been destroyed (which may happen if this is called - // > in a destructor), this function will return an AccessError. - unsafe { result.unwrap_unchecked() } - } - - #[cfg(not(php_zts))] - { - // SAFETY: For non-ZTS builds, ALLOCATION_PROFILING_STATS is a static variable. - // As required by this function's safety requirements, there should not be any - // active borrows to ALLOCATION_PROFILING_STATS, so this mutable reference is sound. - let uninit = unsafe { - let ptr: *mut MaybeUninit = - std::ptr::addr_of_mut!(ALLOCATION_PROFILING_STATS); - &mut *ptr - }; - f(uninit) - } -} - -/// Given the provided allocation length `len`, return whether the allocation -/// should be collected. This is a mutable operation, as the thread-local -/// variable will be modified to reduce the distance until the next sample. -pub fn allocation_profiling_stats_should_collect(len: size_t) -> bool { - let f = |maybe_uninit: &mut MaybeUninit| { - // SAFETY: ALLOCATION_PROFILING_STATS was initialized in GINIT. - let stats = unsafe { maybe_uninit.assume_init_mut() }; + /// # Safety + /// `globals` must point to initialized module globals for the current + /// thread, and no mutable access to its allocation profiling state may be + /// active. + #[inline(always)] + pub unsafe fn should_collect(globals: *mut ProfilerGlobals, len: size_t) -> bool { + // SAFETY: the state is initialized in GINIT and all accesses occur on the + // owning PHP thread. Allocator reentrancy cannot overlap this borrow because + // sampling state is released before stack collection begins. + let stats = unsafe { (*(*globals).allocation_profiling_stats.get()).assume_init_mut() }; stats.should_collect_allocation(len) - }; - - // SAFETY: - // 1. This function doesn't expose any way for the caller to keep a - // borrow alive, nor do the other public functions, so there cannot be - // any existing borrows alive. - // 2. This closure will not cause any new borrows. - // 3. This function isn't called during ALLOCATION_PROFILING_STATS's dtor, - // as MaybeUninit's destructor does nothing, you have to specifically drop - // it. Even if the destructor were called, AllocationProfilingStats's dtor - // doesn't access the TLS variable (it can't, it doesn't have access). - unsafe { allocation_profiling_stats_mut(f) } + } } /// Initializes the allocation profiler's globals. /// /// # Safety -/// -/// Must be called once per PHP thread ginit. +/// Must be called once per PHP thread GINIT. pub unsafe fn ginit() { - // SAFETY: - // 1. During ginit, there will not be any other borrows to stats. - // 2. This closure will not make new borrows to stats. - // 3. This is not during the thread-local destructor. + let interval = ALLOCATION_PROFILING_INTERVAL.load(Ordering::Relaxed); + // SAFETY: ALLOCATION_PROFILING_INTERVAL is always greater than zero. + let sampling_distance = unsafe { NonZeroU64::new_unchecked(interval) }; + // SAFETY: GINIT runs with allocated module globals and before allocator hooks. + let globals = unsafe { module_globals::get_profiler_globals() }; unsafe { - allocation_profiling_stats_mut(|uninit| { - let interval = ALLOCATION_PROFILING_INTERVAL.load(Ordering::Relaxed); - // SAFETY: ALLOCATION_PROFILING_INTERVAL must always be > 0. - let nonzero = NonZeroU64::new_unchecked(interval); - uninit.write(AllocationProfilingStats::new(nonzero)); - }) - }; + (*(*globals).allocation_profiling_stats.get()) + .write(AllocationProfilingStats::new(sampling_distance)); + } #[cfg(not(php_zend_mm_set_custom_handlers_ex))] allocation_le83::alloc_prof_ginit(); @@ -126,36 +51,23 @@ pub unsafe fn ginit() { allocation_ge84::alloc_prof_ginit(); } -/// Initializes the allocation profiler's globals with the provided sampling -/// distance. +/// Reinitializes allocation sampling with the configured distance. /// /// # Safety -/// -/// Must be called once per PHP thread minit, unless the allocation profiling -/// is disabled, in which case it can be skipped. +/// Must be called once per PHP thread MINIT, unless allocation profiling is disabled. pub unsafe fn minit(sampling_distance: NonZeroU64) { - // SAFETY: - // 1. During minit, there will not be any other borrows. - // 2. This closure will not make new borrows. - // 3. This is not during the thread-local destructor. - unsafe { - allocation_profiling_stats_mut(|uninit| { - // SAFETY: previously initialized in ginit, we're just - // re-initializing it because we now have config - *uninit.assume_init_mut() = AllocationProfilingStats::new(sampling_distance); - }) - }; + // SAFETY: GINIT initialized this state, and MINIT has exclusive lifecycle access. + let globals = unsafe { module_globals::get_profiler_globals() }; + let stats = unsafe { (*(*globals).allocation_profiling_stats.get()).assume_init_mut() }; + *stats = AllocationProfilingStats::new(sampling_distance); } -/// Shuts down the allocation profiler's globals. +/// Drops the allocation sampling state. /// /// # Safety -/// -/// Must be called once per PHP thread gshutdown. +/// Must be called once per PHP thread GSHUTDOWN after allocator hooks are removed. pub unsafe fn gshutdown() { - // SAFETY: - // 1. During gshutdown, there will not be any other borrows. - // 2. This closure will not make new borrows. - // 3. This is not during the thread-local destructor. - unsafe { allocation_profiling_stats_mut(|maybe_uninit| maybe_uninit.assume_init_drop()) } + // SAFETY: GINIT initialized this state, and GSHUTDOWN has exclusive lifecycle access. + let globals = unsafe { module_globals::get_profiler_globals() }; + unsafe { (*(*globals).allocation_profiling_stats.get()).assume_init_drop() }; } diff --git a/profiling/src/module_globals.rs b/profiling/src/module_globals.rs index c91559656e..30333bfdb0 100644 --- a/profiling/src/module_globals.rs +++ b/profiling/src/module_globals.rs @@ -1,6 +1,7 @@ use crate::allocation; -use core::cell::Cell; +use core::cell::{Cell, UnsafeCell}; use core::ffi::c_void; +use core::mem::MaybeUninit; use core::ptr; use core::sync::atomic::AtomicU32; @@ -20,6 +21,9 @@ pub struct ProfilerGlobals { /// the PHP thread, so the value must remain atomic despite living in /// thread-local PHP module globals. pub interrupt_count: AtomicU32, + /// Per-thread allocation sampling state. Kept in PHP globals so allocator + /// hooks can reuse an already-resolved TSRM cache instead of accessing Rust TLS. + pub allocation_profiling_stats: UnsafeCell>, } /// We need TSRM to call into GINIT and GSHUTDOWN to observe spawning and @@ -37,8 +41,15 @@ pub static mut GLOBALS_ID: i32 = 0; pub static mut GLOBALS: ProfilerGlobals = ProfilerGlobals { zend_mm_state: Cell::new(ZendMMState::new()), interrupt_count: AtomicU32::new(0), + allocation_profiling_stats: UnsafeCell::new(MaybeUninit::uninit()), }; +#[cfg(all(test, php_zts))] +#[no_mangle] +unsafe extern "C" fn tsrm_get_ls_cache() -> *mut c_void { + ptr::null_mut() +} + #[cfg(php_zts)] mod zts { use core::ffi::c_void; @@ -122,6 +133,7 @@ pub unsafe extern "C" fn ginit(_globals_ptr: *mut c_void) { let globals = _globals_ptr.cast::(); (*globals).zend_mm_state = Cell::new(ZendMMState::new()); (*globals).interrupt_count = AtomicU32::new(0); + (*globals).allocation_profiling_stats = UnsafeCell::new(MaybeUninit::uninit()); } // SAFETY: this is called in thread ginit as expected, and no other places. diff --git a/profiling/src/profiling/mod.rs b/profiling/src/profiling/mod.rs index 759066c937..574320c166 100644 --- a/profiling/src/profiling/mod.rs +++ b/profiling/src/profiling/mod.rs @@ -1178,8 +1178,12 @@ impl Profiler { /// /// If heap live profiling is enabled, the allocation is tracked for later /// cancellation when freed. + /// + /// # Safety + /// `execute_data` must be null or a valid pointer provided by the engine. + /// The profiler walks the execution frames reachable through it. #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - pub fn collect_allocations( + pub unsafe fn collect_allocations( &self, execute_data: *mut zend_execute_data, ptr: *mut std::ffi::c_void,