diff --git a/Cargo.lock b/Cargo.lock index c79063463e..589e051669 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1483,6 +1483,7 @@ dependencies = [ "crossbeam-channel", "datadog-alloc", "datadog-profiling", + "datadog-thin-str", "ddcommon 10.0.0", "env_logger 0.11.3", "indexmap 2.2.6", @@ -1653,6 +1654,15 @@ dependencies = [ "syn 2.0.71", ] +[[package]] +name = "datadog-thin-str" +version = "1.0.0" +dependencies = [ + "allocator-api2", + "naughty-strings", + "tagged-pointer", +] + [[package]] name = "datadog-trace-normalization" version = "0.0.1" @@ -3327,6 +3337,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "naughty-strings" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f60b021f91344453a569296aced9b149bdf75ac182cfc4631f6b55a5aa590f" +dependencies = [ + "serde_json", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -5057,6 +5076,12 @@ dependencies = [ "libc 0.2.159", ] +[[package]] +name = "tagged-pointer" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538c399a85621c9b74ac21001d7e1dd81867ebd076724f047b1227e722de4a7" + [[package]] name = "tarpc" version = "0.31.0" diff --git a/profiling/Cargo.toml b/profiling/Cargo.toml index 2f0794e186..edc1219d68 100644 --- a/profiling/Cargo.toml +++ b/profiling/Cargo.toml @@ -21,6 +21,7 @@ chrono = { version = "0.4" } crossbeam-channel = { version = "0.5", default-features = false, features = ["std"] } datadog-alloc = { git = "https://github.com/DataDog/libdatadog", tag = "v10.0.0" } datadog-profiling = { git = "https://github.com/DataDog/libdatadog", tag = "v10.0.0" } +datadog-thin-str = { version = "1", path = "../thin-str", features = ["std"] } ddcommon = { git = "https://github.com/DataDog/libdatadog", tag = "v10.0.0" } env_logger = { version = "0.11", default-features = false } indexmap = { version = "2.2" } diff --git a/profiling/src/lib.rs b/profiling/src/lib.rs index 3b3db288a0..61b3ab62e2 100644 --- a/profiling/src/lib.rs +++ b/profiling/src/lib.rs @@ -6,8 +6,8 @@ mod logging; mod pcntl; pub mod profiling; mod sapi; -mod thin_str; mod wall_time; +mod well_known; #[cfg(php_run_time_cache)] mod string_set; diff --git a/profiling/src/profiling/mod.rs b/profiling/src/profiling/mod.rs index 7685e0cc52..ff5d788244 100644 --- a/profiling/src/profiling/mod.rs +++ b/profiling/src/profiling/mod.rs @@ -17,6 +17,7 @@ use crate::bindings::ddog_php_prof_get_active_fiber_test as ddog_php_prof_get_ac use crate::bindings::{datadog_php_profiling_get_profiling_context, zend_execute_data}; use crate::config::SystemSettings; +use crate::well_known::WellKnown; use crate::{CLOCKS, TAGS}; use chrono::Utc; use core::{ptr, str}; @@ -26,6 +27,7 @@ use datadog_profiling::api::{ }; use datadog_profiling::exporter::Tag; use datadog_profiling::internal::Profile as InternalProfile; +use datadog_thin_str::ThinString; use log::{debug, info, trace, warn}; use once_cell::sync::OnceCell; use std::borrow::Cow; @@ -49,6 +51,8 @@ use datadog_profiling::api::UpscalingInfo; #[cfg(feature = "exception_profiling")] use crate::exception::EXCEPTION_PROFILING_INTERVAL; +#[cfg(feature = "timeline")] +use crate::timeline::IncludeType; const UPLOAD_PERIOD: Duration = Duration::from_secs(67); @@ -514,9 +518,6 @@ pub enum UploadMessage { Upload(Box), } -#[cfg(feature = "timeline")] -const COW_EVAL: Cow = Cow::Borrowed("[eval]"); - const DDPROF_TIME: &str = "ddprof_time"; const DDPROF_UPLOAD: &str = "ddprof_upload"; @@ -907,14 +908,14 @@ impl Profiler { }]; #[cfg(feature = "timeline")] - pub fn collect_compile_string(&self, now: i64, duration: i64, filename: String, line: u32) { + pub fn collect_compile_string(&self, now: i64, duration: i64, filename: ThinString, line: u32) { let mut labels = Profiler::common_labels(Self::TIMELINE_COMPILE_FILE_LABELS.len()); labels.extend_from_slice(Self::TIMELINE_COMPILE_FILE_LABELS); let n_labels = labels.len(); match self.prepare_and_send_message( vec![ZendFrame { - function: COW_EVAL, + function: WellKnown::Eval.into(), file: Some(filename), line, }], @@ -942,7 +943,7 @@ impl Profiler { now: i64, duration: i64, filename: String, - include_type: &str, + include_type: IncludeType, ) { let mut labels = Profiler::common_labels(Self::TIMELINE_COMPILE_FILE_LABELS.len() + 1); labels.extend_from_slice(Self::TIMELINE_COMPILE_FILE_LABELS); @@ -955,7 +956,11 @@ impl Profiler { match self.prepare_and_send_message( vec![ZendFrame { - function: format!("[{include_type}]").into(), + function: match include_type { + IncludeType::Include => WellKnown::Include.into(), + IncludeType::Require => WellKnown::Require.into(), + IncludeType::Unknown => WellKnown::UnknownIncludeType.into(), + }, file: None, line: 0, }], @@ -984,7 +989,7 @@ impl Profiler { labels.push(Label { key: "event", - value: LabelValue::Str(std::borrow::Cow::Borrowed(event)), + value: LabelValue::Str(Cow::Borrowed(event)), }); let n_labels = labels.len(); @@ -1013,7 +1018,7 @@ impl Profiler { /// This function can be called to collect any fatal errors #[cfg(feature = "timeline")] - pub fn collect_fatal(&self, now: i64, file: String, line: u32, message: String) { + pub fn collect_fatal(&self, now: i64, file: ThinString, line: u32, message: String) { let mut labels = Profiler::common_labels(2); labels.push(Label { @@ -1029,7 +1034,7 @@ impl Profiler { match self.prepare_and_send_message( vec![ZendFrame { - function: "[fatal]".into(), + function: WellKnown::Fatal.into(), file: Some(file), line, }], @@ -1056,7 +1061,7 @@ impl Profiler { pub(crate) fn collect_opcache_restart( &self, now: i64, - file: String, + file: ThinString, line: u32, reason: &'static str, ) { @@ -1109,7 +1114,7 @@ impl Profiler { match self.prepare_and_send_message( vec![ZendFrame { - function: "[idle]".into(), + function: WellKnown::Idle.into(), file: None, line: 0, }], @@ -1165,7 +1170,7 @@ impl Profiler { match self.prepare_and_send_message( vec![ZendFrame { - function: "[gc]".into(), + function: WellKnown::Gc.into(), file: None, line: 0, }], @@ -1233,7 +1238,7 @@ impl Profiler { if let Some(functionname) = extract_function_name(func) { labels.push(Label { key: "fiber", - value: LabelValue::Str(functionname.into()), + value: LabelValue::Str(Cow::from(functionname)), }); } } @@ -1287,12 +1292,16 @@ mod tests { use super::*; use crate::config::AgentEndpoint; use datadog_profiling::exporter::Uri; + use datadog_thin_str::ConstStorage; use log::LevelFilter; fn get_frames() -> Vec { + static FOOBAR: ConstStorage<8> = ConstStorage::from_str("foobar()"); + static FOOBAR_PHP: ConstStorage<10> = ConstStorage::from_str("foobar.php"); + vec![ZendFrame { - function: "foobar()".into(), - file: Some("foobar.php".into()), + function: ThinString::from(&FOOBAR), + file: Some(ThinString::from(&FOOBAR_PHP)), line: 42, }] } diff --git a/profiling/src/profiling/stack_walking.rs b/profiling/src/profiling/stack_walking.rs index 38e1e6b575..d0b6e4ef9f 100644 --- a/profiling/src/profiling/stack_walking.rs +++ b/profiling/src/profiling/stack_walking.rs @@ -1,23 +1,24 @@ use crate::bindings::{zai_str_from_zstr, zend_execute_data, zend_function}; -use std::borrow::Cow; +use crate::well_known::WellKnown; +use datadog_alloc::Global; +use datadog_thin_str::ThinString; +use std::ops::Deref; use std::str::Utf8Error; #[cfg(php_frameless)] use crate::bindings::zend_flf_functions; + #[cfg(php_frameless)] use crate::bindings::{ ZEND_FRAMELESS_ICALL_0, ZEND_FRAMELESS_ICALL_1, ZEND_FRAMELESS_ICALL_2, ZEND_FRAMELESS_ICALL_3, }; -const COW_PHP_OPEN_TAG: Cow = Cow::Borrowed(" = Cow::Borrowed("[truncated]"); - #[derive(Default, Debug)] pub struct ZendFrame { // Most tools don't like frames that don't have function names, so use a // fake name if you need to like ", - pub file: Option, + pub function: ThinString, + pub file: Option, pub line: u32, // use 0 for no line info } @@ -30,7 +31,7 @@ pub struct ZendFrame { /// Namespaces are part of the class_name or function_name respectively. /// Closures and anonymous classes get reformatted by the backend (or maybe /// frontend, either way it's not our concern, at least not right now). -pub fn extract_function_name(func: &zend_function) -> Option { +pub fn extract_function_name(func: &zend_function) -> Option { let method_name: &[u8] = func.name().unwrap_or(b""); /* The top of the stack seems to reasonably often not have a function, but @@ -59,15 +60,17 @@ pub fn extract_function_name(func: &zend_function) -> Option { buffer.extend_from_slice(method_name); - Some(String::from_utf8_lossy(buffer.as_slice()).into_owned()) + let lossy = String::from_utf8_lossy(buffer.as_slice()); + Some(ThinString::from_str_in(&lossy, Global)) } -unsafe fn extract_file_and_line(execute_data: &zend_execute_data) -> (Option, u32) { +unsafe fn extract_file_and_line(execute_data: &zend_execute_data) -> (Option, u32) { // This should be Some, just being cautious. match execute_data.func.as_ref() { Some(func) if !func.is_internal() => { // Safety: zai_str_from_zstr will return a valid ZaiStr. - let file = zai_str_from_zstr(func.op_array.filename.as_mut()).into_string(); + let file_lossy = zai_str_from_zstr(func.op_array.filename.as_mut()).into_string_lossy(); + let file = ThinString::from_str_in(file_lossy.deref(), Global); let lineno = match execute_data.opline.as_ref() { Some(opline) => opline.lineno, None => 0, @@ -82,9 +85,10 @@ unsafe fn extract_file_and_line(execute_data: &zend_execute_data) -> (Option { @@ -100,9 +104,9 @@ mod detail { /// string in the slot currently, then create one by calling the /// provided function, store it in the string cache and cache slot, /// and return it. - fn get_or_insert(&mut self, slot: usize, f: F) -> Option + fn get_or_insert(&mut self, slot: usize, f: F) -> Option where - F: FnOnce() -> Option, + F: FnOnce() -> Option, { debug_assert!(slot < self.cache_slots.len()); let cached = unsafe { self.cache_slots.get_unchecked_mut(slot) }; @@ -116,7 +120,7 @@ mod detail { // so this ThinStr points into the same string set that // created it. let str = unsafe { self.string_set.get_thin_str(thin_str) }; - Some(str.to_string()) + Some(ThinString::from_str_in(str, Global)) } None => { let string = f()?; @@ -221,7 +225,7 @@ mod detail { &**zend_flf_functions.offset(opline.extended_value as isize) }; samples.push(ZendFrame { - function: extract_function_name(func).map(Cow::Owned).unwrap(), + function: extract_function_name(func).unwrap(), file: None, line: 0, }); @@ -240,7 +244,7 @@ mod detail { */ if samples.len() == max_depth - 1 { samples.push(ZendFrame { - function: COW_TRUNCATED, + function: ThinString::from(WellKnown::Truncated), file: None, line: 0, }); @@ -292,7 +296,7 @@ mod detail { let mut stats = cell.borrow_mut(); stats.not_applicable += 1; }); - let function = extract_function_name(func).map(Cow::Owned); + let function = extract_function_name(func); let (file, line) = extract_file_and_line(execute_data); (function, file, line) } @@ -300,7 +304,7 @@ mod detail { if function.is_some() || file.is_some() { Some(ZendFrame { - function: function.unwrap_or(COW_PHP_OPEN_TAG), + function: function.unwrap_or(WellKnown::PhpOpenTag.into()), file, line, }) @@ -312,16 +316,16 @@ mod detail { fn handle_function_cache_slot( func: &zend_function, string_cache: &mut StringCache, - ) -> Option> { + ) -> Option { let fname = string_cache.get_or_insert(0, || extract_function_name(func))?; - Some(Cow::Owned(fname)) + Some(ThinString::from_str_in(&fname, Global)) } unsafe fn handle_file_cache_slot( execute_data: &zend_execute_data, string_cache: &mut StringCache, - ) -> (Option, u32) { - let option = string_cache.get_or_insert(1, || -> Option { + ) -> (Option, u32) { + let option = string_cache.get_or_insert(1, || -> Option { unsafe { // Safety: if we have cache slots, we definitely have a func. let func = &*execute_data.func; @@ -330,7 +334,9 @@ mod detail { }; // SAFETY: calling C function with correct args. - let file = zai_str_from_zstr(func.op_array.filename.as_mut()).into_string(); + let file_lossy = + zai_str_from_zstr(func.op_array.filename.as_mut()).into_string_lossy(); + let file = ThinString::from_str_in(file_lossy.deref(), Global); Some(file) } }); @@ -341,7 +347,7 @@ mod detail { Some(opline) => opline.lineno, None => 0, }; - (Some(filename), lineno) + (Some(ThinString::from_str_in(&filename, Global)), lineno) } None => (None, 0), } @@ -380,7 +386,7 @@ mod detail { */ if samples.len() == max_depth - 1 { samples.push(ZendFrame { - function: COW_TRUNCATED, + function: WellKnown::Truncated.into(), file: None, line: 0, }); @@ -401,7 +407,7 @@ mod detail { // Only create a new frame if there's file or function info. if file.is_some() || function.is_some() { // If there's no function name, use a fake name. - let function = function.map(Cow::Owned).unwrap_or(COW_PHP_OPEN_TAG); + let function = function.unwrap_or(WellKnown::PhpOpenTag.into()); return Some(ZendFrame { function, file, @@ -415,6 +421,17 @@ mod detail { pub use detail::*; +#[cfg(test)] +mod size_tests { + use super::*; + use core::mem::size_of; + + #[test] + fn test_frame_size() { + assert_eq!(size_of::(), size_of::() * 3); + } +} + #[cfg(all(test, stack_walking_tests))] mod tests { use super::*; diff --git a/profiling/src/string_set.rs b/profiling/src/string_set.rs index 5a8a89fa01..7ed64f000b 100644 --- a/profiling/src/string_set.rs +++ b/profiling/src/string_set.rs @@ -1,7 +1,7 @@ -use crate::thin_str::ThinStr; use core::hash; use core::ops::Deref; use datadog_alloc::{ChainAllocator, VirtualAllocator}; +use datadog_thin_str::*; type Hasher = hash::BuildHasherDefault; type HashSet = std::collections::HashSet; @@ -77,13 +77,15 @@ impl StringSet { // No match. Make a new string in the arena, and fudge its // lifetime to appease the borrow checker. let new_str = { - let s = ThinStr::try_from_str_in(str, &self.arena) + let owned = ThinString::try_from_str_in(str, &self.arena) .expect("allocation for StringSet::insert to succeed"); + let borrowed = owned.as_thin_str(); + // SAFETY: all references to this value get re-narrowed to // the lifetime of the string set. The string set will // keep the arena alive, making the access safe. - unsafe { core::mem::transmute::, ThinStr<'static>>(s) } + unsafe { core::mem::transmute::, ThinStr<'static>>(borrowed) } }; // Add it to the set. diff --git a/profiling/src/thin_str.rs b/profiling/src/thin_str.rs deleted file mode 100644 index 01fd8a2f8c..0000000000 --- a/profiling/src/thin_str.rs +++ /dev/null @@ -1,223 +0,0 @@ -use datadog_alloc::{AllocError, Allocator, ChainAllocator}; -use std::alloc::Layout; -use std::borrow::Borrow; -use std::hash; -use std::marker::PhantomData; -use std::ops::Deref; -use std::ptr::NonNull; - -/// A struct which acts like a thin &str. It does this by storing the size -/// of the string just before the bytes of the string. -#[derive(Copy, Clone)] -#[repr(transparent)] -pub struct ThinStr<'a> { - thin_ptr: ThinPtr, - - /// Since [ThinStr] doesn't hold a reference but acts like one, indicate - /// this to the compiler with phantom data. This takes up no space. - _marker: PhantomData<&'a str>, -} - -pub trait ArenaAllocator: Allocator {} - -impl ArenaAllocator for ChainAllocator {} - -impl ThinStr<'static> { - pub fn new() -> ThinStr<'static> { - Self { - thin_ptr: EMPTY_INLINE_STRING.as_thin_ptr(), - _marker: PhantomData, - } - } -} - -impl Default for ThinStr<'static> { - fn default() -> Self { - Self::new() - } -} - -impl<'a> ThinStr<'a> { - // todo: move ArenaAllocator trait to `datadog_alloc` as a marker trait - // (meaning, remove the associated method and leave that in prof)? - #[allow(dead_code)] - pub fn try_from_str_in(str: &str, arena: &'a impl ArenaAllocator) -> Result { - let thin_ptr = ThinPtr::try_from_str_in(str, arena)?; - let _marker = PhantomData; - Ok(Self { thin_ptr, _marker }) - } - - /// Gets the layout of a ThinStr, such as to deallocate it. - #[allow(unused)] - #[inline] - pub fn layout(&self) -> Layout { - self.thin_ptr.layout() - } -} - -impl<'a> Deref for ThinStr<'a> { - type Target = str; - - fn deref(&self) -> &Self::Target { - let slice = { - let len = self.thin_ptr.len(); - let data = self.thin_ptr.data().as_ptr(); - - // SAFETY: bytes are never handed out as mut, so const slices are - // not going to break aliasing rules, and this is the correct - // lifetime for the data. - unsafe { core::slice::from_raw_parts(data, len) } - }; - - // SAFETY: since this is a copy of a valid utf-8 string, then it must - // also be valid utf-8. - unsafe { core::str::from_utf8_unchecked(slice) } - } -} - -impl<'a> hash::Hash for ThinStr<'a> { - fn hash(&self, state: &mut H) { - self.deref().hash(state) - } -} - -impl<'a> PartialEq for ThinStr<'a> { - fn eq(&self, other: &Self) -> bool { - self.deref().eq(other.deref()) - } -} - -impl<'a> Eq for ThinStr<'a> {} - -impl<'a> Borrow for ThinStr<'a> { - fn borrow(&self) -> &str { - self.deref() - } -} - -#[derive(Clone, Copy)] -struct ThinPtr { - /// Points to the beginning of a struct which looks like this: - /// ``` - /// #[repr(C)] - /// struct InlineString { - /// /// Stores the len of `data`. - /// size: [u8; core::mem::size_of::()], - /// data: [u8], - /// } - /// ``` - size_ptr: NonNull, -} - -impl ThinPtr { - /// Reads the size prefix to get the length of the string. - const fn len(self) -> usize { - // SAFETY: ThinStr points to the size prefix of the string. - let size = unsafe { self.size_ptr.cast::<[u8; USIZE_WIDTH]>().as_ptr().read() }; - usize::from_ne_bytes(size) - } - - /// Returns a pointer to the string data (not to the header). - const fn data(self) -> NonNull { - // SAFETY: ThinStr points to the size prefix of the string, and the - // string data is located immediately after without padding. - let ptr = unsafe { self.size_ptr.as_ptr().add(USIZE_WIDTH) }; - - // SAFETY: derived from a NonNull, so it's also NonNull. - unsafe { NonNull::new_unchecked(ptr) } - } - - /// Gets the layout of a ThinStr, such as to deallocate it. - #[allow(unused)] - #[inline] - fn layout(self) -> Layout { - let len = self.len(); - // SAFETY: since this object exists, its layout must be valid. - unsafe { Layout::from_size_align_unchecked(len + USIZE_WIDTH, 1) } - } - - fn try_from_str_in(str: &str, arena: &impl Allocator) -> Result { - let inline_size = str.len() + USIZE_WIDTH; - - let layout = match Layout::from_size_align(inline_size, 1) { - Ok(l) => l, - Err(_) => return Err(AllocError), - }; - let allocation = arena.allocate(layout)?.cast::().as_ptr(); - - let size = allocation.cast::<[u8; USIZE_WIDTH]>(); - // SAFETY: writing into uninitialized new allocation at correct place. - unsafe { size.write(str.len().to_ne_bytes()) }; - - // SAFETY: the data pointer is just after the header, and the - // allocation is at least that long. - let data = unsafe { allocation.add(USIZE_WIDTH) }; - - // SAFETY: the allocation is big enough, locations are distinct, and - // the alignment is 1 (so it's always aligned), and the memory is safe - // for writing. - unsafe { core::ptr::copy_nonoverlapping(str.as_bytes().as_ptr(), data, str.len()) }; - - let size_ptr = unsafe { NonNull::new_unchecked(allocation) }; - Ok(ThinPtr { size_ptr }) - } -} - -#[repr(C)] -struct StaticInlineString { - /// Stores the len of `data`. - size: [u8; core::mem::size_of::()], - data: [u8; N], -} - -impl StaticInlineString { - fn as_thin_ptr(&self) -> ThinPtr { - let ptr = core::ptr::addr_of!(EMPTY_INLINE_STRING).cast::(); - // SAFETY: derived from static address, and ThinStr does not allow - // modifications, so the mut-cast is also fine. - let size_ptr = unsafe { NonNull::new_unchecked(ptr.cast_mut()) }; - ThinPtr { size_ptr } - } -} - -const USIZE_WIDTH: usize = core::mem::size_of::(); - -static EMPTY_INLINE_STRING: StaticInlineString<0> = StaticInlineString::<0> { - size: [0; USIZE_WIDTH], - data: [], -}; - -#[cfg(test)] -mod tests { - use super::*; - use datadog_alloc::Global; - use datadog_profiling::collections::string_table::wordpress_test_data; - - // Not really, please manually de-allocate strings when done with them. - impl ArenaAllocator for Global {} - - #[test] - fn test_allocation_and_deallocation() { - let alloc = Global; - - let mut thin_strs: Vec = wordpress_test_data::WORDPRESS_STRINGS - .iter() - .map(|str| { - let thin_str = ThinStr::try_from_str_in(str, &alloc).unwrap(); - let actual = thin_str.deref(); - assert_eq!(*str, actual); - thin_str - }) - .collect(); - - // This could detect out-of-bounds writes. - for (thin_str, str) in thin_strs.iter().zip(wordpress_test_data::WORDPRESS_STRINGS) { - let actual = thin_str.deref(); - assert_eq!(str, actual); - } - - for thin_str in thin_strs.drain(..) { - unsafe { alloc.deallocate(thin_str.thin_ptr.size_ptr, thin_str.layout()) }; - } - } -} diff --git a/profiling/src/timeline.rs b/profiling/src/timeline.rs index efbbd9a1f4..cc80f7d25f 100644 --- a/profiling/src/timeline.rs +++ b/profiling/src/timeline.rs @@ -4,16 +4,49 @@ use crate::zend::{ InternalFunctionHandler, }; use crate::REQUEST_LOCALS; +use datadog_alloc::Global; +use datadog_thin_str::ThinString; use ddcommon::cstr; use libc::c_char; use log::{error, trace}; #[cfg(php_zts)] use std::cell::Cell; use std::cell::RefCell; -use std::ptr; -use std::time::Instant; -use std::time::SystemTime; -use std::time::UNIX_EPOCH; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; +use std::{fmt, ptr}; + +#[derive(Copy, Clone, Debug)] +pub enum IncludeType { + Include, + Require, + Unknown, +} + +impl From for IncludeType { + fn from(value: u32) -> Self { + match value { + zend::ZEND_INCLUDE => IncludeType::Include, + zend::ZEND_REQUIRE => IncludeType::Require, + _ => IncludeType::Unknown, + } + } +} + +impl AsRef for IncludeType { + fn as_ref(&self) -> &str { + match self { + IncludeType::Include => "include", + IncludeType::Require => "require", + IncludeType::Unknown => "unknown", + } + } +} + +impl fmt::Display for IncludeType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_ref()) + } +} /// The engine's original (or neighbouring extensions) `gc_collect_cycles()` function static mut PREV_GC_COLLECT_CYCLES: Option = None; @@ -208,13 +241,13 @@ unsafe extern "C" fn ddog_php_prof_zend_error_observer( #[cfg(not(zend_error_observer_80))] let filename_str = unsafe { zai_str_from_zstr(file.as_mut()) }; - let filename = filename_str.to_string_lossy().into_owned(); + let filename = ThinString::from_str_in(filename_str.to_string_lossy().as_ref(), Global); let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); if let Some(profiler) = Profiler::get() { let now = now.as_nanos() as i64; profiler.collect_fatal(now, filename, line, unsafe { - zend::zai_str_from_zstr(message.as_mut()).into_string() + zai_str_from_zstr(message.as_mut()).into_string() }); } } @@ -241,8 +274,12 @@ unsafe extern "C" fn ddog_php_prof_zend_accel_schedule_restart_hook(reason: i32) if let Some(profiler) = Profiler::get() { let now = now.as_nanos() as i64; let file = unsafe { - zend::zai_str_from_zstr(zend::zend_get_executed_filename_ex().as_mut()) - .into_string() + // If this has to make a lossy String, then we'll get an extra + // copy (once to String, then another to ThinString). I don't + // expect this to be hit often, nor a nice way around it. + let name = + zai_str_from_zstr(zend_get_executed_filename_ex().as_mut()).into_string_lossy(); + ThinString::from_str_in(name.as_ref(), Global) }; profiler.collect_opcache_restart( now, @@ -583,7 +620,9 @@ unsafe extern "C" fn ddog_php_prof_compile_string( return op_array; } - let filename = zai_str_from_zstr(zend_get_executed_filename_ex().as_mut()).into_string(); + let filename_lossy = + zai_str_from_zstr(zend_get_executed_filename_ex().as_mut()).into_string_lossy(); + let filename = ThinString::from_str_in(&filename_lossy, Global); let line = zend::zend_get_executed_lineno(); @@ -639,11 +678,8 @@ unsafe extern "C" fn ddog_php_prof_compile_file( return op_array; } - let include_type = match r#type as u32 { - zend::ZEND_INCLUDE => "include", // `include_once()` and `include_once()` - zend::ZEND_REQUIRE => "require", // `require()` and `require_once()` - _default => "", - }; + let include_type = IncludeType::from(r#type as u32); + let include_str = include_type.as_ref(); // Extract the filename from the returned op_array. // We could also extract from the handle, but those filenames might be different from @@ -654,7 +690,7 @@ unsafe extern "C" fn ddog_php_prof_compile_file( let filename = zai_str_from_zstr((*op_array).filename.as_mut()).into_string(); trace!( - "Compile file \"{filename}\" with include type \"{include_type}\" took {} nanoseconds", + "Compile file \"{filename}\" with include type \"{include_str}\" took {} nanoseconds", duration.as_nanos(), ); diff --git a/profiling/src/well_known.rs b/profiling/src/well_known.rs new file mode 100644 index 0000000000..287662bbd9 --- /dev/null +++ b/profiling/src/well_known.rs @@ -0,0 +1,51 @@ +use datadog_alloc::Global; + +use datadog_thin_str::{ConstStorage, ThinString}; + +pub enum WellKnown { + Empty, + + Eval, + Fatal, + Gc, + Idle, + Include, + PhpOpenTag, + Require, + Truncated, + UnknownIncludeType, +} + +impl From for ThinString { + fn from(well_known: WellKnown) -> Self { + match well_known { + WellKnown::Empty => ThinString::from(&inline_strings::EMPTY), + WellKnown::Eval => ThinString::from(&inline_strings::EVAL), + WellKnown::Fatal => ThinString::from(&inline_strings::FATAL), + WellKnown::Gc => ThinString::from(&inline_strings::GC), + WellKnown::Idle => ThinString::from(&inline_strings::IDLE), + WellKnown::Include => ThinString::from(&inline_strings::INCLUDE), + WellKnown::PhpOpenTag => ThinString::from(&inline_strings::PHP_OPEN_TAG), + WellKnown::Require => ThinString::from(&inline_strings::REQUIRE), + WellKnown::Truncated => ThinString::from(&inline_strings::TRUNCATED), + WellKnown::UnknownIncludeType => { + ThinString::from(&inline_strings::UNKNOWN_INCLUDE_TYPE) + } + } + } +} + +mod inline_strings { + use super::*; + pub static EMPTY: ConstStorage<0> = ConstStorage::from_str(""); + pub static EVAL: ConstStorage<6> = ConstStorage::from_str("[eval]"); + pub static FATAL: ConstStorage<7> = ConstStorage::from_str("[fatal]"); + pub static GC: ConstStorage<4> = ConstStorage::from_str("[gc]"); + pub static IDLE: ConstStorage<6> = ConstStorage::from_str("[idle]"); + pub static INCLUDE: ConstStorage<9> = ConstStorage::from_str("[include]"); + pub static PHP_OPEN_TAG: ConstStorage<5> = ConstStorage::from_str(" = ConstStorage::from_str("[require]"); + pub static TRUNCATED: ConstStorage<11> = ConstStorage::from_str("[truncated]"); + pub static UNKNOWN_INCLUDE_TYPE: ConstStorage<22> = + ConstStorage::from_str("[unknown include type]"); +}