mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 05:57:09 -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
@@ -135,6 +135,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
}
|
||||
Ok(json!({"enabled": lightcraft_gpu::enabled(), "available": lightcraft_gpu::available(), "adapter": lightcraft_gpu::adapter_name()}))
|
||||
}),
|
||||
cmd!(query "library.memory", "Memory Usage", [], None, "{} — bytes held by each cache (decoded sources, rendered previews, GPU buffers; heap when instrumented)", always, |s, _| {
|
||||
Ok(serde_json::to_value(s.memory_report()).unwrap_or_default())
|
||||
}),
|
||||
cmd!(query "journal.list", "Command Journal", [], None, "{limit?}", always, |s, p| {
|
||||
let lim = p.get("limit").and_then(Value::as_u64).unwrap_or(100) as usize;
|
||||
let start = s.journal.len().saturating_sub(lim);
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod files;
|
||||
pub mod import;
|
||||
pub mod library;
|
||||
pub mod media;
|
||||
pub mod memory;
|
||||
pub mod presets;
|
||||
pub mod sidecar;
|
||||
mod view;
|
||||
|
||||
@@ -169,6 +169,15 @@ impl MediaCache {
|
||||
(self.thumbs.len(), self.thumbs.cost())
|
||||
}
|
||||
|
||||
/// Decoded sources held: (thumbnail level, preview level, full size).
|
||||
pub fn usage(&self) -> (crate::memory::Usage, crate::memory::Usage, crate::memory::Usage) {
|
||||
use crate::memory::Usage;
|
||||
let bytes = |i: &Rgb32f| i.data.len() * 12;
|
||||
let previews = Usage::new(self.previews.len(), self.previews.iter().map(|(_, a)| bytes(a)).sum());
|
||||
let full = self.full.as_ref().map(|(_, a)| Usage::new(1, bytes(a))).unwrap_or_default();
|
||||
(Usage::new(self.thumbs.len(), self.thumbs.cost()), previews, full)
|
||||
}
|
||||
|
||||
pub fn source_ref(&mut self, p: &Photo, level: SourceLevel) -> SourceRef {
|
||||
if let Some(a) = self.get(p.id, level) {
|
||||
return SourceRef::Loaded(a);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//! Memory accounting: what each cache holds (`library.memory`, and `ui.inspect` → `memory` with the
|
||||
//! frontend's own caches added).
|
||||
//!
|
||||
//! The numbers are the caches' own bookkeeping (pixels held), not the process's resident size: the
|
||||
//! allocator keeps freed pages for reuse and the GPU driver maps device buffers, so `ps`/`time -l`
|
||||
//! report more. A binary built with a heap profiler installs [`set_heap_stats`] (e.g.
|
||||
//! `lightcraft-cli` with `--features dhat-heap`) to add live/peak heap bytes.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Entries and bytes held by one cache.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct Usage {
|
||||
pub count: usize,
|
||||
pub bytes: usize,
|
||||
}
|
||||
|
||||
impl Usage {
|
||||
pub fn new(count: usize, bytes: usize) -> Usage {
|
||||
Usage { count, bytes }
|
||||
}
|
||||
}
|
||||
|
||||
/// Heap bytes as counted by an instrumented allocator.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HeapUsage {
|
||||
/// Live heap bytes now.
|
||||
pub current: u64,
|
||||
/// Highest live heap bytes so far.
|
||||
pub peak: u64,
|
||||
}
|
||||
|
||||
/// Device buffers of the GPU renderer.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GpuUsage {
|
||||
/// Every device buffer that exists.
|
||||
pub allocated: u64,
|
||||
/// Of which recycled buffers waiting in the free pool.
|
||||
pub pooled: u64,
|
||||
/// Of which buffers released since their thread's last submit.
|
||||
pub retired: u64,
|
||||
}
|
||||
|
||||
/// What the engine's caches hold.
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MemoryReport {
|
||||
/// Decoded thumbnail-level sources (≤ 512 px, linear float).
|
||||
pub thumb_sources: Usage,
|
||||
/// Decoded preview-level sources (≤ 2560 px, linear float) of the current and prefetched photos.
|
||||
pub preview_sources: Usage,
|
||||
/// The last full-resolution original (exports, 1:1).
|
||||
pub full_source: Usage,
|
||||
/// Rendered thumbnails and view renders (8-bit) in memory.
|
||||
pub rendered: Usage,
|
||||
/// Sum of the above.
|
||||
pub engine_bytes: usize,
|
||||
pub gpu: GpuUsage,
|
||||
/// Live/peak heap when the binary counts allocations.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub heap: Option<HeapUsage>,
|
||||
}
|
||||
|
||||
static HEAP: OnceLock<fn() -> HeapUsage> = OnceLock::new();
|
||||
|
||||
/// Install the heap counter of an instrumented binary (first call wins).
|
||||
pub fn set_heap_stats(f: fn() -> HeapUsage) {
|
||||
let _ = HEAP.set(f);
|
||||
}
|
||||
|
||||
/// Live/peak heap bytes, when the binary installed a counter.
|
||||
pub fn heap_stats() -> Option<HeapUsage> {
|
||||
HEAP.get().map(|f| f())
|
||||
}
|
||||
|
||||
/// The GPU renderer's device buffers.
|
||||
pub fn gpu_usage() -> GpuUsage {
|
||||
let g = lightcraft_gpu::memory();
|
||||
GpuUsage { allocated: g.allocated, pooled: g.pooled, retired: g.retired }
|
||||
}
|
||||
|
||||
impl crate::Session {
|
||||
/// What the engine's caches hold now.
|
||||
pub fn memory_report(&self) -> MemoryReport {
|
||||
let (thumb_sources, preview_sources, full_source) = self.media.usage();
|
||||
let (n, b) = self.media.rendered.mem_usage();
|
||||
let rendered = Usage::new(n, b);
|
||||
MemoryReport {
|
||||
thumb_sources,
|
||||
preview_sources,
|
||||
full_source,
|
||||
rendered,
|
||||
engine_bytes: thumb_sources.bytes + preview_sources.bytes + full_source.bytes + rendered.bytes,
|
||||
gpu: gpu_usage(),
|
||||
heap: heap_stats(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,3 +189,23 @@ fn upright_and_guides_commands() {
|
||||
assert!(s.render_now(id, 160, 160).is_ok());
|
||||
assert!(s.execute("geometry.upright", &json!({"mode": "sideways"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_report_counts_decoded_sources_and_renders() {
|
||||
let mut s = demo();
|
||||
let r = s.execute("library.memory", &json!({})).unwrap();
|
||||
assert_eq!(r["previewSources"]["count"], 0);
|
||||
assert!(r["gpu"]["allocated"].is_u64() && r.get("engineBytes").is_some());
|
||||
let id = s.active().unwrap();
|
||||
s.render_now(id, 800, 600).unwrap();
|
||||
let m = s.memory_report();
|
||||
assert_eq!(m.preview_sources.count, 1);
|
||||
// a 2560 px linear float source: 12 bytes per pixel
|
||||
assert!(m.preview_sources.bytes >= 12 * 2560 * 1000, "{:?}", m.preview_sources);
|
||||
assert_eq!(m.engine_bytes, m.thumb_sources.bytes + m.preview_sources.bytes + m.full_source.bytes + m.rendered.bytes);
|
||||
let r = s.thumb_job(id, 200).unwrap().run();
|
||||
s.accept(&r);
|
||||
let m = s.memory_report();
|
||||
assert_eq!(m.thumb_sources.count, 1);
|
||||
assert!(m.rendered.count >= 1, "the thumbnail render is cached");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user