diff --git a/Cargo.lock b/Cargo.lock index 7ec39965652..c747a35e7a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -544,7 +544,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", ] [[package]] @@ -553,6 +562,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit_field" version = "0.10.2" @@ -1608,6 +1623,7 @@ dependencies = [ "log", "once_cell", "perfcnt", + "proptest", "rand 0.8.5", "rand_distr", "rustc-hash 1.1.0", @@ -3363,7 +3379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", - "bit-set", + "bit-set 0.5.3", "ena", "itertools 0.11.0", "lalrpop-util", @@ -4254,6 +4270,8 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", "bitflags 2.8.0", "lazy_static", "num-traits", @@ -4261,6 +4279,8 @@ dependencies = [ "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax 0.8.5", + "rusty-fork", + "tempfile", "unarray", ] @@ -4373,6 +4393,12 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc55d7dec32ecaf61e0bd90b3d2392d721a28b95cfd23c3e176eccefbeab2f2" +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quinn" version = "0.11.7" @@ -4916,6 +4942,18 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ruzstd" version = "0.3.1" @@ -6205,6 +6243,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc 0.2.173", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/profiling/Cargo.toml b/profiling/Cargo.toml index 788f3c2d4a8..698f7feab99 100644 --- a/profiling/Cargo.toml +++ b/profiling/Cargo.toml @@ -48,6 +48,7 @@ features = ["env-filter", "fmt", "smallvec", "std"] allocator-api2 = { version = "0.2", default-features = false, features = ["alloc"] } criterion = { version = "0.5.1" } datadog-php-profiling = { path = ".", features = ["test"] } +proptest = { version = "1" } [target.'cfg(target_arch = "x86_64")'.dev-dependencies] criterion-perf-events = "0.4.0" diff --git a/profiling/src/bitset.rs b/profiling/src/bitset.rs new file mode 100644 index 00000000000..ac7178016a2 --- /dev/null +++ b/profiling/src/bitset.rs @@ -0,0 +1,239 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause + +use std::hash::{Hash, Hasher}; + +/// Very simple bitset which doesn't allocate, and doesn't change after it has +/// been created. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(transparent)] +pub struct BitSet { + bits: u32, +} + +impl Hash for BitSet { + fn hash(&self, state: &mut H) { + let bits = self.bits; + bits.count_ones().hash(state); + bits.hash(state); + } +} + +impl BitSet { + pub const MAX: usize = u32::BITS as usize; + + /// Creates a new bitset from the provided number. + pub const fn new(bits: u32) -> Self { + Self { bits } + } + + #[inline] + pub fn len(&self) -> usize { + self.bits.count_ones() as usize + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.bits == 0 + } + + #[inline] + pub fn contains(&self, bit: usize) -> bool { + if bit < 32 { + let mask = 1u32 << bit; + let masked = self.bits & mask; + masked != 0 + } else { + false + } + } + + pub fn iter(&self) -> BitSetIter { + BitSetIter::new(self) + } +} + +impl FromIterator for BitSet { + /// Creates a new bitset from the iterator. + /// + /// # Panics + /// + /// Panics if an item is out of the range of the bitset e.g. [`u32::MAX`]. + fn from_iter>(iter: I) -> BitSet { + let mut bits = 0; + let mut insert = |bit| { + // todo: add non-panic API + assert!(bit < BitSet::MAX); + bits |= 1u32 << bit; + }; + for bit in iter { + insert(bit); + } + BitSet { bits } + } +} + +pub struct BitSetIter { + bitset: u32, + offset: u32, + end: u32, +} + +impl BitSetIter { + pub fn new(bitset: &BitSet) -> BitSetIter { + let bitset = bitset.bits; + let offset = 0; + let end = { + let num_bits = u32::BITS; + let leading_zeros = bitset.leading_zeros(); + num_bits - leading_zeros + }; + BitSetIter { + bitset, + offset, + end, + } + } +} + +impl Iterator for BitSetIter { + type Item = usize; + + fn next(&mut self) -> Option { + while self.offset != self.end { + let offset = self.offset; + self.offset += 1; + let mask = 1 << offset; + let masked = self.bitset & mask; + if masked != 0 { + return Some(offset as usize); + } + } + None + } +} + +impl ExactSizeIterator for BitSetIter { + fn len(&self) -> usize { + if self.offset < self.end { + let shifted = self.bitset >> self.offset; + shifted.count_ones() as usize + } else { + 0 + } + } +} + +impl IntoIterator for BitSet { + type Item = usize; + type IntoIter = BitSetIter; + fn into_iter(self) -> Self::IntoIter { + BitSetIter::new(&self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + use proptest::test_runner::{RngAlgorithm, TestRng}; + use std::collections::HashSet; + + #[test] + fn bitset_full() { + let bitset = BitSet::new(u32::MAX); + assert_eq!(bitset.len(), BitSet::MAX); + assert!(!bitset.is_empty()); + + for i in 0..BitSet::MAX { + assert!(bitset.contains(i)); + } + + for (offset, bit) in bitset.iter().enumerate() { + assert_eq!(bit, offset); + } + + let mut iter = bitset.iter(); + let mut len = BitSet::MAX; + assert_eq!(len, iter.len()); + + while let Some(_) = iter.next() { + len -= 1; + assert_eq!(len, iter.len()); + } + } + + #[test] + fn bitset_empty() { + let bitset = BitSet::new(0); + assert_eq!(0, bitset.len()); + assert!(bitset.is_empty()); + for i in 0..BitSet::MAX { + assert!(!bitset.contains(i)); + } + + let mut iter = bitset.iter(); + let len = 0; + assert_eq!(len, iter.len()); + assert_eq!(None, iter.next()); + } + + // There's nothing special about 27, just testing a single possible number. + #[test] + fn bitset_27() { + let bitset = BitSet::new(1 << 27); + assert_eq!(1, bitset.len()); + assert!(!bitset.is_empty()); + assert!(bitset.contains(27)); + + let mut iter = bitset.iter(); + let len = 1; + assert_eq!(len, iter.len()); + assert_eq!(Some(27), iter.next()); + } + + static IOTA: [usize; BitSet::MAX] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, + ]; + + proptest! { + #[test] + fn bitset_acts_like_a_special_hashset( + oracle in proptest::sample::subsequence(&IOTA, 1..IOTA.len()) + .prop_map(HashSet::::from_iter), + ) { + let bitset1 = BitSet::from_iter(oracle.iter().cloned()); + prop_assert_eq!(bitset1.len(), oracle.len()); + + // Items in the oracle exist in the bitset. + for item in oracle.iter() { + prop_assert!(bitset1.contains(*item)); + } + + // Test the other way around to check the iterator implementation. + let mut i = 0; + for item in bitset1.iter() { + prop_assert!(oracle.contains(&item)); + i += 1; + } + // Make sure the iterator ran as many times as we expected. + prop_assert_eq!(i, oracle.len(), + "BitSet's iterator didn't have the expected number of iterations" + ); + + // Like regular sets, insertion order doesn't matter in bitsets. + let mut shuffled = oracle.iter().copied().collect::>(); + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + use rand::seq::SliceRandom; + shuffled.shuffle(&mut rng); + let bitset2 = BitSet::from_iter(shuffled.iter().cloned()); + + prop_assert_eq!( + bitset1, bitset2, + "Insertion order unexpectedly mattered, diff in binary: {:b} vs {:b}", + bitset1.bits, bitset2.bits + ); + } + } +} diff --git a/profiling/src/inlinevec.rs b/profiling/src/inlinevec.rs new file mode 100644 index 00000000000..380645dab16 --- /dev/null +++ b/profiling/src/inlinevec.rs @@ -0,0 +1,255 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause + +use core::slice::Iter; +use std::hash::{Hash, Hasher}; +use std::mem::{ManuallyDrop, MaybeUninit}; +use std::ops::{Deref, Range}; + +pub struct InlineVec { + // This is intended to be on the stack, so large lengths don't make sense. + // Picking u8 also ensures no wasted bytes if you use small types for T. + len: u8, + values: [MaybeUninit; N], +} + +const fn assert_capacity(n: usize) { + if n > u8::MAX as usize { + panic!("InlineVec only supports up to u8::MAX capacities"); + } +} + +impl Default for InlineVec { + fn default() -> Self { + InlineVec::new() + } +} + +impl InlineVec { + pub const fn new() -> Self { + assert_capacity(N); + Self { + len: 0, + // SAFETY: a MaybeUninit<[MaybeUninit; N]> is the same as an + // [MaybeUninit; N] when len=0. + values: unsafe { MaybeUninit::uninit().assume_init() }, + } + } + + /// # Safety + /// There must be unused capacity when called. + const unsafe fn push_unchecked(&mut self, value: T) { + self.as_mut_ptr().add(self.len()).write(value); + self.len += 1; + } + + pub const fn from(values: [T; M]) -> Self { + if N > u8::MAX as usize { + panic!("InlineVec only supports up to u8::MAX capacities"); + } + if M > N { + panic!("InlineVec::new requires an array no larger than the underlying capacity"); + } + + // Steal the guts out of the array. + let mut vec = Self::new(); + let src = values.as_ptr(); + let dst = vec.values.as_mut_ptr().cast(); + // SAFETY: can't be overlapping, we've stack-allocated dst. + unsafe { core::ptr::copy_nonoverlapping(src, dst, M) }; + core::mem::forget(values); + vec.len = M as u8; + vec + } + + pub const fn as_slice(&self) -> &[T] { + // SAFETY: the first N items are initialized. + unsafe { core::slice::from_raw_parts(self.as_ptr(), self.len()) } + } + + // Not const yet, see Rust issue 137737. + pub fn iter(&self) -> Iter<'_, T> { + self.as_slice().iter() + } + + pub const fn is_empty(&self) -> bool { + self.len == 0 + } + + pub const fn len(&self) -> usize { + self.len as usize + } + + pub const fn as_ptr(&self) -> *const T { + self.values.as_ptr().cast() + } + + pub const fn as_mut_ptr(&mut self) -> *mut T { + self.values.as_mut_ptr().cast() + } + + pub const fn try_push(&mut self, value: T) -> Result<(), T> { + if self.len as usize != N { + // SAFETY: we've ensured there is unused capacity first. + unsafe { self.push_unchecked(value) }; + Ok(()) + } else { + Err(value) + } + } +} + +impl Deref for InlineVec { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl core::fmt::Debug for InlineVec { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.deref().fmt(f) + } +} + +impl Clone for InlineVec { + fn clone(&self) -> Self { + let mut cloned = Self::new(); + cloned.clone_from(self); + cloned + } + + fn clone_from(&mut self, source: &Self) { + if core::mem::needs_drop::() { + let base = self.as_mut_ptr(); + for i in 0..self.len() { + // SAFETY: have exclusive access from &mut self, and we're + // doing pointer math within bounds. + unsafe { base.add(i).drop_in_place() } + } + } + self.len = 0; + for item in source.iter() { + // SAFETY: N=N, so there's room, and we've cleared the vec already. + unsafe { self.push_unchecked(item.clone()) } + } + } +} + +impl Copy for InlineVec {} + +pub struct InlineVecIter { + start: usize, + vec: ManuallyDrop>, +} + +impl InlineVecIter { + fn live_range(&self) -> Range { + self.start..self.vec.len() + } +} + +impl Drop for InlineVecIter { + fn drop(&mut self) { + if core::mem::needs_drop::() { + let base = self.vec.as_mut_ptr(); + for i in self.live_range() { + unsafe { base.add(i).drop_in_place() } + } + } + } +} + +impl From> for InlineVecIter { + fn from(vec: InlineVec) -> Self { + Self { + start: 0, + vec: ManuallyDrop::new(vec), + } + } +} + +impl Iterator for InlineVecIter { + type Item = T; + + fn next(&mut self) -> Option { + let live_range = self.live_range(); + if !live_range.is_empty() { + let item = unsafe { self.vec.as_mut_ptr().add(live_range.start).read() }; + self.start += 1; + Some(item) + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.live_range().len(); + (len, Some(len)) + } + + fn count(self) -> usize { + self.live_range().len() + } +} + +impl ExactSizeIterator for InlineVecIter { + fn len(&self) -> usize { + self.live_range().len() + } +} + +impl IntoIterator for InlineVec { + type Item = T; + type IntoIter = InlineVecIter; + + fn into_iter(self) -> Self::IntoIter { + InlineVecIter::from(self) + } +} + +// +// impl From> for ArrayVec { +// fn from(vec: InlineVec) -> Self { +// ArrayVec::from_iter(InlineVecIter::from(vec)) +// } +// } + +unsafe impl Send for InlineVec {} +unsafe impl Sync for InlineVec {} + +impl Hash for InlineVec { + fn hash(&self, state: &mut H) { + self.deref().hash(state) + } +} + +impl PartialEq for InlineVec { + fn eq(&self, other: &Self) -> bool { + self.deref().eq(other.deref()) + } +} + +impl Eq for InlineVec {} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST_INLINE_VEC: InlineVec = { + let mut vec = InlineVec::from([false]); + if vec.try_push(true).is_err() { + panic!("expected to be able push another item into the vec") + } + vec + }; + + #[test] + fn test_inlinevec_const() { + assert_eq!(TEST_INLINE_VEC.as_slice(), &[false, true]); + + let vec: Vec<_> = TEST_INLINE_VEC.iter().copied().collect(); + assert_eq!(vec.as_slice(), &[false, true]); + } +} diff --git a/profiling/src/lib.rs b/profiling/src/lib.rs index eb2f30820cf..3efe728dbab 100644 --- a/profiling/src/lib.rs +++ b/profiling/src/lib.rs @@ -1,12 +1,16 @@ pub mod bindings; +pub mod bitset; pub mod capi; +pub mod inlinevec; +pub mod profiling; + mod clocks; mod config; mod logging; -pub mod profiling; mod pthread; mod sapi; mod thin_str; +mod vec_ext; mod wall_time; #[cfg(php_run_time_cache)] @@ -23,7 +27,6 @@ mod exception; #[cfg(feature = "timeline")] mod timeline; -mod vec_ext; use crate::config::{SystemSettings, INITIAL_SYSTEM_SETTINGS}; use crate::zend::datadog_sapi_globals_request_info; diff --git a/profiling/src/profiling/mod.rs b/profiling/src/profiling/mod.rs index 39f1a205e98..be0f173c9ca 100644 --- a/profiling/src/profiling/mod.rs +++ b/profiling/src/profiling/mod.rs @@ -1,20 +1,16 @@ mod interrupts; -mod sample_type_filter; +mod samples; pub mod stack_walking; mod thread_utils; mod uploader; pub use interrupts::*; -pub use sample_type_filter::*; +pub use samples::*; pub use stack_walking::*; + use thread_utils::get_current_thread_name; use uploader::*; -#[cfg(all(php_has_fibers, not(test)))] -use crate::bindings::ddog_php_prof_get_active_fiber; -#[cfg(all(php_has_fibers, test))] -use crate::bindings::ddog_php_prof_get_active_fiber_test as ddog_php_prof_get_active_fiber; - use crate::bindings::{datadog_php_profiling_get_profiling_context, zend_execute_data}; use crate::config::SystemSettings; use crate::{Clocks, CLOCKS, TAGS}; @@ -37,6 +33,11 @@ use std::sync::{Arc, Barrier}; use std::thread::JoinHandle; use std::time::{Duration, Instant, SystemTime}; +#[cfg(all(php_has_fibers, not(test)))] +use crate::bindings::ddog_php_prof_get_active_fiber; +#[cfg(all(php_has_fibers, test))] +use crate::bindings::ddog_php_prof_get_active_fiber_test as ddog_php_prof_get_active_fiber; + #[cfg(feature = "timeline")] use core::{ptr, str}; #[cfg(feature = "timeline")] @@ -53,6 +54,9 @@ use crate::io::{ SOCKET_WRITE_SIZE_PROFILING_INTERVAL, SOCKET_WRITE_TIME_PROFILING_INTERVAL, }; +#[cfg(feature = "exception_profiling")] +use crate::exception::EXCEPTION_PROFILING_INTERVAL; + #[cfg(any( feature = "allocation_profiling", feature = "exception_profiling", @@ -60,9 +64,6 @@ use crate::io::{ ))] use datadog_profiling::api::UpscalingInfo; -#[cfg(feature = "exception_profiling")] -use crate::exception::EXCEPTION_PROFILING_INTERVAL; - const UPLOAD_PERIOD: Duration = Duration::from_secs(67); pub const NO_TIMESTAMP: i64 = 0; @@ -75,37 +76,6 @@ const UPLOAD_CHANNEL_CAPACITY: usize = 8; /// minit, and is destroyed on mshutdown. static mut PROFILER: OnceCell = OnceCell::new(); -/// Order this array this way: -/// 1. Always enabled types. -/// 2. On by default types. -/// 3. Off by default types. -#[derive(Default, Debug)] -pub struct SampleValues { - interrupt_count: i64, - wall_time: i64, - cpu_time: i64, - alloc_samples: i64, - alloc_size: i64, - timeline: i64, - exception: i64, - socket_read_time: i64, - socket_read_time_samples: i64, - socket_write_time: i64, - socket_write_time_samples: i64, - file_read_time: i64, - file_read_time_samples: i64, - file_write_time: i64, - file_write_time_samples: i64, - socket_read_size: i64, - socket_read_size_samples: i64, - socket_write_size: i64, - socket_write_size_samples: i64, - file_read_size: i64, - file_read_size_samples: i64, - file_write_size: i64, - file_write_size_samples: i64, -} - const WALL_TIME_PERIOD: Duration = Duration::from_millis(10); const WALL_TIME_PERIOD_TYPE: ValueType = ValueType { r#type: "wall-time", @@ -159,6 +129,8 @@ impl<'a> From<&'a Label> for ApiLabel<'a> { } } +// todo: use this alias when libdatadog 21 is released. +// pub type ValueType = datadog_profiling::api::ValueType<'static>; #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub struct ValueType { pub r#type: &'static str, @@ -180,7 +152,7 @@ impl ValueType { /// Apache per-dir settings use different service name, etc. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct ProfileIndex { - pub sample_types: Vec, + pub enabled_profiles: EnabledProfiles, pub tags: Arc>, } @@ -188,7 +160,10 @@ pub struct ProfileIndex { pub struct SampleData { pub frames: Vec, pub labels: Vec