Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions profiling/src/allocation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use core::cell::Cell;
use core::ptr;
use libc::size_t;
use log::{debug, trace};
use rand_distr::{Distribution, Poisson};
use rand::Rng;
use std::ffi::c_void;
use std::num::{NonZero, NonZeroU32, NonZeroU64};
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
Expand Down Expand Up @@ -150,7 +150,7 @@ unsafe extern "C" fn _zend_mm_realloc(
/// Default sampling interval in bytes (4 MiB).
pub const DEFAULT_ALLOCATION_SAMPLING_INTERVAL: NonZeroU32 = NonZero::new(1024 * 4096).unwrap();

/// Sampling distance feed into poison sampling algo. This must be > 0.
/// Mean distance between allocation samples in bytes. This must be > 0.
pub static ALLOCATION_PROFILING_INTERVAL: AtomicU64 =
AtomicU64::new(DEFAULT_ALLOCATION_SAMPLING_INTERVAL.get() as u64);

Expand All @@ -169,7 +169,7 @@ pub static ALLOCATION_PROFILING_SIZE: AtomicU64 = AtomicU64::new(0);
pub struct AllocationProfilingStats {
/// Number of bytes remaining until the next sample collection.
next_sample: i64,
poisson: Poisson<f64>,
mean: f64,
#[cfg(php_zts)]
rng: ThreadRng,
#[cfg(not(php_zts))]
Expand All @@ -178,11 +178,9 @@ pub struct AllocationProfilingStats {

impl AllocationProfilingStats {
fn new(sampling_distance: NonZeroU64) -> AllocationProfilingStats {
// SAFETY: this will only error if lambda <= 0, and it's NonZeroU64.
let poisson = unsafe { Poisson::new(sampling_distance.get() as f64).unwrap_unchecked() };
let mut stats = AllocationProfilingStats {
next_sample: 0,
poisson,
mean: sampling_distance.get() as f64,
#[cfg(php_zts)]
rng: rand::rng(),
#[cfg(not(php_zts))]
Expand All @@ -193,7 +191,12 @@ impl AllocationProfilingStats {
}

fn next_sampling_interval(&mut self) {
self.next_sample = self.poisson.sample(&mut self.rng) as i64;
// Exponential distances give the upscaler's probability: 1 - exp(-size / mean).
let u: f64 = self.rng.random();
let u = if u <= 0.0 { 1e-10 } else { u };
let v = -u.ln() * self.mean;
// Clamp to [8, 20 * mean], matching the libdatadog sampler.
self.next_sample = v.clamp(8.0, 20.0 * self.mean) as i64;
}

fn should_collect_allocation(&mut self, len: size_t) -> bool {
Expand Down Expand Up @@ -342,6 +345,34 @@ pub fn alloc_prof_rshutdown() {
allocation_ge84::alloc_prof_rshutdown(heap_live_enabled);
}

#[cfg(all(test, not(php_zts)))]
mod tests {
use super::*;

#[test]
fn allocation_sampling_matches_upscaling_probability() {
let mean = DEFAULT_ALLOCATION_SAMPLING_INTERVAL.get() as f64;
let trials = 100_000;
for ratio in [0.1, 1.1, 3.0] {
let size = (ratio * mean) as usize;
let mut stats =
AllocationProfilingStats::new(DEFAULT_ALLOCATION_SAMPLING_INTERVAL.into());
stats.rng = StdRng::seed_from_u64(42);
stats.next_sampling_interval();
let sampled = (0..trials)
.filter(|_| stats.should_collect_allocation(size))
.count();
let probability = 1.0 - (-(size as f64) / mean).exp();
let expected = trials as f64 * probability;
let sigma = (expected * (1.0 - probability)).sqrt();
assert!(
(sampled as f64 - expected).abs() < 8.0 * sigma,
"size={size}: sampled {sampled}, expected {expected}"
);
}
}
}

#[cfg(php_zend_mm_set_custom_handlers_ex)]
#[track_caller]
fn initialization_panic() -> ! {
Expand Down
9 changes: 5 additions & 4 deletions profiling/tests/correctness/allocation_time_combined.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
{
"scale_by_duration": true,
"test_name": "php_allocation_time_combined",
"note": "Each iteration allocates three equal 10 MB strings: two in str_replace and one in str_repeat. With at least 128 iterations and p = 1 - exp(-10000000 / 4194304) = 0.9078, the binomial model gives an approximate share standard deviation of at most 0.77 percentage points. The 6-point margins allow over eight standard deviations, including the analyzer's integer truncation of the exact 2/3 and 1/3 shares.",
"stacks": [
{
"profile-type": "alloc-size",
"stack-content": [
{
"regular_expression": "<?php;main;standard\\|str_replace$",
"percent": 66,
"error_margin": 1
"error_margin": 6
},
{
"regular_expression": "<?php;main;standard\\|str_repeat$",
"percent": 33,
"error_margin": 1
"error_margin": 6
}
]
},
Expand All @@ -23,12 +24,12 @@
{
"regular_expression": "<?php;main;standard\\|str_replace$",
"percent": 66,
"error_margin": 3
"error_margin": 6
},
{
"regular_expression": "<?php;main;standard\\|str_repeat$",
"percent": 33,
"error_margin": 3
"error_margin": 6
}
]
},
Expand Down
3 changes: 2 additions & 1 deletion profiling/tests/correctness/allocation_time_combined.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ function main() {
$duration = $_ENV["EXECUTION_TIME"] ?? 10;
$end = microtime(true) + $duration;

while (microtime(true) < $end) {
// Keep enough allocations for stable shares, even on slower CI workers.
for ($i = 0; $i < 128 || microtime(true) < $end; $i++) {
// str_replace is frameless in PHP 8.4+ and allocates a new string
$xs = str_repeat("x", 10_000_000); // 10MB source
$ys = str_replace("x", "y", $xs); // 10MB allocation in frameless function
Expand Down
23 changes: 14 additions & 9 deletions profiling/tests/correctness/allocations.json
Original file line number Diff line number Diff line change
@@ -1,54 +1,59 @@
{
"scale_by_duration": true,
"scale_by_duration": false,
"test_name": "php_allocations",
"note": "512 iterations allocate 18874368000 payload bytes in 2048 strings; string headers add less than 0.001%. At the 4 MiB interval, p = 1 - exp(-size / interval) is 0.9466 or 0.7689. Binomial sampling gives relative standard deviations of 0.76% for total bytes and 0.93% for total count; the 6% margins exceed six standard deviations. Approximate stack-share standard deviations are at most 0.36 percentage points for bytes and 0.49 for count; the 3-point margins exceed six standard deviations. The analyzer truncates shares to integer percentages, hence 33/16 for the exact 1/3 and 1/6 byte shares.",
"stacks": [
{
"profile-type": "alloc-size",
"value-matching-sum": 18874368000,
"error-margin": 6,
"stack-content": [
{
"regular_expression": "<?php;main;a;standard\\|str_repeat$",
"percent": 33,
"error_margin": 5
"error_margin": 3
},
{
"regular_expression": "<?php;main;a;standard\\|str_replace$",
"percent": 33,
"error_margin": 5
"error_margin": 3
},
{
"regular_expression": "<?php;main;b;standard\\|str_repeat$",
"percent": 16,
"error_margin": 5
"error_margin": 3
},
{
"regular_expression": "<?php;main;b;standard\\|str_replace$",
"percent": 16,
"error_margin": 5
"error_margin": 3
}
]
},
{
"profile-type": "alloc-samples",
"value-matching-sum": 2048,
"error-margin": 6,
"stack-content": [
{
"regular_expression": "<?php;main;a;standard\\|str_repeat$",
"percent": 25,
"error_margin": 5
"error_margin": 3
},
{
"regular_expression": "<?php;main;a;standard\\|str_replace$",
"percent": 25,
"error_margin": 5
"error_margin": 3
},
{
"regular_expression": "<?php;main;b;standard\\|str_repeat$",
"percent": 25,
"error_margin": 5
"error_margin": 3
},
{
"regular_expression": "<?php;main;b;standard\\|str_replace$",
"percent": 25,
"error_margin": 5
"error_margin": 3
}
]
}
Expand Down
20 changes: 7 additions & 13 deletions profiling/tests/correctness/allocations.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,24 @@
function a()
{
$a = str_repeat("a", 1024 * 12_000);
str_replace('a', 'b', $a);
// One replacement still allocates a full copy, without per-byte work.
$a[0] = 'b';
str_replace('b', 'c', $a);
}

function b()
{
$a = str_repeat("a", 1024 * 6_000);
str_replace('a', 'b', $a);
$a[0] = 'b';
str_replace('b', 'c', $a);
}

function main()
{
$duration = $_ENV["EXECUTION_TIME"] ?? 10;
$end = microtime(true) + $duration;
while (microtime(true) < $end) {
$start = microtime(true);
// Fixed work makes allocation totals independent of machine speed.
for ($i = 0; $i < 512; $i++) {
a();
b();
$elapsed = microtime(true) - $start;
// sleep for the remainder to 100 ms
// so we end up doing 10 iterations per second
$sleep = (0.1 - $elapsed);
if ($sleep > 0.0) {
usleep((int) ($sleep * 1_000_000));
}
}
}
main();
Loading