diff --git a/Cargo.lock b/Cargo.lock index 8cf016e53f8..9aeb5785509 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6551,6 +6551,7 @@ dependencies = [ "regex", "serde", "serde_json", + "servo_allocator", "servo_config", "task_info", "tikv-jemalloc-sys", @@ -7655,6 +7656,8 @@ dependencies = [ name = "servo_allocator" version = "0.0.1" dependencies = [ + "backtrace", + "fnv", "libc", "tikv-jemalloc-sys", "tikv-jemallocator", diff --git a/components/allocator/Cargo.toml b/components/allocator/Cargo.toml index ae38e78db82..b88d98914aa 100644 --- a/components/allocator/Cargo.toml +++ b/components/allocator/Cargo.toml @@ -11,8 +11,13 @@ rust-version.workspace = true path = "lib.rs" [features] +allocation-tracking = ["backtrace", "fnv"] use-system-allocator = ["libc"] +[dependencies] +backtrace = { workspace = true, optional = true } +fnv = { workspace = true, optional = true } + [target.'cfg(not(any(windows, target_env = "ohos")))'.dependencies] libc = { workspace = true, optional = true } tikv-jemalloc-sys = { workspace = true } diff --git a/components/allocator/lib.rs b/components/allocator/lib.rs index 96160b01e7c..86a4d8055de 100644 --- a/components/allocator/lib.rs +++ b/components/allocator/lib.rs @@ -2,13 +2,53 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -//! Selecting the default global allocator for Servo +//! Selecting the default global allocator for Servo, and exposing common +//! allocator introspection APIs for memory profiling. +use std::os::raw::c_void; + +#[cfg(not(feature = "allocation-tracking"))] #[global_allocator] static ALLOC: Allocator = Allocator; +#[cfg(feature = "allocation-tracking")] +#[global_allocator] +static ALLOC: crate::tracking::AccountingAlloc = + crate::tracking::AccountingAlloc::with_allocator(Allocator); + +#[cfg(feature = "allocation-tracking")] +mod tracking; + +pub fn dump_unmeasured() { + #[cfg(feature = "allocation-tracking")] + ALLOC.dump_unmeasured_allocations(); +} + pub use crate::platform::*; +type EnclosingSizeFn = unsafe extern "C" fn(*const c_void) -> usize; + +/// # Safety +/// No restrictions. The passed pointer is never dereferenced. +/// This function is only marked unsafe because the MallocSizeOfOps APIs +/// requires an unsafe function pointer. +#[cfg(feature = "allocation-tracking")] +unsafe extern "C" fn enclosing_size_impl(ptr: *const c_void) -> usize { + let (adjusted, size) = crate::ALLOC.enclosing_size(ptr); + if size != 0 { + crate::ALLOC.note_allocation(adjusted, size); + } + size +} + +#[allow(non_upper_case_globals)] +#[cfg(feature = "allocation-tracking")] +pub static enclosing_size: Option = Some(crate::enclosing_size_impl); + +#[allow(non_upper_case_globals)] +#[cfg(not(feature = "allocation-tracking"))] +pub static enclosing_size: Option = None; + #[cfg(not(any(windows, feature = "use-system-allocator", target_env = "ohos")))] mod platform { use std::os::raw::c_void; @@ -21,7 +61,10 @@ mod platform { /// /// Passing a non-heap allocated pointer to this function results in undefined behavior. pub unsafe extern "C" fn usable_size(ptr: *const c_void) -> usize { - unsafe { tikv_jemallocator::usable_size(ptr) } + let size = unsafe { tikv_jemallocator::usable_size(ptr) }; + #[cfg(feature = "allocation-tracking")] + crate::ALLOC.note_allocation(ptr, size); + size } /// Memory allocation APIs compatible with libc @@ -46,12 +89,18 @@ mod platform { pub unsafe extern "C" fn usable_size(ptr: *const c_void) -> usize { #[cfg(target_vendor = "apple")] unsafe { - return libc::malloc_size(ptr); + let size = libc::malloc_size(ptr); + #[cfg(feature = "allocation-tracking")] + crate::ALLOC.note_allocation(ptr, size); + size } #[cfg(not(target_vendor = "apple"))] unsafe { - return libc::malloc_usable_size(ptr as *mut _); + let size = libc::malloc_usable_size(ptr as *mut _); + #[cfg(feature = "allocation-tracking")] + crate::ALLOC.note_allocation(ptr, size); + size } } @@ -81,7 +130,10 @@ mod platform { ptr = *(ptr as *const *const c_void).offset(-1) } - HeapSize(heap, 0, ptr) as usize + let size = HeapSize(heap, 0, ptr) as usize; + #[cfg(feature = "allocation-tracking")] + crate::ALLOC.note_allocation(ptr, size); + size } } } diff --git a/components/allocator/tracking.rs b/components/allocator/tracking.rs new file mode 100644 index 00000000000..f5c98467327 --- /dev/null +++ b/components/allocator/tracking.rs @@ -0,0 +1,211 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! An allocator wrapper that records metadata about each live allocation. +//! This metadata can then be used to identify allocations that are not visible +//! through any existing MallocSizeOf path. + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::cell::Cell; +use std::collections::hash_map::Entry; +use std::os::raw::c_void; +use std::sync::Mutex; + +use fnv::{FnvBuildHasher, FnvHashMap}; + +/// The maximum number of allocations that we'll keep track of. Once the limit +/// is reached, we'll evict the first allocation that is smaller than the new addition. +const MAX_TRACKED_ALLOCATIONS: usize = usize::MAX; +/// Cap the number of stack frames that we'll store per allocation. +const MAX_FRAMES: usize = 50; +/// A certain number of frames at the top of the allocation stack are just +/// just internal liballoc implementation details or AccountingAlloc functions. +/// We can skip them without losing any meaningful information. +const SKIPPED_FRAMES: usize = 5; +/// The minimum size of allocation that we'll track. Can be used to reduce overhead +/// by skipping bookkeeping for small allocations. +const MIN_SIZE: usize = 0; + +thread_local! { + static IN_ALLOCATION: Cell = Cell::new(false); +} + +#[derive(PartialEq, Eq)] +struct AllocSite { + /// The stack at the time this allocation was recorded. + frames: [*mut std::ffi::c_void; MAX_FRAMES], + /// The start of the allocated memory. + ptr: *mut u8, + /// The size of the allocated memory. + size: usize, + /// True if this allocation site's size is ever queried after the initial + /// allocation. If false, it means that the allocation is not visible from + /// any of the MallocSizeOf roots. + noted: bool, +} + +impl AllocSite { + fn contains(&self, ptr: *mut u8) -> bool { + ptr >= self.ptr && ptr < unsafe { self.ptr.offset(self.size as isize) } + } +} + +unsafe impl Send for AllocSite {} + +/// A map of pointers to allocation callsite metadata. +static ALLOCATION_SITES: Mutex> = + const { Mutex::new(FnvHashMap::with_hasher(FnvBuildHasher::new())) }; + +#[derive(Default)] +pub struct AccountingAlloc { + allocator: A, +} + +impl AccountingAlloc { + pub const fn with_allocator(allocator: A) -> Self { + Self { allocator } + } + + fn remove_allocation(&self, ptr: *const c_void, size: usize) { + if size < MIN_SIZE { + return; + } + let old = IN_ALLOCATION.with(|status| status.replace(true)); + if old { + return; + } + let mut sites = ALLOCATION_SITES.lock().unwrap(); + if let Entry::Occupied(e) = sites.entry(ptr as usize) { + e.remove(); + } + IN_ALLOCATION.with(|status| status.set(old)); + } + + fn record_allocation(&self, ptr: *mut u8, size: usize) { + if size < MIN_SIZE { + return; + } + let old = IN_ALLOCATION.with(|status| status.replace(true)); + if old { + return; + } + let mut num_skipped = 0; + let mut num_frames = 0; + let mut frames = [std::ptr::null_mut(); MAX_FRAMES]; + backtrace::trace(|frame| { + if num_skipped < SKIPPED_FRAMES { + num_skipped += 1; + return true; + } + frames[num_frames] = frame.ip(); + num_frames += 1; + num_frames < MAX_FRAMES + }); + let site = AllocSite { + frames, + size, + ptr, + noted: false, + }; + let mut sites = ALLOCATION_SITES.lock().unwrap(); + + if sites.len() < MAX_TRACKED_ALLOCATIONS { + sites.insert(ptr as usize, site); + } else if let Some(key) = sites + .iter() + .find(|(_, s)| s.size < site.size) + .map(|(k, _)| k.clone()) + { + sites.remove(&key); + sites.insert(ptr as usize, site); + } + + IN_ALLOCATION.with(|status| status.set(old)); + } + + pub(crate) fn enclosing_size(&self, ptr: *const c_void) -> (*const c_void, usize) { + ALLOCATION_SITES + .lock() + .unwrap() + .iter() + .find(|(_, site)| site.contains(ptr.cast_mut().cast())) + .map_or((std::ptr::null_mut(), 0), |(_, site)| { + (site.ptr.cast(), site.size) + }) + } + + pub(crate) fn note_allocation(&self, ptr: *const c_void, size: usize) { + if size < MIN_SIZE { + return; + } + IN_ALLOCATION.with(|status| status.set(true)); + if let Some(site) = ALLOCATION_SITES.lock().unwrap().get_mut(&(ptr as usize)) { + site.noted = true; + } + IN_ALLOCATION.with(|status| status.set(false)); + } + + pub(crate) fn dump_unmeasured_allocations(&self) { + // Ensure that we ignore all allocations triggered while processing + // the existing allocation data. + IN_ALLOCATION.with(|status| status.set(true)); + { + let sites = ALLOCATION_SITES.lock().unwrap(); + let default = "???"; + for site in sites + .values() + .filter(|site| site.size > MIN_SIZE && !site.noted) + { + let mut resolved = vec![]; + for ip in site.frames.iter().filter(|ip| !ip.is_null()) { + backtrace::resolve(*ip, |symbol| { + resolved.push(( + symbol.filename().map(|f| f.to_owned()), + symbol.lineno(), + symbol.name().map(|n| format!("{}", n)), + )); + }); + } + + println!("---\n{}\n", site.size); + for (filename, line, symbol) in &resolved { + let fname = filename.as_ref().map(|f| f.display().to_string()); + println!( + "{}:{} ({})", + fname.as_deref().unwrap_or(default), + line.unwrap_or_default(), + symbol.as_deref().unwrap_or(default), + ); + } + } + } + IN_ALLOCATION.with(|status| status.set(false)); + } +} + +unsafe impl GlobalAlloc for AccountingAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let ptr = unsafe { self.allocator.alloc(layout) }; + self.record_allocation(ptr, layout.size()); + ptr + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + unsafe { self.allocator.dealloc(ptr, layout) }; + self.remove_allocation(ptr.cast(), layout.size()); + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + let ptr = unsafe { self.allocator.alloc_zeroed(layout) }; + self.record_allocation(ptr, layout.size()); + ptr + } + + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + self.remove_allocation(ptr.cast(), layout.size()); + let ptr = unsafe { self.allocator.realloc(ptr, layout, new_size) }; + self.record_allocation(ptr, new_size); + ptr + } +} diff --git a/components/profile/Cargo.toml b/components/profile/Cargo.toml index 73c7924d863..381218c5e32 100644 --- a/components/profile/Cargo.toml +++ b/components/profile/Cargo.toml @@ -18,6 +18,7 @@ log = { workspace = true } profile_traits = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +servo_allocator = { path = "../allocator" } servo_config = { path = "../config" } time = { workspace = true } diff --git a/components/profile/mem.rs b/components/profile/mem.rs index 0aff98b89d0..22cf17bbf7f 100644 --- a/components/profile/mem.rs +++ b/components/profile/mem.rs @@ -112,6 +112,7 @@ impl Profiler { }) .collect(); let _ = sender.send(MemoryReportResult { results }); + servo_allocator::dump_unmeasured(); true }, ProfilerMsg::Exit => false, diff --git a/components/shared/profile/mem.rs b/components/shared/profile/mem.rs index cd378bf32eb..57b442bd5cb 100644 --- a/components/shared/profile/mem.rs +++ b/components/shared/profile/mem.rs @@ -293,7 +293,7 @@ pub fn perform_memory_report(f: F) { let seen_pointer = move |ptr| SEEN_POINTERS.with(|pointers| !pointers.borrow_mut().insert(ptr)); let mut ops = MallocSizeOfOps::new( servo_allocator::usable_size, - None, + servo_allocator::enclosing_size, Some(Box::new(seen_pointer)), ); f(&mut ops); diff --git a/ports/servoshell/Cargo.toml b/ports/servoshell/Cargo.toml index 06304184929..93199cb4917 100644 --- a/ports/servoshell/Cargo.toml +++ b/ports/servoshell/Cargo.toml @@ -121,7 +121,7 @@ headers = { workspace = true } net = { path = "../../components/net" } net_traits = { workspace = true } serde_json = { workspace = true } -# For optional feature servo_allocator/use-system-allocator +# For optional feature servo_allocator/use-system-allocator and servo_allocator/allocation-tracking servo_allocator = { path = "../../components/allocator" } surfman = { workspace = true, features = ["sm-raw-window-handle-06", "sm-x11"] } winit = { workspace = true }