servo/components/shared/profile/mem.rs
Josh Matthews ba8f923201
Various memory measurement improvements (#36834)
The two significant changes here are 1) a commit that frees memory used
to perform memory reporting once the reporting is complete, 2) memory
reporting for the system font service. There are various other commits
that remove `#[ignore_malloc_size_of]` attributes for data that we are
now able to measure, but they do not significantly change our
measurements when testing servo.org.

Testing: Comparing the output of about:memory on servo.org.

---------

Signed-off-by: Josh Matthews <josh@joshmatthews.net>
2025-05-07 04:00:12 +00:00

294 lines
9.7 KiB
Rust

/* 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/. */
//! APIs for memory profiling.
#![deny(missing_docs)]
use std::cell::{LazyCell, RefCell};
use std::collections::HashSet;
use std::ffi::c_void;
use std::marker::Send;
use crossbeam_channel::Sender;
use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::router::ROUTER;
use log::warn;
use malloc_size_of::MallocSizeOfOps;
use serde::{Deserialize, Serialize};
/// A trait to abstract away the various kinds of message senders we use.
pub trait OpaqueSender<T> {
/// Send a message.
fn send(&self, message: T);
}
impl<T> OpaqueSender<T> for Sender<T> {
fn send(&self, message: T) {
if let Err(e) = Sender::send(self, message) {
warn!(
"Error communicating with the target thread from the profiler: {:?}",
e
);
}
}
}
impl<T> OpaqueSender<T> for IpcSender<T>
where
T: serde::Serialize,
{
fn send(&self, message: T) {
if let Err(e) = IpcSender::send(self, message) {
warn!(
"Error communicating with the target thread from the profiler: {}",
e
);
}
}
}
/// Front-end representation of the profiler used to communicate with the
/// profiler.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ProfilerChan(pub IpcSender<ProfilerMsg>);
/// A handle that encompasses a registration with the memory profiler.
/// The registration is tied to the lifetime of this type; the memory
/// profiler unregister the reporter when this object is dropped.
pub struct ProfilerRegistration {
sender: ProfilerChan,
reporter_name: String,
}
impl Drop for ProfilerRegistration {
fn drop(&mut self) {
self.sender
.send(ProfilerMsg::UnregisterReporter(self.reporter_name.clone()));
}
}
impl ProfilerChan {
/// Send `msg` on this `IpcSender`.
///
/// Warns if the send fails.
pub fn send(&self, msg: ProfilerMsg) {
if let Err(e) = self.0.send(msg) {
warn!("Error communicating with the memory profiler thread: {}", e);
}
}
/// Register a new reporter and return a handle to automatically
/// unregister it in the future.
pub fn prepare_memory_reporting<M, T, C>(
&self,
reporter_name: String,
channel_for_reporter: C,
msg: M,
) -> ProfilerRegistration
where
M: Fn(ReportsChan) -> T + Send + 'static,
T: Send + 'static,
C: OpaqueSender<T> + Send + 'static,
{
// Register the memory reporter.
let (reporter_sender, reporter_receiver) = ipc::channel().unwrap();
ROUTER.add_typed_route(
reporter_receiver,
Box::new(move |message| {
// Just injects an appropriate event into the paint thread's queue.
let request: ReporterRequest = message.unwrap();
channel_for_reporter.send(msg(request.reports_channel));
}),
);
self.send(ProfilerMsg::RegisterReporter(
reporter_name.clone(),
Reporter(reporter_sender),
));
ProfilerRegistration {
sender: self.clone(),
reporter_name,
}
}
/// Runs `f()` with memory profiling.
pub fn run_with_memory_reporting<F, M, T, C>(
&self,
f: F,
reporter_name: String,
channel_for_reporter: C,
msg: M,
) where
F: FnOnce(),
M: Fn(ReportsChan) -> T + Send + 'static,
T: Send + 'static,
C: OpaqueSender<T> + Send + 'static,
{
let _registration = self.prepare_memory_reporting(reporter_name, channel_for_reporter, msg);
f();
}
}
/// The various kinds of memory measurement.
///
/// Here "explicit" means explicit memory allocations done by the application. It includes
/// allocations made at the OS level (via functions such as VirtualAlloc, vm_allocate, and mmap),
/// allocations made at the heap allocation level (via functions such as malloc, calloc, realloc,
/// memalign, operator new, and operator new[]) and where possible, the overhead of the heap
/// allocator itself. It excludes memory that is mapped implicitly such as code and data segments,
/// and thread stacks. "explicit" is not guaranteed to cover every explicit allocation, but it does
/// cover most (including the entire heap), and therefore it is the single best number to focus on
/// when trying to reduce memory usage.
#[derive(Debug, Deserialize, Serialize)]
pub enum ReportKind {
/// A size measurement for an explicit allocation on the jemalloc heap. This should be used
/// for any measurements done via the `MallocSizeOf` trait.
ExplicitJemallocHeapSize,
/// A size measurement for an explicit allocation on the system heap. Only likely to be used
/// for external C or C++ libraries that don't use jemalloc.
ExplicitSystemHeapSize,
/// A size measurement for an explicit allocation not on the heap, e.g. via mmap().
ExplicitNonHeapSize,
/// A size measurement for an explicit allocation whose location is unknown or uncertain.
ExplicitUnknownLocationSize,
/// A size measurement for a non-explicit allocation. This kind is used for global
/// measurements such as "resident" and "vsize", and also for measurements that cross-cut the
/// measurements grouped under "explicit", e.g. by grouping those measurements in a way that's
/// different to how they are grouped under "explicit".
NonExplicitSize,
}
/// A single memory-related measurement.
#[derive(Debug, Deserialize, Serialize)]
pub struct Report {
/// The identifying path for this report.
pub path: Vec<String>,
/// The report kind.
pub kind: ReportKind,
/// The size, in bytes.
pub size: usize,
}
/// A set of reports belonging to a process.
#[derive(Debug, Deserialize, Serialize)]
pub struct ProcessReports {
/// The set of reports.
pub reports: Vec<Report>,
/// The process id.
pub pid: u32,
}
impl ProcessReports {
/// Adopt these reports and configure the process pid.
pub fn new(reports: Vec<Report>) -> Self {
Self {
reports,
pid: std::process::id(),
}
}
}
/// A channel through which memory reports can be sent.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ReportsChan(pub IpcSender<ProcessReports>);
impl ReportsChan {
/// Send `report` on this `IpcSender`.
///
/// Panics if the send fails.
pub fn send(&self, reports: ProcessReports) {
self.0.send(reports).unwrap();
}
}
/// The protocol used to send reporter requests.
#[derive(Debug, Deserialize, Serialize)]
pub struct ReporterRequest {
/// The channel on which reports are to be sent.
pub reports_channel: ReportsChan,
}
/// A memory reporter is capable of measuring some data structure of interest. It's structured as
/// an IPC sender that a `ReporterRequest` in transmitted over. `ReporterRequest` objects in turn
/// encapsulate the channel on which the memory profiling information is to be sent.
///
/// In many cases, clients construct `Reporter` objects by creating an IPC sender/receiver pair and
/// registering the receiving end with the router so that messages from the memory profiler end up
/// injected into the client's event loop.
#[derive(Debug, Deserialize, Serialize)]
pub struct Reporter(pub IpcSender<ReporterRequest>);
impl Reporter {
/// Collect one or more memory reports. Returns true on success, and false on failure.
pub fn collect_reports(&self, reports_channel: ReportsChan) {
self.0.send(ReporterRequest { reports_channel }).unwrap()
}
}
/// An easy way to build a path for a report.
#[macro_export]
macro_rules! path {
($($x:expr),*) => {{
use std::borrow::ToOwned;
vec![$( $x.to_owned() ),*]
}}
}
/// The results produced by the memory reporter.
#[derive(Debug, Deserialize, Serialize)]
pub struct MemoryReportResult {
/// The stringified output.
pub content: String,
}
/// Messages that can be sent to the memory profiler thread.
#[derive(Debug, Deserialize, Serialize)]
pub enum ProfilerMsg {
/// Register a Reporter with the memory profiler. The String is only used to identify the
/// reporter so it can be unregistered later. The String must be distinct from that used by any
/// other registered reporter otherwise a panic will occur.
RegisterReporter(String, Reporter),
/// Unregister a Reporter with the memory profiler. The String must match the name given when
/// the reporter was registered. If the String does not match the name of a registered reporter
/// a panic will occur.
UnregisterReporter(String),
/// Tells the memory profiler to shut down.
Exit,
/// Triggers sending back the memory profiling metrics,
Report(IpcSender<MemoryReportResult>),
}
thread_local!(static SEEN_POINTERS: LazyCell<RefCell<HashSet<*const c_void>>> = const {
LazyCell::new(Default::default)
});
/// Invoke the provided function after initializing the memory profile tools.
/// The function is expected to call all the desired [MallocSizeOf::size_of]
/// for allocations reachable from the current thread.
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 mut ops = MallocSizeOfOps::new(
servo_allocator::usable_size,
None,
Some(Box::new(seen_pointer)),
);
f(&mut ops);
SEEN_POINTERS.with(|pointers| {
let mut pointers = pointers.borrow_mut();
pointers.clear();
pointers.shrink_to_fit();
});
}