Skip to content

Commit ea1d19c

Browse files
make allocation profiling thread safe by using thread local storage for the heap and the previous function pointers (if any)
1 parent b4daa1f commit ea1d19c

File tree

4 files changed

+122
-123
lines changed

4 files changed

+122
-123
lines changed

profiling/src/allocation.rs

Lines changed: 122 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use libc::{c_char, c_int, c_void, size_t};
77
use log::{debug, error, trace, warn};
88
use rand::rngs::ThreadRng;
99
use std::cell::RefCell;
10+
use std::cell::UnsafeCell;
1011
use std::ffi::CStr;
1112
use std::sync::atomic::AtomicU64;
1213
use std::sync::atomic::Ordering::SeqCst;
@@ -19,18 +20,6 @@ use crate::bindings::{
1920

2021
static mut GC_MEM_CACHES_HANDLER: zend::InternalFunctionHandler = None;
2122

22-
/// The engine's previous custom allocation function, if there is one.
23-
static mut PREV_CUSTOM_MM_ALLOC: Option<zend::VmMmCustomAllocFn> = None;
24-
25-
/// The engine's previous custom reallocation function, if there is one.
26-
static mut PREV_CUSTOM_MM_REALLOC: Option<zend::VmMmCustomReallocFn> = None;
27-
28-
/// The engine's previous custom free function, if there is one.
29-
static mut PREV_CUSTOM_MM_FREE: Option<zend::VmMmCustomFreeFn> = None;
30-
31-
/// The heap installed in ZendMM at the time we install our custom handlers
32-
static mut HEAP: Option<*mut zend::_zend_mm_heap> = None;
33-
3423
/// take a sample every 4096 KiB
3524
pub const ALLOCATION_PROFILING_INTERVAL: f64 = 1024.0 * 4096.0;
3625

@@ -50,6 +39,17 @@ pub struct AllocationProfilingStats {
5039
rng: ThreadRng,
5140
}
5241

42+
struct ZendMMState {
43+
/// The heap installed in ZendMM at the time we install our custom handlers
44+
heap: Option<*mut zend::zend_mm_heap>,
45+
/// The engine's previous custom allocation function, if there is one.
46+
prev_custom_mm_alloc: Option<zend::VmMmCustomAllocFn>,
47+
/// The engine's previous custom reallocation function, if there is one.
48+
prev_custom_mm_realloc: Option<zend::VmMmCustomReallocFn>,
49+
/// The engine's previous custom free function, if there is one.
50+
prev_custom_mm_free: Option<zend::VmMmCustomFreeFn>,
51+
}
52+
5353
impl AllocationProfilingStats {
5454
fn new() -> AllocationProfilingStats {
5555
// Safety: this will only error if lambda <= 0
@@ -91,6 +91,18 @@ impl AllocationProfilingStats {
9191

9292
thread_local! {
9393
static ALLOCATION_PROFILING_STATS: RefCell<AllocationProfilingStats> = RefCell::new(AllocationProfilingStats::new());
94+
/// Using an `UnsafeCell` here should be okay. There might not be any synchronisation issues,
95+
/// as it is used in as thread local and only mutated in RINIT and RSHUTDOWN.
96+
static ZEND_MM_STATE: UnsafeCell<ZendMMState> = const {
97+
UnsafeCell::new(
98+
ZendMMState{
99+
heap: None,
100+
prev_custom_mm_alloc: None,
101+
prev_custom_mm_realloc: None,
102+
prev_custom_mm_free: None
103+
}
104+
)
105+
};
94106
}
95107

96108
const NEEDS_RUN_TIME_CHECK_FOR_ENABLED_JIT: bool =
@@ -106,9 +118,6 @@ lazy_static! {
106118
}
107119

108120
pub fn allocation_profiling_minit() {
109-
#[cfg(php_zts)]
110-
return;
111-
112121
unsafe { zend::ddog_php_opcache_init_handle() };
113122
}
114123

@@ -125,9 +134,6 @@ pub fn first_rinit_should_disable_due_to_jit() -> bool {
125134
}
126135

127136
pub fn allocation_profiling_rinit() {
128-
#[cfg(php_zts)]
129-
return;
130-
131137
let allocation_profiling: bool = REQUEST_LOCALS.with(|cell| {
132138
match cell.try_borrow() {
133139
Ok(locals) => {
@@ -145,47 +151,50 @@ pub fn allocation_profiling_rinit() {
145151
return;
146152
}
147153

148-
unsafe {
149-
HEAP = Some(zend::zend_mm_get_heap());
150-
}
151-
152-
if !is_zend_mm() {
153-
// Neighboring custom memory handlers found
154-
debug!("Found another extension using the ZendMM custom handler hook");
154+
ZEND_MM_STATE.with(|cell| {
155+
let zend_mm_state = cell.get();
155156
unsafe {
156-
zend::zend_mm_get_custom_handlers(
157-
// Safety: `unwrap()` is safe here, as `HEAP` is initialized just above
158-
HEAP.unwrap(),
159-
&mut PREV_CUSTOM_MM_ALLOC,
160-
&mut PREV_CUSTOM_MM_FREE,
161-
&mut PREV_CUSTOM_MM_REALLOC,
162-
);
163-
ALLOCATION_PROFILING_ALLOC = allocation_profiling_prev_alloc;
164-
ALLOCATION_PROFILING_FREE = allocation_profiling_prev_free;
165-
ALLOCATION_PROFILING_REALLOC = allocation_profiling_prev_realloc;
166-
ALLOCATION_PROFILING_PREPARE_ZEND_HEAP = prepare_zend_heap_none;
167-
ALLOCATION_PROFILING_RESTORE_ZEND_HEAP = restore_zend_heap_none;
157+
(*zend_mm_state).heap = Some(zend::zend_mm_get_heap());
168158
}
169-
} else {
170-
unsafe {
171-
ALLOCATION_PROFILING_ALLOC = allocation_profiling_orig_alloc;
172-
ALLOCATION_PROFILING_FREE = allocation_profiling_orig_free;
173-
ALLOCATION_PROFILING_REALLOC = allocation_profiling_orig_realloc;
174-
ALLOCATION_PROFILING_PREPARE_ZEND_HEAP = prepare_zend_heap;
175-
ALLOCATION_PROFILING_RESTORE_ZEND_HEAP = restore_zend_heap;
159+
160+
if !is_zend_mm() {
161+
// Neighboring custom memory handlers found
162+
debug!("Found another extension using the ZendMM custom handler hook");
163+
unsafe {
164+
zend::zend_mm_get_custom_handlers(
165+
// Safety: `unwrap()` is safe here, as `heap` is initialized just above
166+
(*zend_mm_state).heap.unwrap(),
167+
&mut (*zend_mm_state).prev_custom_mm_alloc,
168+
&mut (*zend_mm_state).prev_custom_mm_free,
169+
&mut (*zend_mm_state).prev_custom_mm_realloc,
170+
);
171+
ALLOCATION_PROFILING_ALLOC = allocation_profiling_prev_alloc;
172+
ALLOCATION_PROFILING_FREE = allocation_profiling_prev_free;
173+
ALLOCATION_PROFILING_REALLOC = allocation_profiling_prev_realloc;
174+
ALLOCATION_PROFILING_PREPARE_ZEND_HEAP = prepare_zend_heap_none;
175+
ALLOCATION_PROFILING_RESTORE_ZEND_HEAP = restore_zend_heap_none;
176+
}
177+
} else {
178+
unsafe {
179+
ALLOCATION_PROFILING_ALLOC = allocation_profiling_orig_alloc;
180+
ALLOCATION_PROFILING_FREE = allocation_profiling_orig_free;
181+
ALLOCATION_PROFILING_REALLOC = allocation_profiling_orig_realloc;
182+
ALLOCATION_PROFILING_PREPARE_ZEND_HEAP = prepare_zend_heap;
183+
ALLOCATION_PROFILING_RESTORE_ZEND_HEAP = restore_zend_heap;
184+
}
176185
}
177-
}
178186

179-
// install our custom handler to ZendMM
180-
unsafe {
181-
zend::ddog_php_prof_zend_mm_set_custom_handlers(
182-
// Safety: `unwrap()` is safe here, as `HEAP` is initialized just above
183-
HEAP.unwrap(),
184-
Some(alloc_profiling_malloc),
185-
Some(alloc_profiling_free),
186-
Some(alloc_profiling_realloc),
187-
);
188-
}
187+
// install our custom handler to ZendMM
188+
unsafe {
189+
zend::ddog_php_prof_zend_mm_set_custom_handlers(
190+
// Safety: `unwrap()` is safe here, as `heap` is initialized just above
191+
(*zend_mm_state).heap.unwrap(),
192+
Some(alloc_profiling_malloc),
193+
Some(alloc_profiling_free),
194+
Some(alloc_profiling_realloc),
195+
);
196+
}
197+
});
189198

190199
// `is_zend_mm()` should be `false` now, as we installed our custom handlers
191200
if is_zend_mm() {
@@ -198,9 +207,6 @@ pub fn allocation_profiling_rinit() {
198207
}
199208

200209
pub fn allocation_profiling_rshutdown() {
201-
#[cfg(php_zts)]
202-
return;
203-
204210
let allocation_profiling = REQUEST_LOCALS.with(|cell| {
205211
cell.try_borrow()
206212
.map(|locals| locals.system_settings().profiling_allocation_enabled)
@@ -219,57 +225,57 @@ pub fn allocation_profiling_rshutdown() {
219225
return;
220226
}
221227

222-
let mut custom_mm_malloc: Option<zend::VmMmCustomAllocFn> = None;
223-
let mut custom_mm_free: Option<zend::VmMmCustomFreeFn> = None;
224-
let mut custom_mm_realloc: Option<zend::VmMmCustomReallocFn> = None;
225-
unsafe {
226-
zend::zend_mm_get_custom_handlers(
227-
// Safety: `unwrap()` is safe here, as `HEAP` is initialized in `RINIT`
228-
HEAP.unwrap(),
229-
&mut custom_mm_malloc,
230-
&mut custom_mm_free,
231-
&mut custom_mm_realloc,
232-
);
233-
}
234-
if custom_mm_free != Some(alloc_profiling_free)
235-
|| custom_mm_malloc != Some(alloc_profiling_malloc)
236-
|| custom_mm_realloc != Some(alloc_profiling_realloc)
237-
{
238-
// Custom handlers are installed, but it's not us. Someone, somewhere might have
239-
// function pointers to our custom handlers. Best bet to avoid segfaults is to not
240-
// touch custom handlers in ZendMM and make sure our extension will not be
241-
// `dlclose()`-ed so the pointers stay valid
242-
let zend_extension =
243-
unsafe { zend::zend_get_extension(PROFILER_NAME.as_ptr() as *const c_char) };
244-
if !zend_extension.is_null() {
245-
// Safety: Checked for null pointer above.
228+
ZEND_MM_STATE.with(|cell| {
229+
let zend_mm_state = cell.get();
230+
let mut custom_mm_malloc: Option<zend::VmMmCustomAllocFn> = None;
231+
let mut custom_mm_free: Option<zend::VmMmCustomFreeFn> = None;
232+
let mut custom_mm_realloc: Option<zend::VmMmCustomReallocFn> = None;
233+
unsafe {
234+
zend::zend_mm_get_custom_handlers(
235+
// Safety: `unwrap()` is safe here, as `heap` is initialized in `RINIT`
236+
(*zend_mm_state).heap.unwrap(),
237+
&mut custom_mm_malloc,
238+
&mut custom_mm_free,
239+
&mut custom_mm_realloc,
240+
);
241+
}
242+
if custom_mm_free != Some(alloc_profiling_free)
243+
|| custom_mm_malloc != Some(alloc_profiling_malloc)
244+
|| custom_mm_realloc != Some(alloc_profiling_realloc)
245+
{
246+
// Custom handlers are installed, but it's not us. Someone, somewhere might have
247+
// function pointers to our custom handlers. Best bet to avoid segfaults is to not
248+
// touch custom handlers in ZendMM and make sure our extension will not be
249+
// `dlclose()`-ed so the pointers stay valid
250+
let zend_extension =
251+
unsafe { zend::zend_get_extension(PROFILER_NAME.as_ptr() as *const c_char) };
252+
if !zend_extension.is_null() {
253+
// Safety: Checked for null pointer above.
254+
unsafe {
255+
(*zend_extension).handle = std::ptr::null_mut();
256+
}
257+
}
258+
warn!("Found another extension using the custom heap which is unexpected at this point, so the extension handle was `null`'ed to avoid being `dlclose()`'ed.");
259+
} else {
260+
// This is the happy path (restore previously installed custom handlers)!
246261
unsafe {
247-
(*zend_extension).handle = std::ptr::null_mut();
262+
zend::ddog_php_prof_zend_mm_set_custom_handlers(
263+
// Safety: `unwrap()` is safe here, as `heap` is initialized in `RINIT`
264+
(*zend_mm_state).heap.unwrap(),
265+
(*zend_mm_state).prev_custom_mm_alloc,
266+
(*zend_mm_state).prev_custom_mm_free,
267+
(*zend_mm_state).prev_custom_mm_realloc,
268+
);
248269
}
270+
trace!("Memory allocation profiling shutdown gracefully.");
249271
}
250-
warn!("Found another extension using the custom heap which is unexpected at this point, so the extension handle was `null`'ed to avoid being `dlclose()`'ed.");
251-
} else {
252-
// This is the happy path (restore previously installed custom handlers)!
253272
unsafe {
254-
zend::ddog_php_prof_zend_mm_set_custom_handlers(
255-
// Safety: `unwrap()` is safe here, as `HEAP` is initialized in `RINIT`
256-
HEAP.unwrap(),
257-
PREV_CUSTOM_MM_ALLOC,
258-
PREV_CUSTOM_MM_FREE,
259-
PREV_CUSTOM_MM_REALLOC,
260-
);
273+
(*zend_mm_state).heap = None;
261274
}
262-
trace!("Memory allocation profiling shutdown gracefully.");
263-
}
264-
unsafe {
265-
HEAP = None;
266-
}
275+
});
267276
}
268277

269278
pub fn allocation_profiling_startup() {
270-
#[cfg(php_zts)]
271-
return;
272-
273279
unsafe {
274280
let handle = datadog_php_zif_handler::new(
275281
CStr::from_bytes_with_nul_unchecked(b"gc_mem_caches\0"),
@@ -366,8 +372,11 @@ static mut ALLOCATION_PROFILING_ALLOC: unsafe fn(size_t) -> *mut c_void =
366372
allocation_profiling_orig_alloc;
367373

368374
unsafe fn allocation_profiling_prev_alloc(len: size_t) -> *mut c_void {
369-
let prev = PREV_CUSTOM_MM_ALLOC.unwrap();
370-
prev(len)
375+
ZEND_MM_STATE.with(|cell| {
376+
let zend_mm_state = cell.get();
377+
let prev = (*zend_mm_state).prev_custom_mm_alloc.unwrap();
378+
prev(len)
379+
})
371380
}
372381

373382
unsafe fn allocation_profiling_orig_alloc(len: size_t) -> *mut c_void {
@@ -389,8 +398,11 @@ unsafe extern "C" fn alloc_profiling_free(ptr: *mut c_void) {
389398
static mut ALLOCATION_PROFILING_FREE: unsafe fn(*mut c_void) = allocation_profiling_orig_free;
390399

391400
unsafe fn allocation_profiling_prev_free(ptr: *mut c_void) {
392-
let prev = PREV_CUSTOM_MM_FREE.unwrap();
393-
prev(ptr);
401+
ZEND_MM_STATE.with(|cell| {
402+
let zend_mm_state = cell.get();
403+
let prev = (*zend_mm_state).prev_custom_mm_free.unwrap();
404+
prev(ptr)
405+
})
394406
}
395407

396408
unsafe fn allocation_profiling_orig_free(ptr: *mut c_void) {
@@ -422,8 +434,11 @@ static mut ALLOCATION_PROFILING_REALLOC: unsafe fn(*mut c_void, size_t) -> *mut
422434
allocation_profiling_orig_realloc;
423435

424436
unsafe fn allocation_profiling_prev_realloc(prev_ptr: *mut c_void, len: size_t) -> *mut c_void {
425-
let prev = PREV_CUSTOM_MM_REALLOC.unwrap();
426-
prev(prev_ptr, len)
437+
ZEND_MM_STATE.with(|cell| {
438+
let zend_mm_state = cell.get();
439+
let prev = (*zend_mm_state).prev_custom_mm_realloc.unwrap();
440+
prev(prev_ptr, len)
441+
})
427442
}
428443

429444
unsafe fn allocation_profiling_orig_realloc(prev_ptr: *mut c_void, len: size_t) -> *mut c_void {

profiling/tests/phpt/jit_01.phpt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@ if (!extension_loaded('datadog-profiling'))
1515
$arch = php_uname('m');
1616
if (PHP_VERSION_ID < 80100 && in_array($arch, ['aarch64', 'arm64']))
1717
echo "skip: JIT not available on aarch64 on PHP 8.0", PHP_EOL;
18-
19-
// TODO: remove once ZTS support for allocation profiling is done
20-
if (PHP_ZTS) {
21-
echo "skip: not support on ZTS builds at the moment";
22-
}
2318
?>
2419
--INI--
2520
datadog.profiling.enabled=yes

profiling/tests/phpt/jit_02.phpt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,6 @@ if (!extension_loaded('datadog-profiling'))
1414
$arch = php_uname('m');
1515
if (PHP_VERSION_ID < 80100 && in_array($arch, ['aarch64', 'arm64']))
1616
echo "skip: JIT not available on aarch64 on PHP 8.0", PHP_EOL;
17-
18-
// TODO: remove once ZTS support for allocation profiling is done
19-
if (PHP_ZTS) {
20-
echo "skip: not support on ZTS builds at the moment";
21-
}
22-
2317
?>
2418
--INI--
2519
datadog.profiling.enabled=yes

profiling/tests/phpt/phpinfo_02.phpt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ if (!extension_loaded('datadog-profiling'))
1414
$arch = php_uname('m');
1515
if (PHP_VERSION_ID < 80100 && in_array($arch, ['aarch64', 'arm64']))
1616
echo "skip: JIT not available on aarch64 on PHP 8.0", PHP_EOL;
17-
18-
// TODO: remove once ZTS support for allocation profiling is done
19-
if (PHP_ZTS) {
20-
echo "skip: not support on ZTS builds at the moment";
21-
}
2217
?>
2318
--ENV--
2419
DD_PROFILING_ENABLED=yes

0 commit comments

Comments
 (0)