mirror of
https://github.com/servo/servo.git
synced 2025-08-26 23:58:20 +01:00
allocator: Add optional heap allocation measurement tracking. (#38727)
Add an off-by-default allocator mode that tracks all live allocations with sizes and associated stack traces. We also track if each allocation is visited as part of a measuring heap usage in `about:memory`, allowing us to report on allocations that are not tracked yet. Right now the list of untracked allocations is dumped to stdout; I have a python script coming in a separate PR which makes it easier to perform analysis on the massive output. Testing: Manually tested with `./mach build -d --features servo_allocator/allocation-tracking` and visiting about:memory. Part of: #11559 --------- Signed-off-by: Josh Matthews <josh@joshmatthews.net>
This commit is contained in:
parent
2022831e4f
commit
f1a9ceed4f
8 changed files with 280 additions and 7 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -6551,6 +6551,7 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"servo_allocator",
|
||||||
"servo_config",
|
"servo_config",
|
||||||
"task_info",
|
"task_info",
|
||||||
"tikv-jemalloc-sys",
|
"tikv-jemalloc-sys",
|
||||||
|
@ -7655,6 +7656,8 @@ dependencies = [
|
||||||
name = "servo_allocator"
|
name = "servo_allocator"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"fnv",
|
||||||
"libc",
|
"libc",
|
||||||
"tikv-jemalloc-sys",
|
"tikv-jemalloc-sys",
|
||||||
"tikv-jemallocator",
|
"tikv-jemallocator",
|
||||||
|
|
|
@ -11,8 +11,13 @@ rust-version.workspace = true
|
||||||
path = "lib.rs"
|
path = "lib.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
allocation-tracking = ["backtrace", "fnv"]
|
||||||
use-system-allocator = ["libc"]
|
use-system-allocator = ["libc"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
backtrace = { workspace = true, optional = true }
|
||||||
|
fnv = { workspace = true, optional = true }
|
||||||
|
|
||||||
[target.'cfg(not(any(windows, target_env = "ohos")))'.dependencies]
|
[target.'cfg(not(any(windows, target_env = "ohos")))'.dependencies]
|
||||||
libc = { workspace = true, optional = true }
|
libc = { workspace = true, optional = true }
|
||||||
tikv-jemalloc-sys = { workspace = true }
|
tikv-jemalloc-sys = { workspace = true }
|
||||||
|
|
|
@ -2,13 +2,53 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* 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]
|
#[global_allocator]
|
||||||
static ALLOC: Allocator = Allocator;
|
static ALLOC: Allocator = Allocator;
|
||||||
|
|
||||||
|
#[cfg(feature = "allocation-tracking")]
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOC: crate::tracking::AccountingAlloc<Allocator> =
|
||||||
|
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::*;
|
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<EnclosingSizeFn> = Some(crate::enclosing_size_impl);
|
||||||
|
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
#[cfg(not(feature = "allocation-tracking"))]
|
||||||
|
pub static enclosing_size: Option<EnclosingSizeFn> = None;
|
||||||
|
|
||||||
#[cfg(not(any(windows, feature = "use-system-allocator", target_env = "ohos")))]
|
#[cfg(not(any(windows, feature = "use-system-allocator", target_env = "ohos")))]
|
||||||
mod platform {
|
mod platform {
|
||||||
use std::os::raw::c_void;
|
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.
|
/// 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 {
|
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
|
/// Memory allocation APIs compatible with libc
|
||||||
|
@ -46,12 +89,18 @@ mod platform {
|
||||||
pub unsafe extern "C" fn usable_size(ptr: *const c_void) -> usize {
|
pub unsafe extern "C" fn usable_size(ptr: *const c_void) -> usize {
|
||||||
#[cfg(target_vendor = "apple")]
|
#[cfg(target_vendor = "apple")]
|
||||||
unsafe {
|
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"))]
|
#[cfg(not(target_vendor = "apple"))]
|
||||||
unsafe {
|
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)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
211
components/allocator/tracking.rs
Normal file
211
components/allocator/tracking.rs
Normal file
|
@ -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<bool> = 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<FnvHashMap<usize, AllocSite>> =
|
||||||
|
const { Mutex::new(FnvHashMap::with_hasher(FnvBuildHasher::new())) };
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct AccountingAlloc<A = System> {
|
||||||
|
allocator: A,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A> AccountingAlloc<A> {
|
||||||
|
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<A: GlobalAlloc> GlobalAlloc for AccountingAlloc<A> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ log = { workspace = true }
|
||||||
profile_traits = { workspace = true }
|
profile_traits = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
servo_allocator = { path = "../allocator" }
|
||||||
servo_config = { path = "../config" }
|
servo_config = { path = "../config" }
|
||||||
time = { workspace = true }
|
time = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,7 @@ impl Profiler {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let _ = sender.send(MemoryReportResult { results });
|
let _ = sender.send(MemoryReportResult { results });
|
||||||
|
servo_allocator::dump_unmeasured();
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
ProfilerMsg::Exit => false,
|
ProfilerMsg::Exit => false,
|
||||||
|
|
|
@ -293,7 +293,7 @@ pub fn perform_memory_report<F: FnOnce(&mut MallocSizeOfOps)>(f: F) {
|
||||||
let seen_pointer = move |ptr| SEEN_POINTERS.with(|pointers| !pointers.borrow_mut().insert(ptr));
|
let seen_pointer = move |ptr| SEEN_POINTERS.with(|pointers| !pointers.borrow_mut().insert(ptr));
|
||||||
let mut ops = MallocSizeOfOps::new(
|
let mut ops = MallocSizeOfOps::new(
|
||||||
servo_allocator::usable_size,
|
servo_allocator::usable_size,
|
||||||
None,
|
servo_allocator::enclosing_size,
|
||||||
Some(Box::new(seen_pointer)),
|
Some(Box::new(seen_pointer)),
|
||||||
);
|
);
|
||||||
f(&mut ops);
|
f(&mut ops);
|
||||||
|
|
|
@ -121,7 +121,7 @@ headers = { workspace = true }
|
||||||
net = { path = "../../components/net" }
|
net = { path = "../../components/net" }
|
||||||
net_traits = { workspace = true }
|
net_traits = { workspace = true }
|
||||||
serde_json = { 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" }
|
servo_allocator = { path = "../../components/allocator" }
|
||||||
surfman = { workspace = true, features = ["sm-raw-window-handle-06", "sm-x11"] }
|
surfman = { workspace = true, features = ["sm-raw-window-handle-06", "sm-x11"] }
|
||||||
winit = { workspace = true }
|
winit = { workspace = true }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue