Skip to content

Commit 47bcde2

Browse files
perf(prof): speed up interrupt_count checks (#4074)
Co-authored-by: Levi Morrison <levi.morrison@datadoghq.com>
1 parent e3b075e commit 47bcde2

7 files changed

Lines changed: 112 additions & 61 deletions

File tree

profiling/src/allocation/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,10 @@ pub fn collect_allocation(ptr: *mut c_void, len: size_t) {
199199
// Check if there's a pending time interrupt that we can handle now
200200
// instead of waiting for an interrupt handler. This is slightly more
201201
// accurate and efficient, win-win.
202-
let interrupt_count = REQUEST_LOCALS
203-
.try_with_borrow(|locals| locals.interrupt_count.swap(0, Ordering::SeqCst))
204-
.unwrap_or(0);
202+
// SAFETY: allocation samples are collected on an initialized PHP request thread.
203+
let globals = unsafe { module_globals::get_profiler_globals() };
204+
// SAFETY: the current thread's module globals are valid through GSHUTDOWN.
205+
let interrupt_count = unsafe { (*globals).interrupt_count.swap(0, Ordering::Relaxed) };
205206

206207
// SAFETY: execute_data was provided by the engine, and the profiler
207208
// doesn't mutate it.

profiling/src/capi.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ extern "C" fn ddog_php_prof_trigger_time_sample() {
4646
if locals.system_settings().profiling_enabled {
4747
// Safety: only vm interrupts are stored there, or possibly null (edges only).
4848
if let Some(vm_interrupt) = unsafe { locals.vm_interrupt_addr.as_ref() } {
49-
locals.interrupt_count.fetch_add(1, Ordering::SeqCst);
49+
// SAFETY: this callback runs on an initialized PHP request thread.
50+
let globals = unsafe { crate::module_globals::get_profiler_globals() };
51+
// SAFETY: the current thread's module globals are valid through GSHUTDOWN.
52+
unsafe { (*globals).interrupt_count.fetch_add(1, Ordering::Relaxed) };
5053
vm_interrupt.store(true, Ordering::SeqCst);
5154
}
5255
}

profiling/src/lib.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,6 @@ pub struct RequestLocals {
425425
pub system_settings: ptr::NonNull<SystemSettings>,
426426
pub profiling_experimental_heap_live_enabled: bool,
427427

428-
pub interrupt_count: AtomicU32,
429428
pub vm_interrupt_addr: *const AtomicBool,
430429
}
431430

@@ -450,7 +449,6 @@ impl Default for RequestLocals {
450449
tags: vec![],
451450
system_settings: SystemSettings::get(),
452451
profiling_experimental_heap_live_enabled: false,
453-
interrupt_count: AtomicU32::new(0),
454452
vm_interrupt_addr: ptr::null_mut(),
455453
}
456454
}
@@ -738,8 +736,11 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult {
738736
}
739737

740738
if let Some(profiler) = Profiler::get() {
739+
// SAFETY: PHP module globals are initialized for this request thread.
740+
let globals = unsafe { module_globals::get_profiler_globals() };
741741
let interrupt = VmInterrupt {
742-
interrupt_count_ptr: &locals.interrupt_count as *const AtomicU32,
742+
// SAFETY: `globals` is valid until this thread's GSHUTDOWN.
743+
interrupt_count_ptr: unsafe { ptr::addr_of!((*globals).interrupt_count) },
743744
engine_ptr: locals.vm_interrupt_addr,
744745
};
745746
profiler.add_interrupt(interrupt);
@@ -795,8 +796,11 @@ extern "C" fn rshutdown(_type: c_int, _module_number: c_int) -> ZendResult {
795796
// and we don't need to optimize for that.
796797
if system_settings.profiling_enabled {
797798
if let Some(profiler) = Profiler::get() {
799+
// SAFETY: PHP module globals remain initialized through RSHUTDOWN.
800+
let globals = unsafe { module_globals::get_profiler_globals() };
798801
let interrupt = VmInterrupt {
799-
interrupt_count_ptr: &locals.interrupt_count,
802+
// SAFETY: `globals` remains valid until this thread's GSHUTDOWN.
803+
interrupt_count_ptr: unsafe { ptr::addr_of!((*globals).interrupt_count) },
800804
engine_ptr: locals.vm_interrupt_addr,
801805
};
802806
profiler.remove_interrupt(interrupt);

profiling/src/module_globals.rs

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::allocation;
22
use core::cell::Cell;
33
use core::ffi::c_void;
44
use core::ptr;
5+
use core::sync::atomic::AtomicU32;
56

67
#[cfg(php_zend_mm_set_custom_handlers_ex)]
78
use crate::allocation::allocation_ge84::ZendMMState;
@@ -13,6 +14,12 @@ pub struct ProfilerGlobals {
1314
/// Wrapped in `Cell` to prevent torn reads/writes when allocation hooks
1415
/// are called re-entrantly during `rinit()`/`rshutdown()`.
1516
pub zend_mm_state: Cell<ZendMMState>,
17+
/// Number of profiler time interrupts pending for this PHP thread.
18+
///
19+
/// The profiler timer thread updates this through a pointer registered by
20+
/// the PHP thread, so the value must remain atomic despite living in
21+
/// thread-local PHP module globals.
22+
pub interrupt_count: AtomicU32,
1623
}
1724

1825
/// We need TSRM to call into GINIT and GSHUTDOWN to observe spawning and
@@ -29,6 +36,7 @@ pub static mut GLOBALS_ID: i32 = 0;
2936
#[cfg(not(php_zts))]
3037
pub static mut GLOBALS: ProfilerGlobals = ProfilerGlobals {
3138
zend_mm_state: Cell::new(ZendMMState::new()),
39+
interrupt_count: AtomicU32::new(0),
3240
};
3341

3442
#[cfg(php_zts)]
@@ -40,9 +48,13 @@ mod zts {
4048
}
4149

4250
#[inline]
43-
pub unsafe fn tsrmg_bulk(id: i32) -> *mut c_void {
44-
let tls = tsrm_get_ls_cache() as *mut *mut *mut c_void;
45-
let storage = *tls; // void** storage
51+
pub unsafe fn get_ls_cache() -> *mut c_void {
52+
tsrm_get_ls_cache()
53+
}
54+
55+
#[inline]
56+
pub unsafe fn tsrmg_bulk(ls_cache: *mut c_void, id: i32) -> *mut c_void {
57+
let storage = *(ls_cache as *mut *mut *mut c_void); // void** storage
4658

4759
// TSRM_UNSHUFFLE_RSRC_ID(id) is just `id - 1`.
4860
let idx = (id - 1) as usize;
@@ -51,6 +63,27 @@ mod zts {
5163
}
5264
}
5365

66+
#[cfg(php_zts)]
67+
#[inline]
68+
pub unsafe fn get_tsrm_ls_cache() -> *mut c_void {
69+
zts::get_ls_cache()
70+
}
71+
72+
#[cfg(php_zts)]
73+
#[inline]
74+
pub unsafe fn get_tsrm_resource_from_cache(ls_cache: *mut c_void, id: i32) -> *mut c_void {
75+
zts::tsrmg_bulk(ls_cache, id)
76+
}
77+
78+
#[cfg(php_zts)]
79+
#[inline]
80+
pub unsafe fn get_profiler_globals_from_cache(ls_cache: *mut c_void) -> *mut ProfilerGlobals {
81+
// SAFETY: As long as this is called during the times documented by
82+
// get_profiler_globals(), GLOBALS_ID will be set by PHP.
83+
let id = ptr::addr_of!(GLOBALS_ID).read();
84+
get_tsrm_resource_from_cache(ls_cache, id).cast()
85+
}
86+
5487
/// Returns a pointer to the profiler globals for the current thread.
5588
///
5689
/// # Safety
@@ -64,10 +97,7 @@ mod zts {
6497
pub unsafe fn get_profiler_globals() -> *mut ProfilerGlobals {
6598
#[cfg(php_zts)]
6699
{
67-
// SAFETY: As long as this is called during the times documented by
68-
// our own safety requirements, GLOBALS_ID will be set by PHP.
69-
let id = ptr::addr_of!(GLOBALS_ID).read();
70-
zts::tsrmg_bulk(id).cast()
100+
get_profiler_globals_from_cache(get_tsrm_ls_cache())
71101
}
72102

73103
#[cfg(not(php_zts))]
@@ -85,12 +115,13 @@ pub unsafe extern "C" fn ginit(_globals_ptr: *mut c_void) {
85115
#[cfg(php_zts)]
86116
crate::timeline::timeline_ginit();
87117

88-
// Initialize ZendMMState in PHP globals for ZTS builds. For NTS builds,
89-
// this was already done in its const initializer.
118+
// Initialize PHP globals for ZTS builds. For NTS builds, this was already
119+
// done in its const initializer.
90120
#[cfg(php_zts)]
91121
{
92122
let globals = _globals_ptr.cast::<ProfilerGlobals>();
93123
(*globals).zend_mm_state = Cell::new(ZendMMState::new());
124+
(*globals).interrupt_count = AtomicU32::new(0);
94125
}
95126

96127
// SAFETY: this is called in thread ginit as expected, and no other places.
@@ -113,3 +144,19 @@ pub unsafe extern "C" fn gshutdown(_globals_ptr: *mut c_void) {
113144
// SAFETY: this is called in thread gshutdown as expected, no other places.
114145
allocation::gshutdown();
115146
}
147+
148+
// Unit tests are not loaded by PHP, so provide the PHP globals and TSRM symbol
149+
// needed to link code retained in the test executable.
150+
#[cfg(test)]
151+
mod test_symbols {
152+
#[cfg(not(php_zts))]
153+
#[export_name = "compiler_globals"]
154+
static mut TEST_COMPILER_GLOBALS: core::mem::MaybeUninit<crate::zend::zend_compiler_globals> =
155+
core::mem::MaybeUninit::zeroed();
156+
157+
#[cfg(php_zts)]
158+
#[no_mangle]
159+
unsafe extern "C" fn tsrm_get_ls_cache() -> *mut core::ffi::c_void {
160+
core::ptr::null_mut()
161+
}
162+
}

profiling/src/profiling/interrupts.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ impl InterruptManager {
5050
// Reset interrupt counter to prevent sampling during `mshutdown` (PHP 8.0 bug with
5151
// userland destructors), but leave the interrupt flag unchanged as other extensions
5252
// may have raised it.
53-
(*interrupt.interrupt_count_ptr).store(0, Ordering::SeqCst);
53+
(*interrupt.interrupt_count_ptr).store(0, Ordering::Relaxed);
5454
}
5555
}
5656

@@ -62,7 +62,7 @@ impl InterruptManager {
6262
pub(super) fn trigger_interrupts(&self) {
6363
let vm_interrupts = self.vm_interrupts.lock().unwrap();
6464
vm_interrupts.iter().for_each(|obj| unsafe {
65-
(*obj.interrupt_count_ptr).fetch_add(1, Ordering::SeqCst);
65+
(*obj.interrupt_count_ptr).fetch_add(1, Ordering::Relaxed);
6666
(*obj.engine_ptr).store(true, Ordering::SeqCst);
6767
});
6868
}

profiling/src/profiling/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,7 @@ impl Profiler {
11301130

11311131
/// Collect a stack sample with elapsed wall time. Collects CPU time if
11321132
/// it's enabled and available.
1133+
#[export_name = "ddog_php_prof_collect_time"]
11331134
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "debug"))]
11341135
pub fn collect_time(&self, execute_data: *mut zend_execute_data, interrupt_count: u32) {
11351136
// todo: should probably exclude the wall and CPU time used by collecting the sample.

profiling/src/wall_time.rs

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
//! implementation reasons, it has cpu-time code as well.
33
44
use crate::bindings::{zend_execute_data, zend_interrupt_function, VmInterruptFn};
5-
use crate::{profiling::Profiler, RefCellExt, REQUEST_LOCALS};
5+
use crate::module_globals;
6+
use crate::profiling::Profiler;
67
use core::ptr;
7-
use log::debug;
8-
use std::sync::atomic::Ordering;
8+
use core::sync::atomic::Ordering;
99

1010
#[cfg(not(php_frameless))]
1111
mod execute_internal {
@@ -79,7 +79,7 @@ mod execute_internal {
7979
unsafe { prev_execute_internal(execute_data, return_value) };
8080

8181
// See safety section of `execute_data_func_is_trampoline` docs for why
82-
// the leaf frame is used instead of the execute_data ptr.
82+
// the leaf frame is used instead of the execute_data ptr.
8383
ddog_php_prof_interrupt_function(leaf_frame);
8484
}
8585

@@ -109,31 +109,29 @@ static mut PREV_INTERRUPT_FUNCTION: Option<VmInterruptFn> = None;
109109
#[no_mangle]
110110
#[inline(never)]
111111
pub extern "C" fn ddog_php_prof_interrupt_function(execute_data: *mut zend_execute_data) {
112-
let result = REQUEST_LOCALS.try_with_borrow(|locals| {
113-
if !locals.system_settings().profiling_enabled {
114-
return;
115-
}
116-
117-
/* Other extensions/modules or the engine itself may trigger an
118-
* interrupt, but given how expensive it is to gather a stack trace,
119-
* it should only be done if we triggered it ourselves. So
120-
* interrupt_count serves dual purposes:
121-
* 1. Track how many interrupts there were.
122-
* 2. Ensure we don't collect on someone else's interrupt.
123-
*/
124-
let interrupt_count = locals.interrupt_count.swap(0, Ordering::SeqCst);
125-
if interrupt_count == 0 {
126-
return;
127-
}
128-
129-
if let Some(profiler) = Profiler::get() {
130-
// Safety: execute_data was provided by the engine, and the profiler doesn't mutate it.
131-
profiler.collect_time(execute_data, interrupt_count);
132-
}
133-
});
112+
// SAFETY: interrupt callbacks run while the current PHP thread's module globals are valid.
113+
let atomic_count = unsafe { &(*module_globals::get_profiler_globals()).interrupt_count };
114+
115+
/* Other extensions/modules or the engine itself may trigger an
116+
* interrupt, but given how expensive it is to gather a stack trace,
117+
* it should only be done if we triggered it ourselves. So
118+
* interrupt_count serves dual purposes:
119+
* 1. Track how many interrupts there were.
120+
* 2. Ensure we don't collect on someone else's interrupt.
121+
*/
122+
let interrupt_count = atomic_count.swap(0, Ordering::Relaxed);
123+
if interrupt_count == 0 {
124+
return;
125+
}
126+
collect_time_if_enabled(execute_data, interrupt_count);
127+
}
134128

135-
if let Err(err) = result {
136-
debug!("ddog_php_prof_interrupt_function failed to borrow request locals: {err}");
129+
#[inline(never)]
130+
#[export_name = "ddog_php_prof_collect_time_if_enabled"]
131+
extern "C" fn collect_time_if_enabled(execute_data: *mut zend_execute_data, interrupt_count: u32) {
132+
if let Some(profiler) = Profiler::get() {
133+
// Safety: execute_data was provided by the engine, and the profiler doesn't mutate it.
134+
profiler.collect_time(execute_data, interrupt_count);
137135
}
138136
}
139137

@@ -144,7 +142,9 @@ mod frameless {
144142
use crate::bindings::{
145143
zend_flf_functions, zend_flf_handlers, zend_frameless_function_info,
146144
};
147-
use crate::{profiling::Profiler, zend, RefCellExt, REQUEST_LOCALS};
145+
use crate::module_globals;
146+
use crate::wall_time::collect_time_if_enabled;
147+
use crate::zend;
148148
use dynasmrt::{dynasm, DynasmApi, ExecutableBuffer};
149149
use log::error;
150150
use std::ffi::c_void;
@@ -270,24 +270,19 @@ mod frameless {
270270
#[no_mangle]
271271
#[inline(never)]
272272
pub extern "C" fn ddog_php_prof_icall_trampoline_target() {
273-
let interrupt_count = REQUEST_LOCALS
274-
.try_with_borrow(|locals| {
275-
if !locals.system_settings().profiling_enabled {
276-
return 0;
277-
}
278-
locals.interrupt_count.swap(0, Ordering::SeqCst)
279-
})
280-
.unwrap_or(0);
273+
// SAFETY: frameless handlers run while the current PHP thread's module globals are
274+
// valid. Retain the pointer so the authoritative swap reuses the same TSRM lookup.
275+
let atomic_count =
276+
unsafe { &(*module_globals::get_profiler_globals()).interrupt_count };
281277

278+
let interrupt_count = atomic_count.swap(0, Ordering::Relaxed);
282279
if interrupt_count == 0 {
283280
return;
284281
}
285282

286-
if let Some(profiler) = Profiler::get() {
287-
// SAFETY: profiler doesn't mutate execute_data
288-
let execute_data = unsafe { zend::ddog_php_prof_get_current_execute_data() };
289-
profiler.collect_time(execute_data, interrupt_count);
290-
}
283+
// Fetching execute data is intentionally delayed until a profiler interrupt is pending.
284+
let execute_data = unsafe { zend::ddog_php_prof_get_current_execute_data() };
285+
collect_time_if_enabled(execute_data, interrupt_count);
291286
}
292287
}
293288

0 commit comments

Comments
 (0)