Memory per phase with a counting allocator d443639bcf35
The oxy binary's alloc-stats feature installs a counting global allocator; with OXY_TIMINGS=1 each phase then reports live and peak memory and what it allocated, and a summary closes the command. It is how the Linux profile (#35) finds the structures that hold the memory. closes #39
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-29 21:34 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
d443639bcf35254060952ddc1838a83bae5b1f99857703acdab5e2423dfbfdae- Depends on
- 0bdc3f91a39f 1983686fb7a7 5b1114a04905 5b9c6ad45f6e e293c90dca62 ee06a6c557b7 fce44ae9ad29 ff0af1ee299d
- Depended on by
- c397a8c36a9f b582948f1912 d45dab0484ea e0d56dfca51d 19527875ebdc
- In bookmarks
- agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
- In tags
- v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 14 atoms, 1 tree operation, 6 files
Browse the tree at this change
added crates/oxy-core/src/alloc_stats.rs +79 −0
| 1 | + | //! A counting allocator for measuring memory (`--features alloc-stats` on |
|
| 2 | + | //! the `oxy` binary, with `OXY_TIMINGS=1`): each timing phase then also |
|
| 3 | + | //! reports live bytes, the peak so far and what the phase allocated, so a |
|
| 4 | + | //! profile names the structure that holds the memory. |
|
| 5 | + | ||
| 6 | + | use std::alloc::{GlobalAlloc, Layout, System}; |
|
| 7 | + | use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}; |
|
| 8 | + | ||
| 9 | + | static LIVE: AtomicUsize = AtomicUsize::new(0); |
|
| 10 | + | static PEAK: AtomicUsize = AtomicUsize::new(0); |
|
| 11 | + | static TOTAL: AtomicUsize = AtomicUsize::new(0); |
|
| 12 | + | static COUNT: AtomicUsize = AtomicUsize::new(0); |
|
| 13 | + | static ACTIVE: AtomicBool = AtomicBool::new(false); |
|
| 14 | + | ||
| 15 | + | /// The system allocator, counting. Install it with |
|
| 16 | + | /// `#[global_allocator] static A: Counting = Counting;`. |
|
| 17 | + | pub struct Counting; |
|
| 18 | + | ||
| 19 | + | impl Counting { |
|
| 20 | + | fn grew(n: usize) { |
|
| 21 | + | ACTIVE.store(true, Relaxed); |
|
| 22 | + | let live = LIVE.fetch_add(n, Relaxed) + n; |
|
| 23 | + | TOTAL.fetch_add(n, Relaxed); |
|
| 24 | + | COUNT.fetch_add(1, Relaxed); |
|
| 25 | + | PEAK.fetch_max(live, Relaxed); |
|
| 26 | + | } |
|
| 27 | + | } |
|
| 28 | + | ||
| 29 | + | unsafe impl GlobalAlloc for Counting { |
|
| 30 | + | unsafe fn alloc(&self, l: Layout) -> *mut u8 { |
|
| 31 | + | let p = System.alloc(l); |
|
| 32 | + | if !p.is_null() { |
|
| 33 | + | Self::grew(l.size()); |
|
| 34 | + | } |
|
| 35 | + | p |
|
| 36 | + | } |
|
| 37 | + | unsafe fn alloc_zeroed(&self, l: Layout) -> *mut u8 { |
|
| 38 | + | let p = System.alloc_zeroed(l); |
|
| 39 | + | if !p.is_null() { |
|
| 40 | + | Self::grew(l.size()); |
|
| 41 | + | } |
|
| 42 | + | p |
|
| 43 | + | } |
|
| 44 | + | unsafe fn dealloc(&self, p: *mut u8, l: Layout) { |
|
| 45 | + | System.dealloc(p, l); |
|
| 46 | + | LIVE.fetch_sub(l.size(), Relaxed); |
|
| 47 | + | } |
|
| 48 | + | unsafe fn realloc(&self, p: *mut u8, l: Layout, new: usize) -> *mut u8 { |
|
| 49 | + | let q = System.realloc(p, l, new); |
|
| 50 | + | if !q.is_null() { |
|
| 51 | + | LIVE.fetch_sub(l.size(), Relaxed); |
|
| 52 | + | Self::grew(new); |
|
| 53 | + | } |
|
| 54 | + | q |
|
| 55 | + | } |
|
| 56 | + | } |
|
| 57 | + | ||
| 58 | + | /// What the counting allocator has seen, when it is installed. |
|
| 59 | + | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
|
| 60 | + | pub struct Stats { |
|
| 61 | + | /// Bytes allocated and not yet freed. |
|
| 62 | + | pub live: usize, |
|
| 63 | + | /// The most `live` has been. |
|
| 64 | + | pub peak: usize, |
|
| 65 | + | /// Bytes ever allocated. |
|
| 66 | + | pub total: usize, |
|
| 67 | + | /// Allocations ever made. |
|
| 68 | + | pub count: usize, |
|
| 69 | + | } |
|
| 70 | + | ||
| 71 | + | /// The counters, or `None` when the counting allocator is not installed. |
|
| 72 | + | pub fn stats() -> Option<Stats> { |
|
| 73 | + | ACTIVE.load(Relaxed).then(|| Stats { |
|
| 74 | + | live: LIVE.load(Relaxed), |
|
| 75 | + | peak: PEAK.load(Relaxed), |
|
| 76 | + | total: TOTAL.load(Relaxed), |
|
| 77 | + | count: COUNT.load(Relaxed), |
|
| 78 | + | }) |
|
| 79 | + | } |
modified Cargo.toml +4 −0
| … | 28 unchanged lines |
||
| 29 | 29 | indicatif = "0.17" |
|
| 30 | 30 | serde_json = "1" |
|
| 31 | 31 | ||
| 32 | + | [features] |
|
| 33 | + | # A counting global allocator: OXY_TIMINGS=1 then reports memory per phase. |
|
| 34 | + | alloc-stats = [] |
|
| 35 | + | ||
| 32 | 36 | [dev-dependencies] |
|
| 33 | 37 | tempfile = "3" |
modified crates/oxy-core/src/lib.rs +1 −0
| … | 4 unchanged lines |
||
| 5 | 5 | /// This library's version, for hosts that report what they are built on. |
|
| 6 | 6 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); |
|
| 7 | 7 | ||
| 8 | + | pub mod alloc_stats; |
|
| 8 | 9 | pub mod attest; |
|
| 9 | 10 | pub mod attributes; |
|
| 10 | 11 | pub mod cbor; |
|
| … | 21 unchanged lines |
modified crates/oxy-core/src/timing.rs +43 −7
| 1 | 1 | //! Opt-in phase timings (`OXY_TIMINGS=1`): each named phase prints its |
|
| 2 | 2 | //! duration to stderr, for measuring where a command spends its time |
|
| 3 | - | //! without a profiler. |
|
| 3 | + | //! without a profiler. With the counting allocator installed (the `oxy` |
|
| 4 | + | //! binary's `alloc-stats` feature), each also prints live and peak memory |
|
| 5 | + | //! and what the phase allocated. |
|
| 4 | 6 | ||
| 5 | 7 | use std::sync::OnceLock; |
|
| 6 | 8 | use std::time::Instant; |
|
| … | 14 unchanged lines |
||
| 21 | 23 | pub struct Phase { |
|
| 22 | 24 | name: &'static str, |
|
| 23 | 25 | started: Option<Instant>, |
|
| 26 | + | /// Allocator counters at the start, when counting. |
|
| 27 | + | mem: Option<crate::alloc_stats::Stats>, |
|
| 24 | 28 | } |
|
| 25 | 29 | ||
| 26 | 30 | /// Start timing a phase; the guard reports when it goes out of scope. |
|
| 27 | 31 | pub fn phase(name: &'static str) -> Phase { |
|
| 32 | + | let on = enabled(); |
|
| 28 | 33 | Phase { |
|
| 29 | 34 | name, |
|
| 30 | - | started: enabled().then(Instant::now), |
|
| 35 | + | started: on.then(Instant::now), |
|
| 36 | + | mem: if on { |
|
| 37 | + | crate::alloc_stats::stats() |
|
| 38 | + | } else { |
|
| 39 | + | None |
|
| 40 | + | }, |
|
| 41 | + | } |
|
| 42 | + | } |
|
| 43 | + | ||
| 44 | + | fn mib(n: usize) -> f64 { |
|
| 45 | + | n as f64 / (1024.0 * 1024.0) |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | /// The allocator's counters as one line, when counting: for the end of a |
|
| 49 | + | /// command. |
|
| 50 | + | pub fn memory_summary() { |
|
| 51 | + | if let (true, Some(s)) = (enabled(), crate::alloc_stats::stats()) { |
|
| 52 | + | eprintln!( |
|
| 53 | + | "memory peak {:.1} MiB, live {:.1} MiB, {:.1} MiB in {} allocations", |
|
| 54 | + | mib(s.peak), |
|
| 55 | + | mib(s.live), |
|
| 56 | + | mib(s.total), |
|
| 57 | + | s.count |
|
| 58 | + | ); |
|
| 31 | 59 | } |
|
| 32 | 60 | } |
|
| 33 | 61 | ||
| 34 | 62 | impl Drop for Phase { |
|
| 35 | 63 | fn drop(&mut self) { |
|
| 36 | 64 | if let Some(t) = self.started { |
|
| 37 | - | eprintln!( |
|
| 38 | - | "timing {:<28} {:>9.2} ms", |
|
| 39 | - | self.name, |
|
| 40 | - | t.elapsed().as_secs_f64() * 1000.0 |
|
| 41 | - | ); |
|
| 65 | + | let ms = t.elapsed().as_secs_f64() * 1000.0; |
|
| 66 | + | match (self.mem, crate::alloc_stats::stats()) { |
|
| 67 | + | (Some(a), Some(b)) => eprintln!( |
|
| 68 | + | "timing {:<28} {:>9.2} ms live {:>8.1} MiB peak {:>8.1} MiB +{:.1} MiB in {} allocs", |
|
| 69 | + | self.name, |
|
| 70 | + | ms, |
|
| 71 | + | mib(b.live), |
|
| 72 | + | mib(b.peak), |
|
| 73 | + | mib(b.total - a.total), |
|
| 74 | + | b.count - a.count |
|
| 75 | + | ), |
|
| 76 | + | _ => eprintln!("timing {:<28} {:>9.2} ms", self.name, ms), |
|
| 77 | + | } |
|
| 42 | 78 | } |
|
| 43 | 79 | } |
|
| 44 | 80 | } |
modified docs/guide.md +1 −1
| … | 373 unchanged lines |
||
| 374 | 374 | ||
| 375 | 375 | Two files, both `key = "value"` lines: `~/.config/oxy/config.toml` |
|
| 376 | 376 | (`oxy config --user KEY VALUE`) and `.oxy/config` (`oxy config KEY VALUE`). |
|
| 377 | - | The repository file wins. `OXY_HOME` relocates the user directory. `OXY_TIMINGS=1` prints how long each phase of a snapshot takes (loading the base state, rendering it, reading the working copy, building the change) to standard error, for finding out where a slow command spends its time. |
|
| 377 | + | The repository file wins. `OXY_HOME` relocates the user directory. `OXY_TIMINGS=1` prints how long each phase of a snapshot takes (loading the base state, rendering it, reading the working copy, building the change) to standard error, for finding out where a slow command spends its time. Built with `cargo build --release --features alloc-stats`, oxy also reports each phase's live and peak memory and what it allocated, and a summary at the end. |
|
| 378 | 378 | ||
| 379 | 379 | | Key | Meaning | |
|
| 380 | 380 | |---|---| |
|
| … | 51 unchanged lines |
modified src/main.rs +7 −1
| … | 4 unchanged lines |
||
| 5 | 5 | ||
| 6 | 6 | mod cli; |
|
| 7 | 7 | ||
| 8 | + | #[cfg(feature = "alloc-stats")] |
|
| 9 | + | #[global_allocator] |
|
| 10 | + | static ALLOC: oxy_core::alloc_stats::Counting = oxy_core::alloc_stats::Counting; |
|
| 11 | + | ||
| 8 | 12 | fn main() { |
|
| 9 | 13 | // Behave like other CLI tools when output is piped into `head`: die |
|
| 10 | 14 | // quietly on SIGPIPE instead of panicking on a failed write. |
|
| … | 1 unchanged lines |
||
| 12 | 16 | unsafe { |
|
| 13 | 17 | libc::signal(libc::SIGPIPE, libc::SIG_DFL); |
|
| 14 | 18 | } |
|
| 15 | - | match cli::run() { |
|
| 19 | + | let result = cli::run(); |
|
| 20 | + | oxy_core::timing::memory_summary(); |
|
| 21 | + | match result { |
|
| 16 | 22 | Ok(code) => std::process::exit(code), |
|
| 17 | 23 | Err(e) => { |
|
| 18 | 24 | cli::ui::init(false); |
|
| … | 5 unchanged lines |