Skip to content

Commit 507243c

Browse files
committed
feat(profiling): remove execute_internal hook
This does some... uh... let's say "clever" things to avoid using the zend_execute_internal hook. In PHP 8.4, that would prevent frameless function call optimizations from being used. Using the hook also has performance costs even on older versions.
1 parent 3c0d0f9 commit 507243c

File tree

10 files changed

+210
-85
lines changed

10 files changed

+210
-85
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ debug = 2 # full debug info
1414

1515
[profile.release]
1616
codegen-units = 1
17-
debug = "line-tables-only"
17+
debug = 2 #todo: revert back
1818
incremental = false
1919
lto = "fat"
2020

profiling/build.rs

+15
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ fn main() {
2727
let vernum = php_config_vernum();
2828
let post_startup_cb = cfg_post_startup_cb(vernum);
2929
let preload = cfg_preload(vernum);
30+
let good_closure_invoke = cfg_good_closure_invoke(vernum);
3031
let fibers = cfg_fibers(vernum);
3132
let run_time_cache = cfg_run_time_cache(vernum);
3233
let trigger_time_sample = cfg_trigger_time_sample();
@@ -37,6 +38,7 @@ fn main() {
3738
post_startup_cb,
3839
preload,
3940
run_time_cache,
41+
good_closure_invoke,
4042
fibers,
4143
trigger_time_sample,
4244
vernum,
@@ -83,11 +85,13 @@ const ZAI_H_FILES: &[&str] = &[
8385
"../zend_abstract_interface/json/json.h",
8486
];
8587

88+
#[allow(clippy::too_many_arguments)]
8689
fn build_zend_php_ffis(
8790
php_config_includes: &str,
8891
post_startup_cb: bool,
8992
preload: bool,
9093
run_time_cache: bool,
94+
good_closure_invoke: bool,
9195
fibers: bool,
9296
trigger_time_sample: bool,
9397
vernum: u64,
@@ -132,6 +136,7 @@ fn build_zend_php_ffis(
132136
let files = ["src/php_ffi.c", "../ext/handlers_api.c"];
133137
let post_startup_cb = if post_startup_cb { "1" } else { "0" };
134138
let preload = if preload { "1" } else { "0" };
139+
let good_closure_invoke = if good_closure_invoke { "1" } else { "0" };
135140
let fibers = if fibers { "1" } else { "0" };
136141
let run_time_cache = if run_time_cache { "1" } else { "0" };
137142
let trigger_time_sample = if trigger_time_sample { "1" } else { "0" };
@@ -146,6 +151,7 @@ fn build_zend_php_ffis(
146151
.files(files.into_iter().chain(zai_c_files))
147152
.define("CFG_POST_STARTUP_CB", post_startup_cb)
148153
.define("CFG_PRELOAD", preload)
154+
.define("CFG_GOOD_CLOSURE_INVOKE", good_closure_invoke)
149155
.define("CFG_FIBERS", fibers)
150156
.define("CFG_RUN_TIME_CACHE", run_time_cache)
151157
.define("CFG_STACK_WALKING_TESTS", stack_walking_tests)
@@ -308,6 +314,15 @@ fn cfg_php_major_version(vernum: u64) {
308314
println!("cargo:rustc-cfg=php{major_version}");
309315
}
310316

317+
fn cfg_good_closure_invoke(vernum: u64) -> bool {
318+
if vernum >= 80300 {
319+
println!("cargo:rustc-cfg=php_good_closure_invoke");
320+
true
321+
} else {
322+
false
323+
}
324+
}
325+
311326
fn cfg_fibers(vernum: u64) -> bool {
312327
if vernum >= 80100 {
313328
println!("cargo:rustc-cfg=php_has_fibers");

profiling/src/bindings/mod.rs

+12-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use libc::{c_char, c_int, c_uchar, c_uint, c_ushort, c_void, size_t};
66
use std::borrow::Cow;
77
use std::ffi::CStr;
88
use std::marker::PhantomData;
9-
use std::sync::atomic::AtomicBool;
109
use std::{ptr, str};
1110

1211
extern "C" {
@@ -126,9 +125,8 @@ impl _zend_function {
126125

127126
/// Returns the module name, if there is one. May return Some(b"\0").
128127
pub fn module_name(&self) -> Option<&[u8]> {
129-
// Safety: the function's type field is always safe to access.
130-
if unsafe { self.type_ } == ZEND_INTERNAL_FUNCTION as u8 {
131-
// Safety: union access is guarded by ZEND_INTERNAL_FUNCTION, and
128+
if self.is_internal() {
129+
// SAFETY: union access is guarded by ZEND_INTERNAL_FUNCTION, and
132130
// assume its module is valid.
133131
unsafe { self.internal_function.module.as_ref() }
134132
.filter(|module| !module.name.is_null())
@@ -138,6 +136,12 @@ impl _zend_function {
138136
None
139137
}
140138
}
139+
140+
#[inline]
141+
pub fn is_internal(&self) -> bool {
142+
// Safety: the function's type field is always safe to access.
143+
unsafe { self.type_ == ZEND_INTERNAL_FUNCTION as u8 }
144+
}
141145
}
142146

143147
// In general, modify definitions which return int to return ZendResult if
@@ -283,10 +287,11 @@ impl Default for ZendExtension {
283287
}
284288

285289
extern "C" {
286-
/// Retrieves the VM interrupt address of the calling PHP thread.
290+
/// Retrieves the addresses of various parts of executor_globals.
291+
///
287292
/// # Safety
288293
/// Must be called from a PHP thread during a request.
289-
pub fn datadog_php_profiling_vm_interrupt_addr() -> *const AtomicBool;
294+
pub fn ddog_php_prof_executor_global_addrs() -> ExecutorGlobalAddrs;
290295

291296
/// Registers the extension. Note that it's kept in a zend_llist and gets
292297
/// pemalloc'd + memcpy'd into place. The engine says this is a mutable
@@ -342,6 +347,7 @@ extern "C" {
342347
}
343348

344349
use crate::config::ConfigId;
350+
use crate::ExecutorGlobalAddrs;
345351
pub use zend_module_dep as ModuleDep;
346352

347353
impl ModuleDep {

profiling/src/capi.rs

+10-5
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,16 @@ extern "C" fn ddog_php_prof_trigger_time_sample() {
4242
super::REQUEST_LOCALS.with(|cell| {
4343
if let Ok(locals) = cell.try_borrow() {
4444
if locals.system_settings().profiling_enabled {
45-
// Safety: only vm interrupts are stored there, or possibly null (edges only).
46-
if let Some(vm_interrupt) = unsafe { locals.vm_interrupt_addr.as_ref() } {
47-
locals.interrupt_count.fetch_add(1, Ordering::SeqCst);
48-
vm_interrupt.store(true, Ordering::SeqCst);
49-
}
45+
// Safety: only vm interrupts are stored there.
46+
let vm_interrupt = unsafe { locals.executor_global_addrs.vm_interrupt.as_ref() };
47+
let current_execute_data_cache =
48+
unsafe { *locals.executor_global_addrs.current_execute_data.as_ptr() };
49+
50+
locals.interrupt_count.fetch_add(1, Ordering::SeqCst);
51+
locals
52+
.current_execute_data_cache
53+
.store(current_execute_data_cache, Ordering::SeqCst);
54+
vm_interrupt.store(true, Ordering::SeqCst);
5055
}
5156
}
5257
})

profiling/src/lib.rs

+56-27
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ mod exception;
2121
#[cfg(feature = "timeline")]
2222
mod timeline;
2323

24+
use crate::bindings::ddog_php_prof_executor_global_addrs;
2425
use crate::config::{SystemSettings, INITIAL_SYSTEM_SETTINGS};
2526
use bindings::{
26-
self as zend, ddog_php_prof_php_version, ddog_php_prof_php_version_id, ZendExtension,
27-
ZendResult,
27+
self as zend, ddog_php_prof_php_version, ddog_php_prof_php_version_id, zend_execute_data, zval,
28+
ZendExtension, ZendResult,
2829
};
2930
use clocks::*;
3031
use core::ptr;
@@ -33,14 +34,14 @@ use lazy_static::lazy_static;
3334
use libc::c_char;
3435
use log::{debug, error, info, trace, warn};
3536
use once_cell::sync::{Lazy, OnceCell};
36-
use profiling::{LocalRootSpanResourceMessage, Profiler, VmInterrupt};
37+
use profiling::{LocalRootSpanResourceMessage, Profiler};
3738
use sapi::Sapi;
3839
use std::borrow::Cow;
3940
use std::cell::RefCell;
4041
use std::ffi::CStr;
4142
use std::ops::Deref;
4243
use std::os::raw::c_int;
43-
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
44+
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering};
4445
use std::sync::{Arc, Mutex, Once};
4546
use std::time::{Duration, Instant};
4647
use uuid::Uuid;
@@ -333,6 +334,23 @@ extern "C" fn prshutdown() -> ZendResult {
333334
ZendResult::Success
334335
}
335336

337+
// Keep in-sync with php_ffi.c.
338+
#[repr(C)]
339+
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
340+
pub struct ExecutorGlobalAddrs {
341+
pub vm_stack_top: ptr::NonNull<*mut zval>,
342+
pub current_execute_data: ptr::NonNull<*mut zend_execute_data>,
343+
pub vm_interrupt: ptr::NonNull<AtomicBool>,
344+
}
345+
346+
impl ExecutorGlobalAddrs {
347+
/// # Safety
348+
/// Must be called on a PHP thread during a request.
349+
unsafe fn new() -> ExecutorGlobalAddrs {
350+
ddog_php_prof_executor_global_addrs()
351+
}
352+
}
353+
336354
pub struct RequestLocals {
337355
pub env: Option<String>,
338356
pub service: Option<String>,
@@ -347,29 +365,33 @@ pub struct RequestLocals {
347365
pub system_settings: ptr::NonNull<SystemSettings>,
348366

349367
pub interrupt_count: AtomicU32,
350-
pub vm_interrupt_addr: *const AtomicBool,
368+
pub current_execute_data_cache: AtomicPtr<zend_execute_data>,
369+
pub executor_global_addrs: ExecutorGlobalAddrs,
351370
}
352371

353372
impl RequestLocals {
354-
#[track_caller]
355-
pub fn system_settings(&self) -> &SystemSettings {
356-
// SAFETY: it should always be valid, either set to the
357-
// INITIAL_SYSTEM_SETTINGS or to the SYSTEM_SETTINGS.
358-
unsafe { self.system_settings.as_ref() }
359-
}
360-
}
361-
362-
impl Default for RequestLocals {
363-
fn default() -> RequestLocals {
373+
/// # Safety
374+
/// Must be called from a PHP Thread.
375+
unsafe fn new() -> RequestLocals {
364376
RequestLocals {
365377
env: None,
366378
service: None,
367379
version: None,
368380
system_settings: ptr::NonNull::from(INITIAL_SYSTEM_SETTINGS.deref()),
369381
interrupt_count: AtomicU32::new(0),
370-
vm_interrupt_addr: ptr::null_mut(),
382+
current_execute_data_cache: AtomicPtr::new(ptr::null_mut()),
383+
384+
// SAFETY: required by this function's safety conditions.
385+
executor_global_addrs: ExecutorGlobalAddrs::new(),
371386
}
372387
}
388+
389+
#[track_caller]
390+
pub fn system_settings(&self) -> &SystemSettings {
391+
// SAFETY: it should always be valid, either set to the
392+
// INITIAL_SYSTEM_SETTINGS or to the SYSTEM_SETTINGS.
393+
unsafe { self.system_settings.as_ref() }
394+
}
373395
}
374396

375397
thread_local! {
@@ -378,7 +400,8 @@ thread_local! {
378400
wall_time: Instant::now(),
379401
});
380402

381-
static REQUEST_LOCALS: RefCell<RequestLocals> = RefCell::new(RequestLocals::default());
403+
// These are only meant to be used on a PHP thread.
404+
static REQUEST_LOCALS: RefCell<RequestLocals> = RefCell::new(unsafe { RequestLocals::new() });
382405

383406
/// The tags for this thread/request. These get sent to other threads,
384407
/// which is why they are Arc. However, they are wrapped in a RefCell
@@ -426,9 +449,9 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult {
426449
// initialize the thread local storage and cache some items
427450
REQUEST_LOCALS.with(|cell| {
428451
let mut locals = cell.borrow_mut();
429-
// Safety: we are in rinit on a PHP thread.
430-
locals.vm_interrupt_addr = unsafe { zend::datadog_php_profiling_vm_interrupt_addr() };
431452
locals.interrupt_count.store(0, Ordering::SeqCst);
453+
// Safety: we are in rinit on a PHP thread.
454+
locals.executor_global_addrs = unsafe { ddog_php_prof_executor_global_addrs() };
432455

433456
// Safety: We are after first rinit and before mshutdown.
434457
unsafe {
@@ -555,11 +578,17 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult {
555578
}
556579

557580
if let Some(profiler) = PROFILER.lock().unwrap().as_ref() {
558-
let interrupt = VmInterrupt {
559-
interrupt_count_ptr: &locals.interrupt_count as *const AtomicU32,
560-
engine_ptr: locals.vm_interrupt_addr,
581+
let id = profiling::interrupts::Id {
582+
vm_interrupt_addr: locals.executor_global_addrs.vm_interrupt,
583+
current_execute_data_addr: locals.executor_global_addrs.current_execute_data,
584+
};
585+
let state = profiling::interrupts::State {
586+
interrupt_count_addr: ptr::NonNull::from(&locals.interrupt_count),
587+
current_execute_data_addr: ptr::NonNull::from(
588+
&locals.current_execute_data_cache,
589+
),
561590
};
562-
profiler.add_interrupt(interrupt);
591+
profiler.add_interrupt(id, state);
563592
}
564593
});
565594
} else {
@@ -611,11 +640,11 @@ extern "C" fn rshutdown(_type: c_int, _module_number: c_int) -> ZendResult {
611640
// and we don't need to optimize for that.
612641
if system_settings.profiling_enabled {
613642
if let Some(profiler) = PROFILER.lock().unwrap().as_ref() {
614-
let interrupt = VmInterrupt {
615-
interrupt_count_ptr: &locals.interrupt_count,
616-
engine_ptr: locals.vm_interrupt_addr,
643+
let id = profiling::interrupts::Id {
644+
current_execute_data_addr: locals.executor_global_addrs.current_execute_data,
645+
vm_interrupt_addr: locals.executor_global_addrs.vm_interrupt,
617646
};
618-
profiler.remove_interrupt(interrupt);
647+
profiler.remove_interrupt(id);
619648
}
620649
}
621650
});

profiling/src/php_ffi.c

+16-1
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,22 @@ void datadog_php_profiling_startup(zend_extension *extension) {
239239
#endif
240240
}
241241

242-
void *datadog_php_profiling_vm_interrupt_addr(void) { return &EG(vm_interrupt); }
242+
// Keep in-sync with Rust ExecutorGlobalAddrs.
243+
struct executor_global_addrs {
244+
zval **vm_stack_top;
245+
zend_execute_data **current_execute_data;
246+
void *vm_interrupt;
247+
};
248+
249+
// Keep in-sync with Rust version.
250+
struct executor_global_addrs ddog_php_prof_executor_global_addrs(void) {
251+
struct executor_global_addrs addrs = {
252+
&EG(vm_stack_top),
253+
&EG(current_execute_data),
254+
(void*)&EG(vm_interrupt),
255+
};
256+
return addrs;
257+
}
243258

244259
zend_module_entry *datadog_get_module_entry(const char *str, uintptr_t len) {
245260
return zend_hash_str_find_ptr(&module_registry, str, len);

profiling/src/php_ffi.h

-5
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,6 @@ sapi_request_info datadog_sapi_globals_request_info();
5757
*/
5858
zend_module_entry *datadog_get_module_entry(const char *str, uintptr_t len);
5959

60-
/**
61-
* Fetches the VM interrupt address of the calling PHP thread.
62-
*/
63-
void *datadog_php_profiling_vm_interrupt_addr(void);
64-
6560
/**
6661
* For Code Hotspots, we need the tracer's local root span id and the current
6762
* span id. This is a cross-product struct, so keep it in sync with tracer's

0 commit comments

Comments
 (0)