From 35291b35715f972857edd563540f0e146fa89596 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Wed, 13 Nov 2024 12:36:59 +0100 Subject: [PATCH] poc --- profiling/build.rs | 1 + profiling/src/bindings/mod.rs | 2 + profiling/src/profiling/mod.rs | 40 ++++++++++++++++++ profiling/src/profiling/thread_utils.rs | 3 +- profiling/src/timeline.rs | 56 +++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 1 deletion(-) diff --git a/profiling/build.rs b/profiling/build.rs index a87476a460..77f5349b8f 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -357,6 +357,7 @@ fn cfg_php_feature_flags(vernum: u64) { } if vernum >= 80400 { println!("cargo:rustc-cfg=php_frameless"); + println!("cargo:rustc-cfg=php_opcache_restart_hook"); } } diff --git a/profiling/src/bindings/mod.rs b/profiling/src/bindings/mod.rs index b1734cc87c..8258ec3da6 100644 --- a/profiling/src/bindings/mod.rs +++ b/profiling/src/bindings/mod.rs @@ -20,6 +20,8 @@ pub type VmGcCollectCyclesFn = unsafe extern "C" fn() -> i32; #[cfg(feature = "timeline")] pub type VmZendCompileFile = unsafe extern "C" fn(*mut zend_file_handle, i32) -> *mut _zend_op_array; +#[cfg(all(feature = "timeline", php_opcache_restart_hook))] +pub type VmZendAccelScheduleRestartHook = unsafe extern "C" fn(i32); #[cfg(all(feature = "timeline", php_zend_compile_string_has_position))] pub type VmZendCompileString = unsafe extern "C" fn( *mut zend_string, diff --git a/profiling/src/profiling/mod.rs b/profiling/src/profiling/mod.rs index 75f3e4e7ca..afcce4be0b 100644 --- a/profiling/src/profiling/mod.rs +++ b/profiling/src/profiling/mod.rs @@ -1051,6 +1051,46 @@ impl Profiler { } } + /// This function can be called to collect an opcache restart + #[cfg(feature = "timeline")] + pub(crate) fn collect_opcache_restart( + &self, + now: i64, + file: String, + line: u32, + reason: &'static str, + ) { + let mut labels = Profiler::common_labels(1); + + labels.push(Label { + key: "reason", + value: LabelValue::Str(reason.into()), + }); + + let n_labels = labels.len(); + + match self.prepare_and_send_message( + vec![ZendFrame { + function: "[opcache restart]".into(), + file: Some(file), + line, + }], + SampleValues { + timeline: 1, + ..Default::default() + }, + labels, + now, + ) { + Ok(_) => { + trace!("Sent event 'idle' with {n_labels} labels to profiler.") + } + Err(err) => { + warn!("Failed to send event 'idle' with {n_labels} labels to profiler: {err}") + } + } + } + /// This function can be called to collect any kind of inactivity that is happening #[cfg(feature = "timeline")] pub fn collect_idle(&self, now: i64, duration: i64, reason: &'static str) { diff --git a/profiling/src/profiling/thread_utils.rs b/profiling/src/profiling/thread_utils.rs index 1fec857e11..0848ba33d5 100644 --- a/profiling/src/profiling/thread_utils.rs +++ b/profiling/src/profiling/thread_utils.rs @@ -119,7 +119,8 @@ pub fn get_current_thread_name() -> String { } thread_name - }).clone() + }) + .clone() }) } diff --git a/profiling/src/timeline.rs b/profiling/src/timeline.rs index ab69528ca1..3c6aa6551e 100644 --- a/profiling/src/timeline.rs +++ b/profiling/src/timeline.rs @@ -24,6 +24,11 @@ static mut PREV_ZEND_COMPILE_STRING: Option = None; /// The engine's original (or neighbouring extensions) `zend_compile_file()` function static mut PREV_ZEND_COMPILE_FILE: Option = None; +/// The engine's original (or neighbouring extensions) `zend_accel_schedule_restart_hook()` +/// function +static mut PREV_ZEND_ACCEL_SCHEDULE_RESTART_HOOK: Option = + None; + static mut SLEEP_HANDLER: InternalFunctionHandler = None; static mut USLEEP_HANDLER: InternalFunctionHandler = None; static mut TIME_NANOSLEEP_HANDLER: InternalFunctionHandler = None; @@ -192,12 +197,63 @@ unsafe extern "C" fn ddog_php_prof_zend_error_observer( } } +/// Will be called by the opcache extension when a restart is scheduled. The `reason` is this enum: +/// ```C +/// typedef enum _zend_accel_restart_reason { +/// ACCEL_RESTART_OOM, /* restart because of out of memory */ +/// ACCEL_RESTART_HASH, /* restart because of hash overflow */ +/// ACCEL_RESTART_USER /* restart scheduled by opcache_reset() */ +/// } zend_accel_restart_reason; +/// ``` +#[no_mangle] +#[cfg(php_opcache_restart_hook)] +unsafe extern "C" fn ddog_php_prof_zend_accel_schedule_restart_hook(reason: i32) { + let timeline_enabled = REQUEST_LOCALS.with(|cell| { + cell.try_borrow() + .map(|locals| locals.system_settings().profiling_timeline_enabled) + .unwrap_or(false) + }); + + if timeline_enabled { + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + if let Some(profiler) = Profiler::get() { + let now = now.as_nanos() as i64; + let file = unsafe { + zend::zai_str_from_zstr(zend::zend_get_executed_filename_ex().as_mut()) + .into_string() + }; + profiler.collect_opcache_restart( + now, + file, + zend::zend_get_executed_lineno(), + match reason { + 0 => "out of memory", + 1 => "hash overflow", + 2 => "`opcache_restart()` called", + _ => "unknown", + }, + ); + } + } + + if let Some(prev) = PREV_ZEND_ACCEL_SCHEDULE_RESTART_HOOK { + prev(reason); + } +} + /// This functions needs to be called in MINIT of the module pub fn timeline_minit() { unsafe { #[cfg(zend_error_observer)] zend::zend_observer_error_register(Some(ddog_php_prof_zend_error_observer)); + #[cfg(php_opcache_restart_hook)] + { + PREV_ZEND_ACCEL_SCHEDULE_RESTART_HOOK = zend::zend_accel_schedule_restart_hook; + zend::zend_accel_schedule_restart_hook = + Some(ddog_php_prof_zend_accel_schedule_restart_hook); + } + // register our function in the `gc_collect_cycles` pointer PREV_GC_COLLECT_CYCLES = zend::gc_collect_cycles; zend::gc_collect_cycles = Some(ddog_php_prof_gc_collect_cycles);