Skip to content

Commit

Permalink
feat(profiling) support new ZendMM api (#2969)
Browse files Browse the repository at this point in the history
  • Loading branch information
realFlowControl authored Jan 14, 2025
1 parent c73b7f7 commit 46ec759
Show file tree
Hide file tree
Showing 7 changed files with 613 additions and 114 deletions.
2 changes: 2 additions & 0 deletions profiling/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,13 @@ fn cfg_php_feature_flags(vernum: u64) {
if vernum >= 80400 {
println!("cargo:rustc-cfg=php_frameless");
println!("cargo:rustc-cfg=php_opcache_restart_hook");
println!("cargo:rustc-cfg=php_zend_mm_set_custom_handlers_ex");
}
}

fn cfg_zts() {
let output = Command::new("php")
.arg("-n")
.arg("-r")
.arg("echo PHP_ZTS, PHP_EOL;")
.output()
Expand Down
444 changes: 444 additions & 0 deletions profiling/src/allocation/allocation_ge84.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,41 +1,20 @@
use crate::allocation::{
ALLOCATION_PROFILING_COUNT, ALLOCATION_PROFILING_SIZE, ALLOCATION_PROFILING_STATS,
};
use crate::bindings::{
self as zend, datadog_php_install_handler, datadog_php_zif_handler,
ddog_php_prof_copy_long_into_zval,
};
use crate::profiling::Profiler;
use crate::{PROFILER_NAME, REQUEST_LOCALS};
use lazy_static::lazy_static;
use libc::{c_char, c_int, c_void, size_t};
use log::{debug, error, trace, warn};
use rand::rngs::ThreadRng;
use rand_distr::{Distribution, Poisson};
use std::cell::{RefCell, UnsafeCell};
use std::cell::UnsafeCell;
use std::ptr;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering::{Relaxed, SeqCst};

static mut GC_MEM_CACHES_HANDLER: zend::InternalFunctionHandler = None;

/// take a sample every 4096 KiB
pub const ALLOCATION_PROFILING_INTERVAL: f64 = 1024.0 * 4096.0;

/// This will store the count of allocations (including reallocations) during
/// a profiling period. This will overflow when doing more than u64::MAX
/// allocations, which seems big enough to ignore.
pub static ALLOCATION_PROFILING_COUNT: AtomicU64 = AtomicU64::new(0);

/// This will store the accumulated size of all allocations in bytes during the
/// profiling period. This will overflow when allocating more than 18 exabyte
/// of memory (u64::MAX) which might not happen, so we can ignore this.
pub static ALLOCATION_PROFILING_SIZE: AtomicU64 = AtomicU64::new(0);

pub struct AllocationProfilingStats {
/// number of bytes until next sample collection
next_sample: i64,
poisson: Poisson<f64>,
rng: ThreadRng,
}

type ZendHeapPrepareFn = unsafe fn(heap: *mut zend::_zend_mm_heap) -> c_int;
type ZendHeapRestoreFn = unsafe fn(heap: *mut zend::_zend_mm_heap, custom_heap: c_int);

Expand Down Expand Up @@ -69,49 +48,7 @@ struct ZendMMState {
free: unsafe fn(*mut c_void),
}

impl AllocationProfilingStats {
fn new() -> AllocationProfilingStats {
// Safety: this will only error if lambda <= 0
let poisson = Poisson::new(ALLOCATION_PROFILING_INTERVAL).unwrap();
let mut stats = AllocationProfilingStats {
next_sample: 0,
poisson,
rng: rand::thread_rng(),
};
stats.next_sampling_interval();
stats
}

fn next_sampling_interval(&mut self) {
self.next_sample = self.poisson.sample(&mut self.rng) as i64;
}

fn track_allocation(&mut self, len: size_t) {
self.next_sample -= len as i64;

if self.next_sample > 0 {
return;
}

self.next_sampling_interval();

if let Some(profiler) = Profiler::get() {
// Safety: execute_data was provided by the engine, and the profiler doesn't mutate it.
unsafe {
profiler.collect_allocations(
zend::ddog_php_prof_get_current_execute_data(),
1_i64,
len as i64,
)
};
}
}
}

thread_local! {
static ALLOCATION_PROFILING_STATS: RefCell<AllocationProfilingStats> =
RefCell::new(AllocationProfilingStats::new());

/// Using an `UnsafeCell` here should be okay. There might not be any
/// synchronisation issues, as it is used in as thread local and only
/// mutated in RINIT and RSHUTDOWN.
Expand Down Expand Up @@ -150,7 +87,7 @@ lazy_static! {
static ref JIT_ENABLED: bool = unsafe { zend::ddog_php_jit_enabled() };
}

pub fn alloc_prof_minit() {
pub fn alloc_prof_ginit() {
unsafe { zend::ddog_php_opcache_init_handle() };
}

Expand All @@ -167,23 +104,6 @@ pub fn first_rinit_should_disable_due_to_jit() -> bool {
}

pub fn alloc_prof_rinit() {
let allocation_profiling: bool = REQUEST_LOCALS.with(|cell| {
match cell.try_borrow() {
Ok(locals) => {
let system_settings = locals.system_settings();
system_settings.profiling_allocation_enabled
},
Err(_err) => {
error!("Memory allocation was not initialized correctly due to a borrow error. Please report this to Datadog.");
false
}
}
});

if !allocation_profiling {
return;
}

ZEND_MM_STATE.with(|cell| {
let zend_mm_state = cell.get();

Expand Down Expand Up @@ -247,16 +167,6 @@ pub fn alloc_prof_rinit() {
}

pub fn alloc_prof_rshutdown() {
let allocation_profiling = REQUEST_LOCALS.with(|cell| {
cell.try_borrow()
.map(|locals| locals.system_settings().profiling_allocation_enabled)
.unwrap_or(false)
});

if !allocation_profiling {
return;
}

// If `is_zend_mm()` is true, the custom handlers have been reset to `None`
// already. This is unexpected, therefore we will not touch the ZendMM
// handlers anymore as resetting to prev handlers might result in segfaults
Expand Down Expand Up @@ -401,7 +311,7 @@ unsafe extern "C" fn alloc_prof_malloc(len: size_t) -> *mut c_void {
}

unsafe fn alloc_prof_prev_alloc(len: size_t) -> *mut c_void {
// Safety: `ALLOCATION_PROFILING_ALLOC` will be initialised in
// Safety: `ZEND_MM_STATE.prev_custom_mm_alloc` will be initialised in
// `alloc_prof_rinit()` and only point to this function when
// `prev_custom_mm_alloc` is also initialised
let alloc = tls_zend_mm_state!(prev_custom_mm_alloc).unwrap();
Expand All @@ -426,8 +336,8 @@ unsafe extern "C" fn alloc_prof_free(ptr: *mut c_void) {
}

unsafe fn alloc_prof_prev_free(ptr: *mut c_void) {
// Safety: `ALLOCATION_PROFILING_FREE` will be initialised in
// `alloc_prof_free()` and only point to this function when
// Safety: `ZEND_MM_STATE.prev_custom_mm_free` will be initialised in
// `alloc_prof_rinit()` and only point to this function when
// `prev_custom_mm_free` is also initialised
let free = tls_zend_mm_state!(prev_custom_mm_free).unwrap();
free(ptr)
Expand Down Expand Up @@ -459,8 +369,8 @@ unsafe extern "C" fn alloc_prof_realloc(prev_ptr: *mut c_void, len: size_t) -> *
}

unsafe fn alloc_prof_prev_realloc(prev_ptr: *mut c_void, len: size_t) -> *mut c_void {
// Safety: `ALLOCATION_PROFILING_REALLOC` will be initialised in
// `alloc_prof_realloc()` and only point to this function when
// Safety: `ZEND_MM_STATE.prev_custom_mm_realloc` will be initialised in
// `alloc_prof_rinit()` and only point to this function when
// `prev_custom_mm_realloc` is also initialised
let realloc = tls_zend_mm_state!(prev_custom_mm_realloc).unwrap();
realloc(prev_ptr, len)
Expand Down
138 changes: 138 additions & 0 deletions profiling/src/allocation/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use crate::bindings::{self as zend};
use crate::profiling::Profiler;
use crate::REQUEST_LOCALS;
use libc::size_t;
use log::{error, trace};
use rand::rngs::ThreadRng;
use rand_distr::{Distribution, Poisson};
use std::cell::RefCell;
use std::sync::atomic::AtomicU64;

#[cfg(php_zend_mm_set_custom_handlers_ex)]
mod allocation_ge84;
#[cfg(not(php_zend_mm_set_custom_handlers_ex))]
pub mod allocation_le83;

/// take a sample every 4096 KiB
pub const ALLOCATION_PROFILING_INTERVAL: f64 = 1024.0 * 4096.0;

/// This will store the count of allocations (including reallocations) during
/// a profiling period. This will overflow when doing more than u64::MAX
/// allocations, which seems big enough to ignore.
pub static ALLOCATION_PROFILING_COUNT: AtomicU64 = AtomicU64::new(0);

/// This will store the accumulated size of all allocations in bytes during the
/// profiling period. This will overflow when allocating more than 18 exabyte
/// of memory (u64::MAX) which might not happen, so we can ignore this.
pub static ALLOCATION_PROFILING_SIZE: AtomicU64 = AtomicU64::new(0);

pub struct AllocationProfilingStats {
/// number of bytes until next sample collection
next_sample: i64,
poisson: Poisson<f64>,
rng: ThreadRng,
}

impl AllocationProfilingStats {
fn new() -> AllocationProfilingStats {
// Safety: this will only error if lambda <= 0
let poisson = Poisson::new(ALLOCATION_PROFILING_INTERVAL).unwrap();
let mut stats = AllocationProfilingStats {
next_sample: 0,
poisson,
rng: rand::thread_rng(),
};
stats.next_sampling_interval();
stats
}

fn next_sampling_interval(&mut self) {
self.next_sample = self.poisson.sample(&mut self.rng) as i64;
}

fn track_allocation(&mut self, len: size_t) {
self.next_sample -= len as i64;

if self.next_sample > 0 {
return;
}

self.next_sampling_interval();

if let Some(profiler) = Profiler::get() {
// Safety: execute_data was provided by the engine, and the profiler doesn't mutate it.
unsafe {
profiler.collect_allocations(
zend::ddog_php_prof_get_current_execute_data(),
1_i64,
len as i64,
)
};
}
}
}

thread_local! {
static ALLOCATION_PROFILING_STATS: RefCell<AllocationProfilingStats> =
RefCell::new(AllocationProfilingStats::new());
}

pub fn alloc_prof_ginit() {
#[cfg(not(php_zend_mm_set_custom_handlers_ex))]
allocation_le83::alloc_prof_ginit();
#[cfg(php_zend_mm_set_custom_handlers_ex)]
allocation_ge84::alloc_prof_ginit();
}

pub fn alloc_prof_gshutdown() {
#[cfg(php_zend_mm_set_custom_handlers_ex)]
allocation_ge84::alloc_prof_gshutdown();
}

#[cfg(not(php_zend_mm_set_custom_handlers_ex))]
pub fn alloc_prof_startup() {
allocation_le83::alloc_prof_startup();
}

pub fn alloc_prof_rinit() {
let allocation_profiling: bool = REQUEST_LOCALS.with(|cell| {
match cell.try_borrow() {
Ok(locals) => {
let system_settings = locals.system_settings();
system_settings.profiling_allocation_enabled
},
Err(_err) => {
error!("Memory allocation was not initialized correctly due to a borrow error. Please report this to Datadog.");
false
}
}
});

if !allocation_profiling {
return;
}

#[cfg(not(php_zend_mm_set_custom_handlers_ex))]
allocation_le83::alloc_prof_rinit();
#[cfg(php_zend_mm_set_custom_handlers_ex)]
allocation_ge84::alloc_prof_rinit();

trace!("Memory allocation profiling enabled.")
}

pub fn alloc_prof_rshutdown() {
let allocation_profiling = REQUEST_LOCALS.with(|cell| {
cell.try_borrow()
.map(|locals| locals.system_settings().profiling_allocation_enabled)
.unwrap_or(false)
});

if !allocation_profiling {
return;
}

#[cfg(not(php_zend_mm_set_custom_handlers_ex))]
allocation_le83::alloc_prof_rshutdown();
#[cfg(php_zend_mm_set_custom_handlers_ex)]
allocation_ge84::alloc_prof_rshutdown();
}
8 changes: 6 additions & 2 deletions profiling/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ pub type VmMmCustomAllocFn = unsafe extern "C" fn(size_t) -> *mut c_void;
pub type VmMmCustomReallocFn = unsafe extern "C" fn(*mut c_void, size_t) -> *mut c_void;
#[cfg(feature = "allocation_profiling")]
pub type VmMmCustomFreeFn = unsafe extern "C" fn(*mut c_void);
#[cfg(all(feature = "allocation_profiling", php_zend_mm_set_custom_handlers_ex))]
pub type VmMmCustomGcFn = unsafe extern "C" fn() -> size_t;
#[cfg(all(feature = "allocation_profiling", php_zend_mm_set_custom_handlers_ex))]
pub type VmMmCustomShutdownFn = unsafe extern "C" fn(bool, bool);

// todo: this a lie on some PHP versions; is it a problem even though zend_bool
// was always supposed to be 0 or 1 anyway?
Expand Down Expand Up @@ -175,13 +179,13 @@ pub struct ModuleEntry {
/// thread for module globals. The function pointers in [`ModuleEntry::globals_ctor`] and
/// [`ModuleEntry::globals_dtor`] will only be called if this is a non-zero.
pub globals_size: size_t,
#[cfg(php_zts)]
/// Pointer to a `ts_rsrc_id` (which is a [`i32`]). For C-Extension this is created using the
/// `ZEND_DECLARE_MODULE_GLOBALS(module_name)` macro.
/// See <https://heap.space/xref/PHP-8.3/Zend/zend_API.h?r=a89d22cc#249>
#[cfg(php_zts)]
pub globals_id_ptr: *mut ts_rsrc_id,
#[cfg(not(php_zts))]
/// Pointer to the module globals struct in NTS mode
#[cfg(not(php_zts))]
pub globals_ptr: *mut c_void,
/// Constructor for module globals.
/// Be aware this will only be called in case [`ModuleEntry::globals_size`] is non-zero and for
Expand Down
4 changes: 3 additions & 1 deletion profiling/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#[cfg(not(php_zend_mm_set_custom_handlers_ex))]
use crate::allocation;
use crate::bindings::zai_config_type::*;
use crate::bindings::{
Expand Down Expand Up @@ -101,7 +102,8 @@ impl SystemSettings {
}

// Work around version-specific issues.
if allocation::first_rinit_should_disable_due_to_jit() {
#[cfg(not(php_zend_mm_set_custom_handlers_ex))]
if allocation::allocation_le83::first_rinit_should_disable_due_to_jit() {
system_settings.profiling_allocation_enabled = false;
}
swap(&mut system_settings, SYSTEM_SETTINGS.assume_init_mut());
Expand Down
Loading

0 comments on commit 46ec759

Please sign in to comment.