mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #11181 from Slavik-85/f3-overlay-light
F3 overlay: paint at 20 Hz, no per-frame OS calls, less GPU timing
This commit is contained in:
8 files changed
+250
-39
No files matched your search
@@ -1392,6 +1392,8 @@ pub struct Renderer {
|
||||
native: Option<native_pack::NativePack>,
|
||||
/// Time the shader pack's passes on the GPU (F3, or `HORIZON_SHADER_PACK_TIMING`).
|
||||
pass_timing: bool,
|
||||
/// Least time between timed frames (`set_pass_timing_gap`).
|
||||
pass_timing_gap: std::time::Duration,
|
||||
/// Times Horizon's own passes on the GPU, as `pass_timing` does (`own_pass_times`).
|
||||
own_timing: Option<pack_timing::PassTiming>,
|
||||
/// The entity vertices Horizon draws itself while a shader pack draws the others.
|
||||
@@ -4742,6 +4744,7 @@ impl Renderer {
|
||||
pack_loading: None,
|
||||
native: None,
|
||||
pass_timing: pack_timing::PassTiming::logging(),
|
||||
pass_timing_gap: std::time::Duration::ZERO,
|
||||
own_timing: None,
|
||||
pack_entity_rest: Vec::new(),
|
||||
cloud_bindings,
|
||||
@@ -6826,6 +6829,15 @@ impl Renderer {
|
||||
pub fn set_pass_timing(&mut self, on: bool) {
|
||||
self.pass_timing = on || pack_timing::PassTiming::logging();
|
||||
}
|
||||
/// Times a frame at most once per `gap` while passes are timed (the F3 overlay needs far fewer
|
||||
/// samples than a measurement). `HORIZON_SHADER_PACK_TIMING` keeps every sample.
|
||||
pub fn set_pass_timing_gap(&mut self, gap: std::time::Duration) {
|
||||
self.pass_timing_gap = if pack_timing::PassTiming::logging() {
|
||||
std::time::Duration::ZERO
|
||||
} else {
|
||||
gap
|
||||
};
|
||||
}
|
||||
/// The running shader pack's timer, of either kind.
|
||||
fn pass_timer<'a>(
|
||||
pack: &'a mut Option<(i32, Option<pack_post::PackPost>)>,
|
||||
@@ -7401,6 +7413,7 @@ impl Renderer {
|
||||
.flatten();
|
||||
}
|
||||
if let Some(t) = &mut self.own_timing {
|
||||
t.gap = self.pass_timing_gap;
|
||||
t.begin(&self.device, &self.queue);
|
||||
}
|
||||
if self.animated_textures {
|
||||
@@ -9038,6 +9051,9 @@ impl Renderer {
|
||||
.then(|| pack_timing::PassTiming::new(&self.device, &self.queue))
|
||||
.flatten();
|
||||
}
|
||||
if let Some(Some(t)) = Self::pass_timer(&mut self.pack_post, &mut self.native) {
|
||||
t.gap = self.pass_timing_gap;
|
||||
}
|
||||
let pack_reads_world = pack_post || native_post;
|
||||
laps.lap("r_uploads");
|
||||
// Phase 2 of occlusion culling draws over the depth, and a shader pack or TAA reads it.
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
//! `HORIZON_SHADER_PACK_TIMING=1`. One frame
|
||||
//! at a time is timed, and the CPU never waits for it: its timestamps are resolved in a later
|
||||
//! frame, once the GPU has finished it (Metal resolves them without waiting for the passes that
|
||||
//! write them), then read back when mapped. The next frame after that is timed again.
|
||||
//! write them), then read back when mapped. The next frame after that is timed again, once `gap`
|
||||
//! has passed since the last.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const REPORT: usize = 30;
|
||||
/// Passes timed per frame at most (two timestamps each).
|
||||
@@ -43,6 +45,10 @@ pub struct PassTiming {
|
||||
/// `mark`'s pipeline, for the target format it was made for.
|
||||
marker: Option<(wgpu::TextureFormat, wgpu::RenderPipeline)>,
|
||||
reports: usize,
|
||||
/// Least time between timed frames.
|
||||
pub gap: Duration,
|
||||
/// When the last timed frame began.
|
||||
timed_at: Option<Instant>,
|
||||
}
|
||||
|
||||
impl PassTiming {
|
||||
@@ -81,6 +87,8 @@ impl PassTiming {
|
||||
log: Self::logging(),
|
||||
marker: None,
|
||||
reports: 0,
|
||||
gap: Duration::ZERO,
|
||||
timed_at: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,9 +131,12 @@ impl PassTiming {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if matches!(self.stage, Stage::Idle) {
|
||||
if matches!(self.stage, Stage::Idle)
|
||||
&& self.timed_at.is_none_or(|at| at.elapsed() >= self.gap)
|
||||
{
|
||||
self.names.clear();
|
||||
self.stage = Stage::Recording;
|
||||
self.timed_at = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -308,12 +308,13 @@ impl DebugEntries {
|
||||
pub fn is_using_profile(&self, profile: Profile) -> bool {
|
||||
self.profile == Some(profile)
|
||||
}
|
||||
/// `getStatus`: never for an entry the statuses leave out.
|
||||
/// `getStatus`: never for an entry the statuses leave out. The debug renderers ask every
|
||||
/// frame, so no identifier is built for the lookup.
|
||||
pub fn status(&self, path: &str) -> Status {
|
||||
self.statuses
|
||||
.get(&identifier(path))
|
||||
.copied()
|
||||
.unwrap_or(Status::Never)
|
||||
.iter()
|
||||
.find(|(id, _)| id.strip_prefix("minecraft:") == Some(path))
|
||||
.map_or(Status::Never, |(_, status)| *status)
|
||||
}
|
||||
/// `setStatus`: the statuses are custom from now on, and saved.
|
||||
pub fn set_status(&mut self, path: &str, status: Status) {
|
||||
@@ -343,11 +344,13 @@ impl DebugEntries {
|
||||
};
|
||||
shown && entry(path).is_some_and(|e| e.reduced || !reduced)
|
||||
}
|
||||
/// `getCurrentlyEnabled().isEmpty()` negated: something to draw.
|
||||
/// `getCurrentlyEnabled().isEmpty()` negated: something to draw. Asked every frame, F3 closed
|
||||
/// or not, so it walks the few statuses kept rather than every entry.
|
||||
pub fn any_enabled(&self, overlay: bool, reduced: bool) -> bool {
|
||||
ENTRIES
|
||||
.iter()
|
||||
.any(|e| self.enabled(e.path, overlay, reduced))
|
||||
self.statuses
|
||||
.keys()
|
||||
.filter_map(|id| id.strip_prefix("minecraft:"))
|
||||
.any(|path| self.enabled(path, overlay, reduced))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
//! them last, over any screen, at the debug GUI scale.
|
||||
use crate::{
|
||||
font::Font,
|
||||
hud::HudVertex,
|
||||
hud_layout::{GuiDraw, PoseOp, fill},
|
||||
java_text,
|
||||
text::Text,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// `extractLines`'s background: ARGB 0x90505050.
|
||||
const BACKGROUND: u32 = 0x9050_5050;
|
||||
@@ -15,6 +17,39 @@ const TEXT: u32 = 0xFFE0_E0E0;
|
||||
/// `Font.lineHeight`.
|
||||
const LINE_HEIGHT: i32 = 9;
|
||||
|
||||
/// How long the painted overlay is reused: 20 times a second, the tick rate most of its lines
|
||||
/// change at or below (fps once a second, memory twice).
|
||||
const REPAINT: Duration = Duration::from_millis(50);
|
||||
/// Least time between frames timed for the overlay's GPU lines. Their medians still span about
|
||||
/// 1.5 s of samples.
|
||||
pub const GPU_TIMING_GAP: Duration = Duration::from_millis(100);
|
||||
|
||||
/// The overlay's painted vertices between rebuilds: copying them is far cheaper than formatting
|
||||
/// its lines and laying out their glyphs each frame.
|
||||
#[derive(Default)]
|
||||
pub struct PaintCache {
|
||||
vertices: Vec<HudVertex>,
|
||||
/// When they were painted, and for what layout (see [`PaintCache::fresh`]).
|
||||
painted: Option<(Instant, PaintLayout)>,
|
||||
}
|
||||
|
||||
/// What the painted vertices depend on besides the lines: the framebuffer size, the GUI scale's
|
||||
/// bits and `Font::epoch` (glyphs move in the atlas when it changes).
|
||||
pub type PaintLayout = ([u32; 2], u32, u64);
|
||||
|
||||
impl PaintCache {
|
||||
/// The vertices, while they are recent and painted for this `layout`.
|
||||
pub fn fresh(&self, now: Instant, layout: PaintLayout) -> Option<&[HudVertex]> {
|
||||
let (at, painted) = self.painted?;
|
||||
(painted == layout && now.duration_since(at) < REPAINT).then_some(&self.vertices)
|
||||
}
|
||||
pub fn store(&mut self, now: Instant, layout: PaintLayout, vertices: &[HudVertex]) {
|
||||
self.vertices.clear();
|
||||
self.vertices.extend_from_slice(vertices);
|
||||
self.painted = Some((now, layout));
|
||||
}
|
||||
}
|
||||
|
||||
/// What the enabled entries reported (`DebugScreenDisplayer`).
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Displayer {
|
||||
@@ -422,8 +457,9 @@ pub fn light_line(sky: u8, block: u8) -> String {
|
||||
}
|
||||
|
||||
/// What the process holds, for `DebugEntryMemory` (Horizon has no JVM heap):
|
||||
/// anonymous resident memory for the heap's used part, all resident memory
|
||||
/// for its committed part, the machine's memory for its maximum.
|
||||
/// anonymous (on Windows, private) resident memory for the heap's used part,
|
||||
/// all resident memory for its committed part, the machine's memory for its
|
||||
/// maximum.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ProcessMemory {
|
||||
pub used: u64,
|
||||
@@ -432,14 +468,20 @@ pub struct ProcessMemory {
|
||||
}
|
||||
|
||||
impl ProcessMemory {
|
||||
/// From `/proc/self/status` and `/proc/meminfo`; `None` elsewhere.
|
||||
/// From `/proc/self/status` and `/proc/meminfo` on Linux, the process's
|
||||
/// counters on Windows; `None` elsewhere.
|
||||
pub fn read() -> Option<Self> {
|
||||
if !cfg!(target_os = "linux") {
|
||||
return None;
|
||||
if cfg!(target_os = "linux") {
|
||||
let status = std::fs::read_to_string("/proc/self/status").ok()?;
|
||||
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
|
||||
return Self::parse(&status, &meminfo);
|
||||
}
|
||||
let status = std::fs::read_to_string("/proc/self/status").ok()?;
|
||||
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
|
||||
Self::parse(&status, &meminfo)
|
||||
let [used, allocated, max] = horizon_threads::process_memory::usage()?;
|
||||
Some(Self {
|
||||
used,
|
||||
allocated,
|
||||
max: max.max(1),
|
||||
})
|
||||
}
|
||||
fn parse(status: &str, meminfo: &str) -> Option<Self> {
|
||||
let kib = |text: &str, key: &str| -> Option<u64> {
|
||||
@@ -510,6 +552,38 @@ mod tests {
|
||||
v.iter().map(|s| (*s).to_owned()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn painted_vertices_are_reused_until_stale_or_laid_out_again() {
|
||||
let vertex = HudVertex {
|
||||
pos: [1.0, 2.0],
|
||||
uv: [0.0; 2],
|
||||
color: 7,
|
||||
depth: 0.0,
|
||||
glint_uv: [0.0; 2],
|
||||
text: 1,
|
||||
};
|
||||
let layout = ([1920, 1080], 2.0f32.to_bits(), 5);
|
||||
let start = Instant::now();
|
||||
let mut cache = PaintCache::default();
|
||||
assert!(cache.fresh(start, layout).is_none());
|
||||
cache.store(start, layout, &[vertex]);
|
||||
let just_before = start + Duration::from_millis(49);
|
||||
assert_eq!(cache.fresh(just_before, layout).map(<[_]>::len), Some(1));
|
||||
// Stale after a twentieth of a second, so the stats keep moving.
|
||||
assert!(cache.fresh(start + REPAINT, layout).is_none());
|
||||
// Another window size, GUI scale or glyph layout repaints at once.
|
||||
for other in [
|
||||
([1280, 720], layout.1, layout.2),
|
||||
(layout.0, 3.0f32.to_bits(), layout.2),
|
||||
(layout.0, layout.1, 6),
|
||||
] {
|
||||
assert!(cache.fresh(just_before, other).is_none());
|
||||
}
|
||||
// A smaller repaint leaves nothing of the last one behind.
|
||||
cache.store(start, layout, &[]);
|
||||
assert_eq!(cache.fresh(start, layout).map(<[_]>::len), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn looking_at_lines_match_vanilla() {
|
||||
let props = [
|
||||
@@ -611,6 +685,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn the_memory_lines_show_on_windows() {
|
||||
let m = ProcessMemory::read().expect("Windows reports the process's memory");
|
||||
assert!(
|
||||
0 < m.used && m.used <= m.allocated && m.allocated <= m.max,
|
||||
"{m:?}"
|
||||
);
|
||||
let lines = MemoryMeter::default()
|
||||
.lines()
|
||||
.expect("the meter reads the same counters");
|
||||
assert!(lines[0].starts_with("Mem: "), "{lines:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns_follow_the_vanilla_split() {
|
||||
// The default profile's entries, sorted by identifier as
|
||||
|
||||
@@ -3217,6 +3217,10 @@ struct Game<'a> {
|
||||
debug_options: Option<debug_options::DebugOptionsUi>,
|
||||
/// `DebugEntryMemory`'s readings (game/debug_lines.rs).
|
||||
memory_meter: std::cell::RefCell<horizon_client::debug_overlay::MemoryMeter>,
|
||||
/// The F3 overlay's vertices between rebuilds (`draw_debug_overlay_under`).
|
||||
debug_paint: RefCell<horizon_client::debug_overlay::PaintCache>,
|
||||
/// The F3 overlay's last reading of the refresh rate, and when it was taken.
|
||||
debug_refresh: std::cell::Cell<Option<(Instant, Option<u32>)>>,
|
||||
/// `Minecraft.gpuUtilization`: the last timed frame's GPU time over its
|
||||
/// CPU time, in percent.
|
||||
gpu_utilization: f64,
|
||||
@@ -17100,9 +17104,16 @@ impl Game<'_> {
|
||||
pr.lap("pr_frame");
|
||||
// Renderer encoding and GPU waiting belong to render, not prepare.
|
||||
drop(prepare);
|
||||
let debug_passes = self.debug_lists_gpu_passes();
|
||||
let result = match self.renderer.as_mut() {
|
||||
Some(renderer) => {
|
||||
renderer.set_pass_timing(self.debug || self.gpu_passes);
|
||||
renderer.set_pass_timing(debug_passes || self.gpu_passes);
|
||||
// `gpu_passes` is a measurement: it takes every sample it can.
|
||||
renderer.set_pass_timing_gap(if self.gpu_passes {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
horizon_client::debug_overlay::GPU_TIMING_GAP
|
||||
});
|
||||
horizon_client::spans::time("renderer", || renderer.render(&frame))
|
||||
}
|
||||
None => Err("renderer".into()),
|
||||
@@ -18159,6 +18170,7 @@ impl ApplicationHandler for Game<'_> {
|
||||
if self.window.is_some() {
|
||||
return;
|
||||
}
|
||||
debug_keys::warm_debug_cpu();
|
||||
// The Window constructor: created fullscreen when options.txt or --fullscreen says so.
|
||||
let fullscreen = self.settings.fullscreen || self.options.fullscreen;
|
||||
let size = self.options.window_size.map(f64::from);
|
||||
@@ -20396,6 +20408,8 @@ impl<'a> Game<'a> {
|
||||
),
|
||||
debug_options: None,
|
||||
memory_meter: Default::default(),
|
||||
debug_paint: Default::default(),
|
||||
debug_refresh: Default::default(),
|
||||
gpu_utilization: 0.0,
|
||||
rendered_mobs: Default::default(),
|
||||
screen_blur_requested: Default::default(),
|
||||
|
||||
@@ -15,12 +15,27 @@ use horizon_client::{
|
||||
use horizon_core::geometry::{Axis, Vec3};
|
||||
use horizon_protocol::packets::play::{BlockEntityTagQuery, EntityTagQuery, Serverbound};
|
||||
use horizon_world::nbt::Tag;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::keyboard::KeyCode;
|
||||
|
||||
/// `ChatFormatting.YELLOW` and `RED`.
|
||||
const YELLOW: u32 = 0xFF_FF55;
|
||||
const RED: u32 = 0xFF_5555;
|
||||
|
||||
/// The overlay's CPU name. Reading it can start a process (`reg`, `sysctl`), which the render
|
||||
/// thread shouldn't wait for, so [`warm_debug_cpu`] reads it ahead.
|
||||
static DEBUG_CPU: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||
|
||||
/// Reads the overlay's CPU name in the background when the window opens, for F3's first frame.
|
||||
pub(super) fn warm_debug_cpu() {
|
||||
// A thread that can't start only leaves the read to that first frame.
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("debug-cpu-name".into())
|
||||
.spawn(|| {
|
||||
DEBUG_CPU.get_or_init(horizon_client::crash::cpu);
|
||||
});
|
||||
}
|
||||
|
||||
/// The game as `KeyboardHandler` sees it while it runs one key event.
|
||||
struct Host<'g, 'a> {
|
||||
game: &'g mut Game<'a>,
|
||||
@@ -289,7 +304,8 @@ impl Game<'_> {
|
||||
}
|
||||
/// `DebugScreenOverlay.extractRenderState`: the enabled entries' lines
|
||||
/// (`debug-profile.json`), at the debug GUI scale, and the charts while
|
||||
/// the overlay is open. F1 hides it unless a screen is open.
|
||||
/// the overlay is open. F1 hides it unless a screen is open. Horizon paints
|
||||
/// it 20 times a second and copies those vertices in between.
|
||||
pub(super) fn draw_debug_overlay_under(&self, batch: &mut HudBatch, size: [u32; 2]) {
|
||||
let screen_open = self.screen != Screen::Playing;
|
||||
let reduced = self.reduced_debug_info();
|
||||
@@ -297,8 +313,19 @@ impl Game<'_> {
|
||||
|| !self.debug_entries.any_enabled(self.debug, reduced)
|
||||
|| (self.ui.hide_gui && !screen_open)
|
||||
{
|
||||
// Nothing reads the server's lines now: it stops computing them each tick.
|
||||
if let Some(server) = &self.integrated {
|
||||
server.request_debug(None);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let now = Instant::now();
|
||||
let layout = (size, batch.scale.to_bits(), self.gui.atlas.font.epoch());
|
||||
if let Some(vertices) = self.debug_paint.borrow().fresh(now, layout) {
|
||||
batch.vertices.extend_from_slice(vertices);
|
||||
return;
|
||||
}
|
||||
let first_vertex = batch.vertices.len();
|
||||
let framebuffer = size.map(|v| v as i32);
|
||||
let (scale, [width, height]) = debug_overlay::scale(
|
||||
framebuffer,
|
||||
@@ -343,6 +370,33 @@ impl Game<'_> {
|
||||
draws.push(hud_layout::GuiDraw::Pop);
|
||||
}
|
||||
hud_layout::paint(batch, atlas, &draws);
|
||||
self.debug_paint
|
||||
.borrow_mut()
|
||||
.store(now, layout, &batch.vertices[first_vertex..]);
|
||||
}
|
||||
/// Whether the overlay is listing GPU pass times (its `system_specs` lines), the only reason
|
||||
/// it has to time passes.
|
||||
pub(super) fn debug_lists_gpu_passes(&self) -> bool {
|
||||
self.debug
|
||||
&& self.title.is_none()
|
||||
&& !(self.ui.hide_gui && self.screen == Screen::Playing)
|
||||
&& self.debug_entry("system_specs")
|
||||
}
|
||||
/// The monitor's refresh rate in millihertz, asked of the OS at most once a second (on
|
||||
/// Windows, winit calls `EnumDisplaySettingsExW` each time).
|
||||
fn debug_refresh_rate(&self) -> Option<u32> {
|
||||
if let Some((at, rate)) = self.debug_refresh.get()
|
||||
&& at.elapsed() < Duration::from_secs(1)
|
||||
{
|
||||
return rate;
|
||||
}
|
||||
let rate = self
|
||||
.window
|
||||
.as_ref()
|
||||
.and_then(|w| w.current_monitor())
|
||||
.and_then(|m| m.refresh_rate_millihertz());
|
||||
self.debug_refresh.set(Some((Instant::now(), rate)));
|
||||
rate
|
||||
}
|
||||
/// `DebugEntryFps`.
|
||||
pub(super) fn debug_fps_line(&self, d: &mut debug_overlay::Displayer) {
|
||||
@@ -357,14 +411,9 @@ impl Game<'_> {
|
||||
wgpu::PresentMode::FifoRelaxed => " (fifo relaxed)",
|
||||
_ => "",
|
||||
});
|
||||
let refresh = self
|
||||
.window
|
||||
.as_ref()
|
||||
.and_then(|w| w.current_monitor())
|
||||
.and_then(|m| m.refresh_rate_millihertz())
|
||||
.map_or("0".into(), |mhz| {
|
||||
debug_overlay::refresh_rate_label(f64::from(mhz) / 1000.0)
|
||||
});
|
||||
let refresh = self.debug_refresh_rate().map_or("0".into(), |mhz| {
|
||||
debug_overlay::refresh_rate_label(f64::from(mhz) / 1000.0)
|
||||
});
|
||||
// Horizon: with frame generation running, the frames reaching the screen too.
|
||||
let generated = self
|
||||
.renderer
|
||||
@@ -420,12 +469,11 @@ impl Game<'_> {
|
||||
framebuffer: [i32; 2],
|
||||
) {
|
||||
if let Some(r) = self.renderer.as_ref() {
|
||||
static CPU: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||
d.group(
|
||||
"system",
|
||||
[format!(
|
||||
"CPU: {}",
|
||||
CPU.get_or_init(horizon_client::crash::cpu)
|
||||
DEBUG_CPU.get_or_init(horizon_client::crash::cpu)
|
||||
)],
|
||||
);
|
||||
let info = &r.info;
|
||||
@@ -445,7 +493,8 @@ impl Game<'_> {
|
||||
);
|
||||
if let Some(w) = self.window.as_ref() {
|
||||
let density = w.scale_factor();
|
||||
let logical = w.inner_size().to_logical::<f64>(density);
|
||||
let logical = winit::dpi::PhysicalSize::new(framebuffer[0], framebuffer[1])
|
||||
.to_logical::<f64>(density);
|
||||
d.group(
|
||||
"system",
|
||||
[format!(
|
||||
|
||||
@@ -14,7 +14,7 @@ libmimalloc-sys = { version = "0.1.49", default-features = false, features = ["e
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_Console", "Win32_System_IO", "Win32_System_ProcessStatus", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_WindowsAndMessaging"] }
|
||||
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_Console", "Win32_System_IO", "Win32_System_ProcessStatus", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_WindowsAndMessaging"] }
|
||||
|
||||
[features]
|
||||
# Counts live heap bytes by startup step and thread (`CensusAllocator`); for measuring only.
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
//! Read-only process memory diagnostics.
|
||||
/// The process's memory counters.
|
||||
#[cfg(windows)]
|
||||
fn counters() -> Option<windows_sys::Win32::System::ProcessStatus::PROCESS_MEMORY_COUNTERS> {
|
||||
fn counters() -> Option<windows_sys::Win32::System::ProcessStatus::PROCESS_MEMORY_COUNTERS_EX2> {
|
||||
use windows_sys::Win32::System::ProcessStatus::{
|
||||
K32GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS,
|
||||
K32GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS_EX2,
|
||||
};
|
||||
use windows_sys::Win32::System::Threading::GetCurrentProcess;
|
||||
let size = std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
|
||||
// SAFETY: all-zero is a valid value of this plain C struct, and the call writes at most `size`
|
||||
// bytes into it; the current process's pseudo-handle needs no closing.
|
||||
let size = std::mem::size_of::<PROCESS_MEMORY_COUNTERS_EX2>() as u32;
|
||||
// SAFETY: all-zero is a valid value of this plain C struct, which starts with the fields of the
|
||||
// one the call is declared with, and the call writes at most `size` bytes into it; the current
|
||||
// process's pseudo-handle needs no closing.
|
||||
unsafe {
|
||||
let mut counters: PROCESS_MEMORY_COUNTERS = std::mem::zeroed();
|
||||
let mut counters: PROCESS_MEMORY_COUNTERS_EX2 = std::mem::zeroed();
|
||||
counters.cb = size;
|
||||
(K32GetProcessMemoryInfo(GetCurrentProcess(), &mut counters, size) != 0).then_some(counters)
|
||||
(K32GetProcessMemoryInfo(GetCurrentProcess(), (&raw mut counters).cast(), size) != 0)
|
||||
.then_some(counters)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +24,34 @@ pub fn resident_bytes() -> Option<u64> {
|
||||
Some(counters()?.WorkingSetSize as u64)
|
||||
}
|
||||
|
||||
/// The private memory this process holds in RAM, all it holds in RAM and the machine's RAM, in
|
||||
/// bytes.
|
||||
#[cfg(windows)]
|
||||
pub fn usage() -> Option<[u64; 3]> {
|
||||
use windows_sys::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX};
|
||||
let counters = counters()?;
|
||||
let resident = counters.WorkingSetSize as u64;
|
||||
let private = match counters.PrivateWorkingSetSize {
|
||||
// Windows before 10 22H2 leaves it empty: the private commit is the nearest it has.
|
||||
0 => (counters.PrivateUsage as u64).min(resident),
|
||||
bytes => bytes as u64,
|
||||
};
|
||||
// SAFETY: all-zero is a valid value of this plain C struct, its length is set as the call
|
||||
// requires, and the call only writes into it.
|
||||
let ram = unsafe {
|
||||
let mut status: MEMORYSTATUSEX = std::mem::zeroed();
|
||||
status.dwLength = std::mem::size_of::<MEMORYSTATUSEX>() as u32;
|
||||
(GlobalMemoryStatusEx(&mut status) != 0).then_some(status.ullTotalPhys)
|
||||
}?;
|
||||
Some([private, resident, ram])
|
||||
}
|
||||
|
||||
/// [`usage`]'s counters: not read on this system.
|
||||
#[cfg(not(windows))]
|
||||
pub fn usage() -> Option<[u64; 3]> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The memory this process holds in RAM, in bytes.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn resident_bytes() -> Option<u64> {
|
||||
|
||||
Reference in new issue
Block a user