mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-03 06:59:53 -06:00
M5.6a: memory report (library.memory, ui.inspect memory) and opt-in DHAT heap profile
Per-cache bytes: decoded thumbnail/preview/full sources, rendered previews, GPU device buffers (allocated/pooled/retired), loupe stage caches (CPU and GPU) and textures. lightcraft-cli --features dhat-heap adds live/peak heap and writes dhat-heap.json. Baseline (14 raws, loupe, 12 steps): peak RSS 1.9-2.3 GB, footprint 2.5-2.6 GB; heap peak 634 MB after import (probes decode every raw), 930 MB during grid thumbnails (parallel decodes); GPU free pool grows to 2 GB (exact-size reuse). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c2a09ed8e9
commit
78e50b5fc5
+80
-16
@@ -1,6 +1,7 @@
|
||||
//! The wgpu device, the compiled kernels, buffers, dispatch and readback.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
@@ -64,31 +65,72 @@ struct Kernel {
|
||||
/// as the queue then runs every earlier use before any later one. Reuse matters: allocating (and
|
||||
/// zero-filling) a fresh 100–300 MB buffer per pass costs as much as the pass itself.
|
||||
pub struct Buf {
|
||||
buf: Option<wgpu::Buffer>,
|
||||
buf: Option<Tracked>,
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
impl Buf {
|
||||
pub fn raw(&self) -> &wgpu::Buffer {
|
||||
self.buf.as_ref().expect("live buffer")
|
||||
&self.buf.as_ref().expect("live buffer").0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Buf {
|
||||
fn drop(&mut self) {
|
||||
if let Some(b) = self.buf.take() {
|
||||
RETIRED_BYTES.fetch_add(b.0.size(), Ordering::Relaxed);
|
||||
RETIRED.with(|r| r.borrow_mut().push(b));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Buffers dropped on this thread since its last submit.
|
||||
static RETIRED: std::cell::RefCell<Vec<wgpu::Buffer>> = const { std::cell::RefCell::new(Vec::new()) };
|
||||
/// A device buffer, counted in [`ALLOCATED`] while it exists.
|
||||
pub(crate) struct Tracked(wgpu::Buffer);
|
||||
|
||||
impl Tracked {
|
||||
fn new(b: wgpu::Buffer) -> Tracked {
|
||||
ALLOCATED.fetch_add(b.size(), Ordering::Relaxed);
|
||||
Tracked(b)
|
||||
}
|
||||
}
|
||||
|
||||
/// Most bytes kept in the free pool.
|
||||
const POOL_BYTES: u64 = 2 << 30;
|
||||
impl Drop for Tracked {
|
||||
fn drop(&mut self) {
|
||||
ALLOCATED.fetch_sub(self.0.size(), Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Buffers dropped on this thread since its last submit.
|
||||
static RETIRED: std::cell::RefCell<Vec<Tracked>> = const { std::cell::RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
/// Bytes of device buffers that exist (in use, retired or pooled).
|
||||
static ALLOCATED: AtomicU64 = AtomicU64::new(0);
|
||||
/// Bytes waiting in per-thread retired lists.
|
||||
static RETIRED_BYTES: AtomicU64 = AtomicU64::new(0);
|
||||
/// Bytes in the free pool.
|
||||
static POOLED: AtomicU64 = AtomicU64::new(0);
|
||||
/// Most bytes kept in the free pool (see [`crate::set_pool_limit`]).
|
||||
pub(crate) static POOL_LIMIT: AtomicU64 = AtomicU64::new(DEFAULT_POOL_BYTES);
|
||||
|
||||
/// Default for the most bytes kept in the free pool.
|
||||
pub(crate) const DEFAULT_POOL_BYTES: u64 = 2 << 30;
|
||||
|
||||
/// Device buffers held by the renderer (bytes).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct GpuMemory {
|
||||
/// Every buffer that exists (in use + retired + pooled).
|
||||
pub allocated: u64,
|
||||
/// Of which recycled buffers waiting in the free pool.
|
||||
pub pooled: u64,
|
||||
/// Of which buffers dropped since their thread's last submit (pooled at its next submit).
|
||||
pub retired: u64,
|
||||
}
|
||||
|
||||
pub(crate) fn memory() -> GpuMemory {
|
||||
GpuMemory { allocated: ALLOCATED.load(Ordering::Relaxed), pooled: POOLED.load(Ordering::Relaxed), retired: RETIRED_BYTES.load(Ordering::Relaxed) }
|
||||
}
|
||||
|
||||
pub struct Gpu {
|
||||
pub device: wgpu::Device,
|
||||
@@ -97,7 +139,7 @@ pub struct Gpu {
|
||||
kernels: HashMap<&'static str, Kernel>,
|
||||
dummy: wgpu::Buffer,
|
||||
/// Recycled buffers (see [`Buf`]).
|
||||
free: std::sync::Mutex<Vec<wgpu::Buffer>>,
|
||||
free: std::sync::Mutex<Vec<Tracked>>,
|
||||
/// Largest storage buffer the device accepts (bytes).
|
||||
pub max_buffer: u64,
|
||||
}
|
||||
@@ -217,15 +259,19 @@ impl Gpu {
|
||||
let size = (len.max(1) * 4) as u64;
|
||||
let recycled = {
|
||||
let mut f = self.free.lock().unwrap_or_else(|e| e.into_inner());
|
||||
f.iter().position(|b| b.size() == size).map(|i| f.swap_remove(i))
|
||||
let b = f.iter().position(|b| b.0.size() == size).map(|i| f.swap_remove(i));
|
||||
if b.is_some() {
|
||||
POOLED.fetch_sub(size, Ordering::Relaxed);
|
||||
}
|
||||
b
|
||||
};
|
||||
let buf = recycled.unwrap_or_else(|| {
|
||||
self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
Tracked::new(self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
}))
|
||||
});
|
||||
Buf { buf: Some(buf), len }
|
||||
}
|
||||
@@ -234,11 +280,29 @@ impl Gpu {
|
||||
pub fn submit(&self, cmds: impl IntoIterator<Item = wgpu::CommandBuffer>) {
|
||||
self.queue.submit(cmds);
|
||||
let retired = RETIRED.with(|r| std::mem::take(&mut *r.borrow_mut()));
|
||||
let bytes: u64 = retired.iter().map(|b| b.0.size()).sum();
|
||||
RETIRED_BYTES.fetch_sub(bytes, Ordering::Relaxed);
|
||||
POOLED.fetch_add(bytes, Ordering::Relaxed);
|
||||
self.free.lock().unwrap_or_else(|e| e.into_inner()).extend(retired);
|
||||
self.trim(POOL_LIMIT.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
/// Free pooled buffers (oldest first) until at most `keep` bytes stay pooled.
|
||||
pub fn trim(&self, keep: u64) {
|
||||
let mut f = self.free.lock().unwrap_or_else(|e| e.into_inner());
|
||||
f.extend(retired);
|
||||
let mut total: u64 = f.iter().map(|b| b.size()).sum();
|
||||
while total > POOL_BYTES && !f.is_empty() {
|
||||
total -= f.remove(0).size();
|
||||
let mut total: u64 = f.iter().map(|b| b.0.size()).sum();
|
||||
let mut dropped = Vec::new();
|
||||
while total > keep && !f.is_empty() {
|
||||
let b = f.remove(0);
|
||||
total -= b.0.size();
|
||||
POOLED.fetch_sub(b.0.size(), Ordering::Relaxed);
|
||||
dropped.push(b);
|
||||
}
|
||||
drop(f);
|
||||
if !dropped.is_empty() {
|
||||
drop(dropped);
|
||||
// let wgpu release the memory now rather than at its next maintenance
|
||||
let _ = self.device.poll(wgpu::PollType::Poll);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +318,7 @@ impl Gpu {
|
||||
contents: bytes,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
Buf { buf: Some(buf), len }
|
||||
Buf { buf: Some(Tracked::new(buf)), len }
|
||||
}
|
||||
|
||||
/// Whether a buffer of `len` 32-bit values fits the device limits.
|
||||
|
||||
@@ -114,6 +114,65 @@ pub fn adapter_name() -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Device buffers held by the renderer.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use ctx::GpuMemory;
|
||||
|
||||
/// Device buffers held by the renderer.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct GpuMemory {
|
||||
pub allocated: u64,
|
||||
pub pooled: u64,
|
||||
pub retired: u64,
|
||||
}
|
||||
|
||||
/// Device memory held by the renderer's buffers (zero without a device).
|
||||
pub fn memory() -> GpuMemory {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
ctx::memory()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
GpuMemory::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep at most `bytes` of recycled buffers in the free pool from now on (trimming it now).
|
||||
pub fn set_pool_limit(bytes: u64) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
ctx::POOL_LIMIT.store(bytes, Ordering::Relaxed);
|
||||
trim_pool(bytes);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = bytes;
|
||||
}
|
||||
|
||||
/// Free recycled buffers until at most `keep` bytes stay pooled (e.g. when the app goes idle).
|
||||
pub fn trim_pool(keep: u64) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(g) = GPU.get().and_then(|g| g.as_ref()) {
|
||||
g.trim(keep);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = keep;
|
||||
}
|
||||
|
||||
/// Device bytes held by a view's GPU stages (kept with its [`StageCache`]); 0 when it has none.
|
||||
pub fn stage_bytes(stages: &StageCache) -> usize {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
stages.peek_extension::<GpuStages>().map(|g| g.bytes()).unwrap_or(0)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = stages;
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Render `src` with `s` on the GPU, reusing the device-resident stages kept with `stages` (the
|
||||
/// view's CPU stage cache). `None`: render on the CPU instead.
|
||||
pub fn render(src: &Arc<Rgb32f>, info: &SourceInfo, s: &DevelopSettings, req: &RenderRequest, stages: Option<&StageCache>) -> Option<Rendered> {
|
||||
|
||||
@@ -82,6 +82,41 @@ impl GpuStages {
|
||||
self.lock().len()
|
||||
}
|
||||
|
||||
/// Device bytes held (each buffer counted once).
|
||||
pub fn bytes(&self) -> usize {
|
||||
let mut seen: Vec<*const Buf> = Vec::new();
|
||||
let mut total = 0;
|
||||
let mut add = |b: &Arc<Buf>| {
|
||||
let p = Arc::as_ptr(b);
|
||||
if !seen.contains(&p) {
|
||||
seen.push(p);
|
||||
total += b.len * 4;
|
||||
}
|
||||
};
|
||||
for e in self.lock().iter() {
|
||||
add(&e.sampled);
|
||||
if let Some((_, b)) = &e.lin {
|
||||
add(b);
|
||||
}
|
||||
let p = &e.planes;
|
||||
p.log_l.iter().for_each(&mut add);
|
||||
p.base.iter().for_each(|(_, b)| add(b));
|
||||
p.clarity.iter().for_each(|(_, b)| add(b));
|
||||
p.texture.iter().for_each(|(_, b)| add(b));
|
||||
p.dark.iter().for_each(|(_, b, _)| add(b));
|
||||
}
|
||||
if let Some((_, b)) = &*self.source.lock().unwrap_or_else(|e| e.into_inner()) {
|
||||
add(b);
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
/// Drop everything (e.g. when memory is needed).
|
||||
pub fn clear(&self) {
|
||||
self.lock().clear();
|
||||
*self.source.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user