Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(profiling) support new ZendMM api #2969

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions profiling/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,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")
realFlowControl marked this conversation as resolved.
Show resolved Hide resolved
.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::sync::atomic::AtomicU64;
use std::cell::UnsafeCell;
use std::sync::atomic::Ordering::{Relaxed, SeqCst};
use std::{ffi, ptr};

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
Loading