Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+622
View File
@@ -0,0 +1,622 @@
use std::collections::VecDeque;
use std::ffi::OsStr;
use byte_unit::Byte;
use chrono::{DateTime, Local, Utc};
use itertools::Itertools as _;
use num_traits::Zero;
use ordered_float::OrderedFloat;
use serde::Serialize;
use sysinfo::ProcessesToUpdate;
use warp_core::channel::ChannelState;
use warpui::{App, AppContext, Entity, ModelContext, SingletonEntity};
use crate::{
send_telemetry_from_app_ctx, send_telemetry_sync_from_ctx, server::telemetry,
system::memory_footprint, terminal::TerminalView, TelemetryEvent,
};
/// The threshold at which we emit a memory usage warning.
const MEMORY_USAGE_WARNING_THRESHOLD: Option<Byte> = byte_unit::Byte::GIGABYTE.multiply(10);
/// The refresh interval for system information, in seconds.
const REFRESH_INTERVAL_S: usize = 5;
/// The refresh interval for system information.
const REFRESH_INTERVAL: std::time::Duration =
std::time::Duration::from_secs(REFRESH_INTERVAL_S as u64);
/// The time window that a resource usage report covers, in seconds.
const REPORT_WINDOW_S: usize = 300;
/// The number of data points aggregated into a resource usage report.
const REPORT_SAMPLE_COUNT: usize = REPORT_WINDOW_S / REFRESH_INTERVAL_S;
// Make sure the refresh interval cleanly divides the report window into an
// integral number of samples.
static_assertions::const_assert_eq!(REPORT_WINDOW_S % REFRESH_INTERVAL_S, 0);
pub enum SystemInfoEvent {
/// There is new system info available for consumers to query.
Refreshed,
/// The application is using a large quantity of memory.
MemoryUsageHigh,
}
pub struct SystemInfo {
/// A structure we can use to efficiently query system information.
system: sysinfo::System,
/// Whether or not we've already emitted an event due to high memory usage.
has_emitted_memory_warning_event: bool,
/// A circular buffer storing resource usage data.
stats: StatsBuffer,
/// A helper structure for reporting resource usage via telemetry events.
resource_usage_reporter: ResourceUsageReporter,
/// The long OS version.
long_os_version: Option<String>,
}
impl SystemInfo {
/// Creates a new [`SystemInfo`] model and begins periodic fetching of
/// system information.
///
/// Currently only retrieves and exposes memory usage information for the
/// current process.
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
let mut me = Self {
system: sysinfo::System::new(),
has_emitted_memory_warning_event: false,
stats: Default::default(),
resource_usage_reporter: Default::default(),
long_os_version: sysinfo::System::long_os_version(),
};
// Initialize the underlying system info. This is necessary in order
// for our first read of CPU stats to be accurate, as they are computed
// as a delta between the previous refresh and now.
me.system.refresh_processes_specifics(
ProcessesToUpdate::Some(&[Self::current_pid()]),
false, /* refresh_dead_processes */
Self::refresh_kind(),
);
// If we're doing automated heap usage tracking, set up periodic
// refreshes of the memory usage data.
Self::schedule_refresh(ctx);
me
}
pub fn handle_block_created(&mut self) {
self.resource_usage_reporter.handle_block_created();
}
/// Returns the amount of memory being used by the current process, in
/// bytes.
pub fn used_memory(&self) -> Byte {
self.system
.process(Self::current_pid())
.expect("current process should exist")
.memory()
.into()
}
/// Returns the full memory footprint of the current process, in bytes.
///
/// Unlike [`used_memory`] (RSS), this includes memory that has been
/// swapped out or compressed by the OS. On macOS this matches the value
/// shown by Activity Monitor.
pub fn memory_footprint(&self) -> Byte {
memory_footprint::memory_footprint_bytes().into()
}
/// Returns the average CPU usage over the refresh interval.
///
/// If one CPU core is utilized at 100%, this will return 1. It may return
/// a value >1 on multi-core machines.
pub fn cpu_usage(&self) -> f32 {
let total_usage = self
.system
.process(Self::current_pid())
.expect("current process should exist")
.cpu_usage();
total_usage / 100.
}
pub fn long_os_version(&self) -> Option<&str> {
self.long_os_version.as_deref()
}
fn schedule_refresh(ctx: &mut ModelContext<Self>) {
ctx.spawn(
async {
warpui::r#async::Timer::after(REFRESH_INTERVAL).await;
},
|me, _, ctx| {
me.refresh(ctx);
Self::schedule_refresh(ctx);
},
);
}
fn refresh(&mut self, ctx: &mut ModelContext<Self>) {
self.system.refresh_processes_specifics(
ProcessesToUpdate::Some(&[Self::current_pid()]),
false, /* refresh_dead_processes */
Self::refresh_kind(),
);
ctx.emit(SystemInfoEvent::Refreshed);
// Add resource usage information to our circular buffer.
self.stats.push(Sample {
cpu: self.cpu_usage(),
});
let rss = self.used_memory();
let footprint = self.memory_footprint();
self.check_for_excessive_memory_usage(rss, footprint, ctx);
// Once we have a full buffer of statistics, consider sending a report
// each time we store new resource usage data.
if self.stats.is_full() {
self.resource_usage_reporter.maybe_send_report(ctx);
}
}
/// Checks for excessive memory usage. This may send a telemetry event
/// and trigger a Sentry heap profile dump if excessive usage is detected.
///
/// The threshold check uses `memory_footprint` (which includes swapped
/// and compressed pages) so we actually detect high memory situations.
/// The Rudderstack telemetry event still reports `rss` so existing
/// dashboards are unaffected.
fn check_for_excessive_memory_usage(
&mut self,
rss: Byte,
memory_footprint: Byte,
ctx: &mut ModelContext<Self>,
) {
if self.has_emitted_memory_warning_event {
return;
}
// Use footprint (not RSS) for the threshold so we catch memory
// that has been swapped out or compressed by the OS.
if memory_footprint
< MEMORY_USAGE_WARNING_THRESHOLD.expect("Threshold should not overflow u64")
{
return;
}
// Collect a detailed memory breakdown for diagnostics.
let memory_breakdown = memory_footprint::memory_breakdown();
// If we're tracking heap usage and detect excessive memory usage,
// dump and upload the current heap profiling data.
#[cfg(feature = "heap_usage_tracking")]
{
let breakdown_for_sentry = memory_breakdown.clone();
ctx.spawn(
crate::profiling::dump_jemalloc_heap_profile(breakdown_for_sentry),
|_, _, _| {},
);
}
// Send a telemetry event indicating that memory usage is extreme.
// Report RSS here to keep Rudderstack dashboards consistent.
let total_application_usage_bytes = rss.as_u64();
send_telemetry_sync_from_ctx!(
TelemetryEvent::MemoryUsageHigh {
total_application_usage_bytes,
memory_breakdown,
},
ctx
);
ctx.emit(SystemInfoEvent::MemoryUsageHigh);
self.has_emitted_memory_warning_event = true;
}
/// Returns the pid of the current process.
fn current_pid() -> sysinfo::Pid {
sysinfo::get_current_pid().expect("Platform should support process IDs")
}
/// Returns the [`sysinfo::ProcessRefreshKind`] that should be used when
/// retrieving information about the current process.
fn refresh_kind() -> sysinfo::ProcessRefreshKind {
sysinfo::ProcessRefreshKind::nothing()
.with_memory()
.with_cpu()
}
#[cfg_attr(not(windows), allow(dead_code))]
pub fn refresh_all_processes(&mut self) {
self.system.refresh_processes_specifics(
ProcessesToUpdate::All,
true, /* remove_dead_processes */
Self::refresh_kind(),
);
}
#[cfg_attr(not(windows), allow(dead_code))]
pub fn processes_by_name<'a>(
&'a self,
name: &'a str,
) -> impl Iterator<Item = &'a sysinfo::Process> {
self.system.processes_by_name(OsStr::new(name))
}
}
impl Entity for SystemInfo {
type Event = SystemInfoEvent;
}
impl SingletonEntity for SystemInfo {}
/// Helper structure for making resource usage reports.
struct ResourceUsageReporter {
/// The number of blocks created since we last reported on resource usage
/// statistics.
blocks_created_since_last_report: usize,
/// The time at which we sent the last report.
time_last_report_sent: DateTime<Utc>,
}
impl ResourceUsageReporter {
/// We won't produce a new report unless the user has created at least
/// this many blocks since the last one.
const MIN_BLOCKS_CREATED_PER_MEMORY_REPORT: usize = 5;
/// We won't produce a new report unless at least this much time has
/// passed since the last one.
const MIN_DURATION_BETWEEN_MEMORY_REPORTS: chrono::Duration = chrono::Duration::hours(1);
/// We won't produce a report unless the user has been active recently.
const USER_RECENTLY_ACTIVE_INTERVAL: chrono::Duration = chrono::Duration::minutes(5);
/// Handles creation of a block in a blocklist.
fn handle_block_created(&mut self) {
self.blocks_created_since_last_report += 1;
}
/// Sends a resource usage report if the required conditions are met.
fn maybe_send_report(&mut self, ctx: &mut ModelContext<SystemInfo>) {
if self.should_send_report() {
// Immediately set the time at which we sent the last report, to
// ensure we don't send two if it takes a little while to schedule
// the background task below.
self.time_last_report_sent = Utc::now();
// We do this in a task callback to ensure that all terminal views
// will be returned when iterating over the app context. Without
// this, we'll skip the active terminal view, as it has been
// removed from the app context temporarily in order to provide
// mutable access to it.
ctx.spawn(futures::future::ready(()), |me, _, ctx| {
me.refresh(ctx);
let total_application_usage = me.used_memory();
me.resource_usage_reporter.send_report(
total_application_usage,
me.stats.iter(),
ctx,
);
});
}
}
/// Returns whether or not it's time to generate a report.
fn should_send_report(&self) -> bool {
// Don't send reports too frequently.
if Utc::now().signed_duration_since(self.time_last_report_sent)
< Self::MIN_DURATION_BETWEEN_MEMORY_REPORTS
{
return false;
}
// If we don't know when the user was last active, don't send a report.
let Some(last_active_time) =
DateTime::<Utc>::from_timestamp(App::last_active_timestamp(), 0)
else {
return false;
};
// Don't send a report unless the user has been active recently.
if Utc::now().signed_duration_since(last_active_time) > Self::USER_RECENTLY_ACTIVE_INTERVAL
{
return false;
}
true
}
/// Sends a resource usage report.
fn send_report<'a>(
&mut self,
total_application_usage: Byte,
samples: impl Iterator<Item = &'a Sample>,
ctx: &mut AppContext,
) {
let cpu_usage_stats = Self::compute_cpu_usage_stats(samples);
let memory_usage_stats = Self::compute_memory_usage_stats(total_application_usage, ctx);
// We send two different events at the moment, as one contains general
// resource usage information, and one contains more detailed info
// about memory consumption caused by the blocklist.
//
// TODO(vorporeal): Clean up the memory usage one, either eliminating it
// or merging it into the general resource usage telemetry event.
send_telemetry_from_app_ctx!(
TelemetryEvent::ResourceUsageStats {
cpu: cpu_usage_stats.into(),
mem: memory_usage_stats.into(),
},
ctx
);
// Only send detailed memory usage reports in dogfood, for the time being.
if ChannelState::channel().is_dogfood() {
// Only send the detailed memory usage report if the user has created
// enough blocks since the last detailed memory usage report.
if self.blocks_created_since_last_report >= Self::MIN_BLOCKS_CREATED_PER_MEMORY_REPORT {
send_telemetry_from_app_ctx!(TelemetryEvent::from(memory_usage_stats), ctx);
self.blocks_created_since_last_report = 0;
}
}
}
fn compute_cpu_usage_stats<'a>(samples: impl Iterator<Item = &'a Sample>) -> CpuUsageStats {
let mut num_samples = 0;
let mut avg_usage = 0.;
let mut max_usage = OrderedFloat::zero();
for sample in samples {
num_samples += 1;
avg_usage += sample.cpu;
max_usage = std::cmp::max(max_usage, sample.cpu.into());
}
avg_usage /= num_samples as f32;
let num_cpus = num_cpus::get();
CpuUsageStats {
num_cpus,
avg_usage,
max_usage: max_usage.into_inner(),
}
}
fn compute_memory_usage_stats(
total_application_usage: Byte,
ctx: &mut AppContext,
) -> MemoryUsageStats {
let mut stats = MemoryUsageStats::new(total_application_usage);
// Don't compute detailed memory usage statistics outside of debug builds.
if !ChannelState::enable_debug_features() {
return stats;
}
let now = Local::now();
// Loop over all terminal views, collecting information about how
// many blocks they contain, number of lines, amount of memory,
// and the active/inactive breakdown.
for window_id in ctx.window_ids().collect_vec() {
for terminal_view in ctx
.views_of_type::<TerminalView>(window_id)
.into_iter()
.flatten()
.map(|handle| handle.as_ref(ctx))
{
let model = terminal_view.model.lock();
stats.add_blocks(now, model.block_list().blocks().iter());
}
}
stats
}
}
impl Default for ResourceUsageReporter {
fn default() -> Self {
Self {
blocks_created_since_last_report: 0,
time_last_report_sent: DateTime::UNIX_EPOCH,
}
}
}
/// Statistics about CPU usage.
struct CpuUsageStats {
/// The number of "CPUs" on the machine. This actually measure the number
/// of _logical_ CPUs, i.e.: CPU cores (including SMT pseudo-cores).
num_cpus: usize,
/// The maximum CPU usage over the measurement interval, represented as a
/// value in the range [0, num_cpus].
max_usage: f32,
/// The average CPU usage over the measurement interval, represented as a
/// value in the range [0, num_cpus].
avg_usage: f32,
}
impl From<CpuUsageStats> for telemetry::CpuUsageStats {
fn from(value: CpuUsageStats) -> Self {
Self {
num_cpus: value.num_cpus,
max_usage: value.max_usage,
avg_usage: value.avg_usage,
}
}
}
#[derive(Copy, Clone)]
struct MemoryUsageStats {
total_application_usage_bytes: usize,
total_blocks: usize,
total_lines: usize,
/// Statistics about blocks that have been seen in the past 5 minutes.
active_block_stats: BlockMemoryStats,
/// Statistics about blocks that haven't been seen since [5m, 1h).
inactive_5m_stats: BlockMemoryStats,
/// Statistics about blocks that haven't been seen since [1h, 24h).
inactive_1h_stats: BlockMemoryStats,
/// Statistics about blocks that haven't been seen since [24h, ..).
inactive_24h_stats: BlockMemoryStats,
}
impl MemoryUsageStats {
fn new(total_application_usage: Byte) -> Self {
Self {
total_application_usage_bytes: total_application_usage.as_u64() as usize,
total_blocks: 0,
total_lines: 0,
active_block_stats: Default::default(),
inactive_5m_stats: Default::default(),
inactive_1h_stats: Default::default(),
inactive_24h_stats: Default::default(),
}
}
fn add_blocks<'a>(
&mut self,
now: DateTime<Local>,
blocks: impl Iterator<Item = &'a crate::terminal::model::block::Block>,
) {
// We compute block-related memory stats across various intervals.
// "Activity" refers to how recently the block was painted.
const DURATION_5M: chrono::Duration = chrono::Duration::minutes(5);
const DURATION_1H: chrono::Duration = chrono::Duration::hours(1);
const DURATION_24H: chrono::Duration = chrono::Duration::hours(24);
for block in blocks {
let num_lines: usize = block.all_grids_iter().map(|grid| grid.len()).sum();
self.total_blocks += 1;
self.total_lines += num_lines;
let last_painted_at = block
.last_painted_at()
.unwrap_or(DateTime::UNIX_EPOCH.into());
let stats = match now - last_painted_at {
duration if duration < DURATION_5M => &mut self.active_block_stats,
duration if duration < DURATION_1H => &mut self.inactive_5m_stats,
duration if duration < DURATION_24H => &mut self.inactive_1h_stats,
_ => &mut self.inactive_24h_stats,
};
stats.num_blocks += 1;
stats.num_lines += num_lines;
stats.estimated_memory_usage_bytes += block.estimated_memory_usage_bytes();
}
}
}
impl From<MemoryUsageStats> for TelemetryEvent {
fn from(value: MemoryUsageStats) -> Self {
TelemetryEvent::MemoryUsageStats {
total_application_usage_bytes: value.total_application_usage_bytes,
total_blocks: value.total_blocks,
total_lines: value.total_lines,
active_block_stats: value.active_block_stats.into(),
inactive_5m_stats: value.inactive_5m_stats.into(),
inactive_1h_stats: value.inactive_1h_stats.into(),
inactive_24h_stats: value.inactive_24h_stats.into(),
}
}
}
impl From<MemoryUsageStats> for telemetry::MemoryUsageStats {
fn from(value: MemoryUsageStats) -> Self {
Self {
total_application_usage_bytes: value.total_application_usage_bytes,
total_blocks: value.total_blocks,
total_lines: value.total_lines,
active_block_stats: value.active_block_stats.into(),
inactive_5m_stats: value.inactive_5m_stats.into(),
inactive_1h_stats: value.inactive_1h_stats.into(),
inactive_24h_stats: value.inactive_24h_stats.into(),
}
}
}
#[derive(Copy, Clone, Default, Serialize, PartialEq)]
struct BlockMemoryStats {
num_blocks: usize,
num_lines: usize,
estimated_memory_usage_bytes: usize,
}
impl std::fmt::Debug for BlockMemoryStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BlockMemoryStats")
.field("num_blocks", &self.num_blocks)
.field("num_lines", &self.num_lines)
.field(
"estimated_memory_usage_bytes",
&byte_unit::Byte::from(self.estimated_memory_usage_bytes)
.get_adjusted_unit(byte_unit::Unit::MB),
)
.finish()
}
}
impl From<BlockMemoryStats> for telemetry::BlockMemoryUsageStats {
fn from(value: BlockMemoryStats) -> Self {
Self {
num_blocks: value.num_blocks,
num_lines: value.num_lines,
estimated_memory_usage_bytes: value.estimated_memory_usage_bytes,
}
}
}
/// A single resource usage sample point.
struct Sample {
/// The CPU usage since the last sample, represented as a value in the
/// range [0, num_cpus].
cpu: f32,
}
/// A simple fixed-size circular buffer for storing resource usage sample
/// points.
struct StatsBuffer {
stats: VecDeque<Sample>,
}
impl StatsBuffer {
/// Constructs a new [`StatsBuffer`].
fn new() -> Self {
Self {
stats: VecDeque::with_capacity(REPORT_SAMPLE_COUNT),
}
}
/// Returns whether or not the buffer is full of samples.
///
/// If true, adding a sample will replace the oldest sample in the buffer.
fn is_full(&self) -> bool {
self.stats.len() == self.stats.capacity()
}
/// Pushes a new sample into the buffer. If the buffer is at capacity,
/// the oldest sample will be removed to make room for the new one.
fn push(&mut self, sample: Sample) {
if self.is_full() {
self.stats.pop_front();
}
self.stats.push_back(sample);
}
/// Returns an iterator over all samples in the buffer.
fn iter(&self) -> impl Iterator<Item = &Sample> {
self.stats.iter()
}
}
impl Default for StatsBuffer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "info_tests.rs"]
mod tests;
+56
View File
@@ -0,0 +1,56 @@
use byte_unit::Byte;
use crate::{terminal::model::test_utils::TestBlockBuilder, test_util::mock_blockgrid};
use super::*;
#[test]
fn test_memory_usage_stats_construction() {
let total_application_usage_bytes = 1024;
let mut stats = MemoryUsageStats::new(Byte::from_u64(total_application_usage_bytes));
let now = Local::now();
let mut block_with_content = TestBlockBuilder::new().build();
block_with_content.set_prompt_and_command_grid(mock_blockgrid("line1\nline2"));
block_with_content.set_output_grid(mock_blockgrid("line3"));
block_with_content.update_last_painted_at(now);
let inactive_5m_block = TestBlockBuilder::new().build();
inactive_5m_block.update_last_painted_at(now - chrono::Duration::minutes(10));
let inactive_1h_block1 = TestBlockBuilder::new().build();
inactive_1h_block1.update_last_painted_at(now - chrono::Duration::minutes(70));
let inactive_1h_block2 = TestBlockBuilder::new().build();
inactive_1h_block2.update_last_painted_at(now - chrono::Duration::minutes(70));
let blocks = [
block_with_content,
inactive_5m_block,
inactive_1h_block1,
inactive_1h_block2,
TestBlockBuilder::new().build(),
];
stats.add_blocks(now, blocks.iter());
assert_eq!(
stats.total_application_usage_bytes,
total_application_usage_bytes as usize
);
assert_eq!(stats.total_blocks, 5);
assert_eq!(stats.total_lines, 3);
assert_eq!(stats.active_block_stats.num_blocks, 1);
assert_eq!(stats.active_block_stats.num_lines, 3);
assert_eq!(stats.inactive_5m_stats.num_blocks, 1);
assert_eq!(stats.inactive_5m_stats.num_lines, 0);
assert_eq!(stats.inactive_1h_stats.num_blocks, 2);
assert_eq!(stats.inactive_1h_stats.num_lines, 0);
assert_eq!(stats.inactive_24h_stats.num_blocks, 1);
assert_eq!(stats.inactive_24h_stats.num_lines, 0);
}
+211
View File
@@ -0,0 +1,211 @@
/// Returns the full memory footprint of the current process, in bytes.
///
/// Unlike RSS (resident set size), this includes memory that has been swapped
/// out or compressed by the OS. On macOS, this returns `phys_footprint` from
/// `task_info(TASK_VM_INFO)`, which is the same value displayed by Activity
/// Monitor.
pub fn memory_footprint_bytes() -> u64 {
platform::memory_footprint_bytes()
}
/// Returns a platform-specific JSON object with a detailed breakdown of the
/// current process's memory usage.
///
/// Each platform populates whichever fields it can natively provide. The
/// returned value is an opaque JSON blob suitable for attaching to Sentry
/// events and telemetry payloads.
pub fn memory_breakdown() -> serde_json::Value {
platform::memory_breakdown()
}
// ---------------------------------------------------------------------------
// macOS
// ---------------------------------------------------------------------------
#[cfg(target_os = "macos")]
mod platform {
use std::mem;
use mach2::kern_return::KERN_SUCCESS;
use mach2::task::task_info;
use mach2::task_info::{task_vm_info, TASK_VM_INFO};
use mach2::traps::mach_task_self;
/// Calls `task_info(TASK_VM_INFO)` and returns the populated struct on
/// success, or `None` if the call fails.
fn query_task_vm_info() -> Option<task_vm_info> {
// SAFETY: We zero-initialise the struct and pass its exact size to the
// kernel. `task_info` writes into the struct up to `count` natural
// ints and returns `KERN_SUCCESS` on success.
unsafe {
let mut info: task_vm_info = mem::zeroed();
let mut count = (mem::size_of::<task_vm_info>() / mem::size_of::<i32>()) as u32;
let kr = task_info(
mach_task_self(),
TASK_VM_INFO,
&mut info as *mut _ as *mut i32,
&mut count,
);
if kr == KERN_SUCCESS {
Some(info)
} else {
None
}
}
}
pub fn memory_footprint_bytes() -> u64 {
query_task_vm_info()
.map(|info| info.phys_footprint)
.unwrap_or(0)
}
pub fn memory_breakdown() -> serde_json::Value {
let Some(info) = query_task_vm_info() else {
return serde_json::json!({});
};
// Copy fields out of the packed struct into locals to avoid
// unaligned references (task_vm_info is repr(C, packed(4))).
let total_footprint = info.phys_footprint;
let resident = info.resident_size;
let compressed = info.compressed;
let internal = info.internal;
let device = info.device;
let gpu_memory = info.ledger_tag_graphics_footprint;
let gpu_memory_compressed = info.ledger_tag_graphics_footprint_compressed;
let media_memory = info.ledger_tag_media_footprint;
let neural_memory = info.ledger_tag_neural_footprint;
let purgeable = info.ledger_purgeable_nonvolatile;
serde_json::json!({
"total_footprint": total_footprint,
"resident": resident,
"compressed": compressed,
"internal": internal,
"device": device,
"gpu_memory": gpu_memory,
"gpu_memory_compressed": gpu_memory_compressed,
"media_memory": media_memory,
"neural_memory": neural_memory,
"purgeable": purgeable,
})
}
}
// ---------------------------------------------------------------------------
// Linux
// ---------------------------------------------------------------------------
#[cfg(target_os = "linux")]
mod platform {
/// Reads `/proc/self/status` and sums `VmRSS` + `VmSwap` to approximate
/// the full memory footprint (resident + swapped).
pub fn memory_footprint_bytes() -> u64 {
read_proc_self_status().unwrap_or(0)
}
fn read_proc_self_status() -> Option<u64> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let mut rss_kb: u64 = 0;
let mut swap_kb: u64 = 0;
for line in status.lines() {
if let Some(value) = line.strip_prefix("VmRSS:") {
rss_kb = parse_kb(value);
} else if let Some(value) = line.strip_prefix("VmSwap:") {
swap_kb = parse_kb(value);
}
}
Some((rss_kb + swap_kb) * 1024)
}
fn parse_kb(s: &str) -> u64 {
// Lines look like "VmRSS: 12345 kB"
s.split_whitespace()
.next()
.and_then(|v| v.parse().ok())
.unwrap_or(0)
}
pub fn memory_breakdown() -> serde_json::Value {
let Ok(status) = std::fs::read_to_string("/proc/self/status") else {
return serde_json::json!({});
};
let mut result = serde_json::Map::new();
for line in status.lines() {
let (key, value) = if let Some(v) = line.strip_prefix("VmRSS:") {
("vm_rss", v)
} else if let Some(v) = line.strip_prefix("VmSwap:") {
("vm_swap", v)
} else if let Some(v) = line.strip_prefix("VmSize:") {
("vm_size", v)
} else {
continue;
};
result.insert(
key.to_string(),
serde_json::Value::Number((parse_kb(value) * 1024).into()),
);
}
serde_json::Value::Object(result)
}
}
// ---------------------------------------------------------------------------
// Windows
// ---------------------------------------------------------------------------
#[cfg(target_os = "windows")]
mod platform {
use std::mem;
use windows::Win32::System::ProcessStatus::{K32GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
use windows::Win32::System::Threading::GetCurrentProcess;
#[repr(C)]
struct ProcessMemoryCountersEx {
base: PROCESS_MEMORY_COUNTERS,
private_usage: usize,
}
/// Uses `GetProcessMemoryInfo` to read `PrivateUsage` from
/// `PROCESS_MEMORY_COUNTERS_EX`, which accounts for private committed
/// memory (resident + paged out).
///
/// The `windows` crate doesn't expose `PROCESS_MEMORY_COUNTERS_EX`
/// directly, but it is layout-compatible with `PROCESS_MEMORY_COUNTERS`
/// plus one trailing `usize` field (`PrivateUsage`). We define a minimal
/// wrapper to read that field.
pub fn memory_footprint_bytes() -> u64 {
query_counters()
.map(|c| c.private_usage as u64)
.unwrap_or(0)
}
fn query_counters() -> Option<ProcessMemoryCountersEx> {
// SAFETY: `GetCurrentProcess` returns a pseudo-handle that does not
// need to be closed. `K32GetProcessMemoryInfo` writes into the
// provided struct up to `cb` bytes.
unsafe {
let handle = GetCurrentProcess();
let mut counters: ProcessMemoryCountersEx = mem::zeroed();
counters.base.cb = mem::size_of::<ProcessMemoryCountersEx>() as u32;
if K32GetProcessMemoryInfo(handle, &mut counters.base, counters.base.cb).as_bool() {
Some(counters)
} else {
None
}
}
}
pub fn memory_breakdown() -> serde_json::Value {
let Some(counters) = query_counters() else {
return serde_json::json!({});
};
serde_json::json!({
"working_set": counters.base.WorkingSetSize,
"private_usage": counters.private_usage,
"peak_working_set": counters.base.PeakWorkingSetSize,
})
}
}