Skip to content

Commit b13f0d3

Browse files
bench(profiling): measure live heap tracking paths
1 parent 17a0855 commit b13f0d3

6 files changed

Lines changed: 246 additions & 56 deletions

File tree

profiling/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ perfcnt = "0.8.0"
6161
name = "stack_walking"
6262
harness = false
6363

64+
[[bench]]
65+
name = "heap_live_tracking"
66+
harness = false
67+
6468
[features]
6569
default = ["io_profiling"]
6670
debug_stats = []
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use core::cell::{Cell, UnsafeCell};
2+
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
3+
4+
// Compile the production tracker without linking the PHP extension executable.
5+
#[allow(dead_code)]
6+
#[path = "../src/profiling/live_heap.rs"]
7+
mod live_heap;
8+
use live_heap::{LiveHeapTracker, LocalLiveHeapTracker};
9+
10+
const BASE_ADDRESS: usize = 0x1000_0000;
11+
const TRACKED_ALLOCATIONS: usize = 2048;
12+
const ADDRESS_STRIDE: usize = 64;
13+
14+
fn address(index: usize) -> usize {
15+
BASE_ADDRESS + index % TRACKED_ALLOCATIONS * ADDRESS_STRIDE
16+
}
17+
18+
fn untracked_address(index: usize) -> usize {
19+
address(index) + 8
20+
}
21+
22+
struct Tracker {
23+
shared: LiveHeapTracker<[usize; 4]>,
24+
local: UnsafeCell<LocalLiveHeapTracker>,
25+
}
26+
27+
impl Tracker {
28+
fn new() -> Self {
29+
Self {
30+
shared: LiveHeapTracker::new(),
31+
local: UnsafeCell::new(LocalLiveHeapTracker::new()),
32+
}
33+
}
34+
35+
fn track(&self, ptr: usize, sample: [usize; 4]) -> bool {
36+
// Criterion invokes setup and measured routines sequentially.
37+
unsafe { (&mut *self.local.get()).track(&self.shared, ptr, sample) }
38+
}
39+
40+
fn untrack(&self, ptr: usize) -> Option<[usize; 4]> {
41+
// SAFETY: same sequential access as `track`.
42+
unsafe { (&mut *self.local.get()).untrack(&self.shared, ptr) }
43+
}
44+
}
45+
46+
fn populated_tracker() -> Tracker {
47+
let tracker = Tracker::new();
48+
for index in 0..TRACKED_ALLOCATIONS {
49+
assert!(tracker.track(address(index), [0; 4]));
50+
}
51+
tracker
52+
}
53+
54+
fn benchmark(c: &mut Criterion) {
55+
let mut group = c.benchmark_group("heap_live_tracking");
56+
57+
{
58+
let tracker = populated_tracker();
59+
let next = Cell::new(0);
60+
group.bench_function("allocate_tracked", |b| {
61+
b.iter_batched(
62+
|| {
63+
let index = next.get();
64+
next.set(index + 1);
65+
let ptr = untracked_address(index);
66+
let _ = tracker.untrack(ptr);
67+
(ptr, [0; 4])
68+
},
69+
|(ptr, sample)| black_box(tracker.track(ptr, sample)),
70+
BatchSize::PerIteration,
71+
)
72+
});
73+
}
74+
75+
{
76+
let tracker = populated_tracker();
77+
let next = Cell::new(0);
78+
group.bench_function("free_tracked", |b| {
79+
b.iter_batched(
80+
|| {
81+
let index = next.get();
82+
next.set(index + 1);
83+
let ptr = untracked_address(index);
84+
assert!(tracker.track(ptr, [0; 4]));
85+
ptr
86+
},
87+
|ptr| black_box(tracker.untrack(ptr)),
88+
BatchSize::PerIteration,
89+
)
90+
});
91+
}
92+
93+
{
94+
let tracker = populated_tracker();
95+
let next = Cell::new(0);
96+
group.bench_function("free_untracked", |b| {
97+
b.iter_batched(
98+
|| {
99+
let index = next.get();
100+
next.set(index + 1);
101+
let ptr = untracked_address(index);
102+
let _ = tracker.untrack(ptr);
103+
ptr
104+
},
105+
|ptr| black_box(tracker.untrack(ptr)),
106+
BatchSize::PerIteration,
107+
)
108+
});
109+
}
110+
111+
group.finish();
112+
}
113+
114+
criterion_group!(benches, benchmark);
115+
criterion_main!(benches);

profiling/src/allocation/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub use profiling_stats::*;
55
use crate::bindings::{self as zend};
66
use crate::config::SystemSettings;
77
use crate::module_globals;
8+
use crate::profiling::live_heap::LocalLiveHeapTracker;
89
use crate::profiling::Profiler;
910
use crate::{RefCellExt, REQUEST_LOCALS};
1011
use core::cell::Cell;
@@ -98,6 +99,7 @@ pub struct AllocationProfilingStats {
9899
rng: ThreadRng,
99100
#[cfg(not(php_zts))]
100101
rng: StdRng,
102+
live_heap: LocalLiveHeapTracker,
101103
}
102104

103105
impl AllocationProfilingStats {
@@ -111,6 +113,7 @@ impl AllocationProfilingStats {
111113
rng: rand::rng(),
112114
#[cfg(not(php_zts))]
113115
rng: StdRng::from_os_rng(),
116+
live_heap: LocalLiveHeapTracker::new(),
114117
};
115118
stats.next_sampling_interval();
116119
stats

profiling/src/allocation/profiling_stats.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! can be used but expose a relatively safe API.
55
66
use super::{AllocationProfilingStats, ALLOCATION_PROFILING_INTERVAL};
7+
use crate::profiling::live_heap::LiveHeapTracker;
78
use libc::size_t;
89
use std::mem::MaybeUninit;
910

@@ -101,6 +102,28 @@ pub fn allocation_profiling_stats_should_collect(len: size_t) -> bool {
101102
unsafe { allocation_profiling_stats_mut(f) }
102103
}
103104

105+
pub(crate) fn live_heap_track<T>(tracker: &LiveHeapTracker<T>, ptr: usize, sample: T) -> bool {
106+
// SAFETY: allocation stats are initialized before allocation hooks run,
107+
// and the closure does not retain or recursively borrow them.
108+
unsafe {
109+
allocation_profiling_stats_mut(|stats| {
110+
stats
111+
.assume_init_mut()
112+
.live_heap
113+
.track(tracker, ptr, sample)
114+
})
115+
}
116+
}
117+
118+
pub(crate) fn live_heap_untrack<T>(tracker: &LiveHeapTracker<T>, ptr: usize) -> Option<T> {
119+
// SAFETY: same lifecycle and borrowing guarantees as `live_heap_track`.
120+
unsafe {
121+
allocation_profiling_stats_mut(|stats| {
122+
stats.assume_init_mut().live_heap.untrack(tracker, ptr)
123+
})
124+
}
125+
}
126+
104127
/// Initializes the allocation profiler's globals.
105128
///
106129
/// # Safety
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use dashmap::DashMap;
2+
use rustc_hash::FxBuildHasher;
3+
use std::sync::atomic::{AtomicUsize, Ordering};
4+
5+
/// Maximum number of allocations to track for live heap profiling.
6+
const MAX_SIZE: usize = 4096;
7+
8+
/// Tracks live heap samples by allocation address. FxHasher spreads sequential
9+
/// ZendMM addresses across DashMap's shards without hashing already-randomized
10+
/// pointer bytes with SipHash.
11+
pub(crate) struct LiveHeapTracker<T> {
12+
allocations: DashMap<usize, T, FxBuildHasher>,
13+
count: AtomicUsize,
14+
}
15+
16+
impl<T> LiveHeapTracker<T> {
17+
pub(crate) fn new() -> Self {
18+
Self {
19+
allocations: DashMap::with_hasher(FxBuildHasher),
20+
count: AtomicUsize::new(0),
21+
}
22+
}
23+
24+
pub(crate) fn len(&self) -> usize {
25+
self.count.load(Ordering::Relaxed)
26+
}
27+
28+
pub(crate) fn clear(&self) {
29+
self.allocations.clear();
30+
self.count.store(0, Ordering::Relaxed);
31+
}
32+
33+
fn track(&self, ptr: usize, sample: T) -> bool {
34+
// Best-effort cap: in ZTS the count check and insert still race, so
35+
// the map can briefly exceed MAX_SIZE.
36+
if self.len() >= MAX_SIZE {
37+
return false;
38+
}
39+
40+
if self.allocations.insert(ptr, sample).is_none() {
41+
self.count.fetch_add(1, Ordering::Relaxed);
42+
}
43+
true
44+
}
45+
46+
fn untrack(&self, ptr: usize) -> Option<T> {
47+
let result = self.allocations.remove(&ptr).map(|(_, sample)| sample);
48+
if result.is_some() {
49+
self.count.fetch_sub(1, Ordering::Relaxed);
50+
}
51+
result
52+
}
53+
}
54+
55+
impl<T: Clone> LiveHeapTracker<T> {
56+
pub(crate) fn snapshot(&self) -> Vec<T> {
57+
self.allocations
58+
.iter()
59+
.map(|entry| entry.value().clone())
60+
.collect()
61+
}
62+
}
63+
64+
impl<T> Default for LiveHeapTracker<T> {
65+
fn default() -> Self {
66+
Self::new()
67+
}
68+
}
69+
70+
pub(crate) struct LocalLiveHeapTracker;
71+
72+
impl LocalLiveHeapTracker {
73+
pub(crate) const fn new() -> Self {
74+
Self
75+
}
76+
77+
pub(crate) fn track<T>(&mut self, tracker: &LiveHeapTracker<T>, ptr: usize, sample: T) -> bool {
78+
tracker.track(ptr, sample)
79+
}
80+
81+
pub(crate) fn untrack<T>(&mut self, tracker: &LiveHeapTracker<T>, ptr: usize) -> Option<T> {
82+
tracker.untrack(ptr)
83+
}
84+
}
85+
86+
impl Default for LocalLiveHeapTracker {
87+
fn default() -> Self {
88+
Self::new()
89+
}
90+
}

0 commit comments

Comments
 (0)