├── .gitignore ├── CODE_OF_CONDUCT.md ├── examples ├── ad-hoc.rs ├── heap.rs └── heap-testing.rs ├── tests ├── assert_eq-failure.rs ├── assert_ne-failure.rs ├── assert-failure.rs ├── ad-hoc-panics.rs ├── heap-panics.rs ├── ad-hoc.rs └── heap.rs ├── .github └── workflows │ └── ci.yml ├── LICENSE-MIT ├── Cargo.toml ├── README.md ├── LICENSE-APACHE └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | Cargo.lock 3 | 4 | # Generated by `cargo test` 5 | dhat-heap.json 6 | dhat-ad-hoc.json 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # The Rust Performance Book Code of Conduct 2 | 3 | This repository uses the [Rust Code of Conduct]. 4 | 5 | [Rust Code of Conduct]: https://www.rust-lang.org/conduct.html 6 | -------------------------------------------------------------------------------- /examples/ad-hoc.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let _profiler = dhat::Profiler::new_ad_hoc(); 3 | 4 | dhat::ad_hoc_event(100); 5 | println!("Hello, world!"); 6 | dhat::ad_hoc_event(200); 7 | } 8 | -------------------------------------------------------------------------------- /examples/heap.rs: -------------------------------------------------------------------------------- 1 | // This is a very simple example of how to do heap profiling of a program. You 2 | // may want to create a feature called `dhat-heap` in your `Cargo.toml` and 3 | // uncomment the `#[cfg(feature = "dhat-heap")]` attributes. 4 | 5 | //#[cfg(feature = "dhat-heap")] 6 | #[global_allocator] 7 | static ALLOC: dhat::Alloc = dhat::Alloc; 8 | 9 | fn main() { 10 | //#[cfg(feature = "dhat-heap")] 11 | let _profiler = dhat::Profiler::new_heap(); 12 | 13 | println!("Hello, world!"); 14 | } 15 | -------------------------------------------------------------------------------- /tests/assert_eq-failure.rs: -------------------------------------------------------------------------------- 1 | #[global_allocator] 2 | static ALLOC: dhat::Alloc = dhat::Alloc; 3 | 4 | #[test] 5 | #[should_panic( 6 | expected = "dhat: assertion failed: `(left == right)`\n left: `32`,\n right: `31`: oh dear 99" 7 | )] 8 | fn main() { 9 | let _profiler = dhat::Profiler::builder().testing().eprint_json().build(); 10 | 11 | let _v1 = vec![1, 2, 3, 4]; 12 | let _v2 = vec![5, 6, 7, 8]; 13 | 14 | // Test with and without extra arguments. 15 | let stats = dhat::HeapStats::get(); 16 | dhat::assert_eq!(stats.curr_blocks, 2); 17 | dhat::assert_eq!(stats.curr_bytes, 31, "oh dear {}", 99); // failure 18 | } 19 | -------------------------------------------------------------------------------- /tests/assert_ne-failure.rs: -------------------------------------------------------------------------------- 1 | #[global_allocator] 2 | static ALLOC: dhat::Alloc = dhat::Alloc; 3 | 4 | #[test] 5 | #[should_panic(expected = "dhat: assertion failed: `(left != right)`\n left: `32`,\n right: `32`")] 6 | fn main() { 7 | let _profiler = dhat::Profiler::builder().testing().eprint_json().build(); 8 | 9 | let _v1 = vec![1, 2, 3, 4]; 10 | let _v2 = vec![5, 6, 7, 8]; 11 | 12 | // Test with and without extra arguments. 13 | let stats = dhat::HeapStats::get(); 14 | dhat::assert_ne!(stats.curr_blocks, 1); 15 | dhat::assert_ne!(stats.curr_bytes, 32); // failure 16 | dhat::assert_ne!(stats.curr_bytes, 33, "more"); 17 | } 18 | -------------------------------------------------------------------------------- /examples/heap-testing.rs: -------------------------------------------------------------------------------- 1 | // This is a very simple example of how to do heap usage testing of a program. 2 | // To use this code for a test, you need to make some changes: 3 | // - Move it into an integration test file within `tests/`. 4 | // - Rename `main()` to whatever you want, and add `#[test]` to it. 5 | // 6 | // Also, only use one `#[test]` per integration test, to avoid tests 7 | // interfering with each other. 8 | 9 | #[global_allocator] 10 | static ALLOC: dhat::Alloc = dhat::Alloc; 11 | 12 | fn main() { 13 | let _profiler = dhat::Profiler::builder().testing().build(); 14 | 15 | let _v1 = vec![1, 2, 3, 4]; 16 | let _v2 = vec![5, 6, 7, 8]; 17 | 18 | let stats = dhat::HeapStats::get(); 19 | dhat::assert_eq!(stats.curr_blocks, 2); 20 | dhat::assert_eq!(stats.curr_bytes, 32); 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | name: ${{ matrix.os }} 11 | 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | 18 | steps: 19 | - name: Clone repository 20 | uses: actions/checkout@v2 21 | 22 | - name: Install clippy and rustfmt 23 | run: | 24 | rustup component add clippy 25 | rustup component add rustfmt 26 | 27 | - name: Build (debug) 28 | run: cargo build --verbose 29 | 30 | - name: Build (release) 31 | run: cargo build --verbose --release 32 | 33 | - name: Run tests (debug) 34 | run: cargo test --verbose 35 | 36 | - name: Run tests (release) 37 | run: cargo test --verbose --release 38 | 39 | - name: Check formatting 40 | run: cargo fmt -- --check 41 | 42 | - name: Run clippy 43 | run: cargo clippy -- -D warnings 44 | -------------------------------------------------------------------------------- /tests/assert-failure.rs: -------------------------------------------------------------------------------- 1 | #[global_allocator] 2 | static ALLOC: dhat::Alloc = dhat::Alloc; 3 | 4 | #[test] 5 | fn main() { 6 | let profiler = dhat::Profiler::builder().testing().eprint_json().build(); 7 | 8 | let _v1 = vec![1, 2, 3, 4]; 9 | let _v2 = vec![5, 6, 7, 8]; 10 | 11 | // Test with and without extra arguments. 12 | let stats = dhat::HeapStats::get(); 13 | dhat::assert!(stats.curr_blocks == 2); 14 | 15 | dhat::assert_is_panic( 16 | || dhat::assert!(stats.curr_bytes == 31), 17 | "dhat: assertion failed: stats.curr_bytes == 31", 18 | ); 19 | 20 | dhat::assert_is_panic( 21 | || dhat::assert!(stats.curr_bytes == 32, "extra {} {}", 1, "2"), 22 | "dhat: asserting after the profiler has asserted", 23 | ); 24 | 25 | drop(profiler); 26 | 27 | let _profiler = dhat::Profiler::builder().testing().eprint_json().build(); 28 | 29 | // Normal assert on a second profiler. 30 | dhat::assert_is_panic(|| dhat::assert!(false), "dhat: assertion failed: false"); 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dhat" 3 | version = "0.3.3" 4 | authors = ["Nicholas Nethercote "] 5 | license = "MIT OR Apache-2.0" 6 | edition = "2021" 7 | description = "A library for heap profiling and ad hoc profiling with DHAT." 8 | readme = "README.md" 9 | homepage = "https://github.com/nnethercote/dhat-rs" 10 | repository = "https://github.com/nnethercote/dhat-rs" 11 | keywords = ["profiling"] 12 | categories = ["development-tools::profiling"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | backtrace = "0.3.63" 18 | mintex = "0.1.2" 19 | rustc-hash = "1.1" 20 | lazy_static = "1.4" 21 | parking_lot = "0.12" 22 | serde = { version = "1.0", features = ["derive"] } 23 | serde_json = "1.0" 24 | thousands = "0.2" 25 | 26 | [dev-dependencies] 27 | serial_test = "0.5" 28 | 29 | # Some tests require debug info. `cargo test` enables debug info, but `cargo 30 | # test --release` does not, so we enable it here. In Rust 1.56 and earlier, 31 | # `profile.bench` was used for `cargo test --release`, but in Rust 1.57 this 32 | # changed to `profile.release`. So we specify both. 33 | [profile.bench] 34 | debug = 1 35 | [profile.release] 36 | debug = 1 37 | -------------------------------------------------------------------------------- /tests/ad-hoc-panics.rs: -------------------------------------------------------------------------------- 1 | // Test most of the panics that can occur during ad hoc profiling. Because we 2 | // can't have multiple `#[test]` instances in a single test, we use 3 | // `assert_is_panic` to test multiple panics within a single `#[test]`. 4 | #[test] 5 | fn main() { 6 | dhat::assert_is_panic( 7 | || dhat::AdHocStats::get(), 8 | "dhat: getting ad hoc stats when no profiler is running", 9 | ); 10 | 11 | dhat::assert_is_panic( 12 | || dhat::assert!(true), 13 | "dhat: asserting when no profiler is running", 14 | ); 15 | 16 | { 17 | let _profiler = dhat::Profiler::new_ad_hoc(); 18 | 19 | dhat::assert_is_panic( 20 | || dhat::Profiler::new_ad_hoc(), 21 | "dhat: creating a profiler while a profiler is already running", 22 | ); 23 | 24 | dhat::assert_is_panic( 25 | || dhat::HeapStats::get(), 26 | "dhat: getting heap stats while doing ad hoc profiling", 27 | ); 28 | 29 | dhat::assert_is_panic( 30 | || dhat::assert!(true), 31 | "dhat: asserting while not in testing mode", 32 | ); 33 | } 34 | 35 | dhat::assert_is_panic( 36 | || dhat::AdHocStats::get(), 37 | "dhat: getting ad hoc stats when no profiler is running", 38 | ); 39 | 40 | dhat::assert_is_panic( 41 | || dhat::assert!(true), 42 | "dhat: asserting when no profiler is running", 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /tests/heap-panics.rs: -------------------------------------------------------------------------------- 1 | #[global_allocator] 2 | static ALLOC: dhat::Alloc = dhat::Alloc; 3 | 4 | // Test most of the panics that can occur during heap profiling. Because we 5 | // can't have multiple `#[test]` instances in a single test, we use 6 | // `assert_is_panic` to test multiple panics within a single `#[test]`. 7 | #[test] 8 | fn main() { 9 | dhat::assert_is_panic( 10 | || dhat::HeapStats::get(), 11 | "dhat: getting heap stats when no profiler is running", 12 | ); 13 | 14 | dhat::assert_is_panic( 15 | || dhat::assert!(true), 16 | "dhat: asserting when no profiler is running", 17 | ); 18 | 19 | { 20 | let _profiler = dhat::Profiler::new_heap(); 21 | 22 | dhat::assert_is_panic( 23 | || dhat::Profiler::new_heap(), 24 | "dhat: creating a profiler while a profiler is already running", 25 | ); 26 | 27 | dhat::assert_is_panic( 28 | || dhat::AdHocStats::get(), 29 | "dhat: getting ad hoc stats while doing heap profiling", 30 | ); 31 | 32 | dhat::assert_is_panic( 33 | || dhat::assert!(true), 34 | "dhat: asserting while not in testing mode", 35 | ); 36 | } 37 | 38 | dhat::assert_is_panic( 39 | || dhat::HeapStats::get(), 40 | "dhat: getting heap stats when no profiler is running", 41 | ); 42 | 43 | dhat::assert_is_panic( 44 | || dhat::assert!(true), 45 | "dhat: asserting when no profiler is running", 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dhat-rs 2 | 3 | **Warning:** *This crate is experimental. It relies on implementation techniques 4 | that are hard to keep working for 100% of configurations. It may work fine for 5 | you, or it may crash, hang, or otherwise do the wrong thing. Its maintenance is 6 | not a high priority of the author. Support requests such as issues and pull 7 | requests may receive slow responses, or no response at all. Sorry!* 8 | 9 | This crate provides heap profiling and ad hoc profiling capabilities to Rust 10 | programs, similar to those provided by [DHAT]. 11 | 12 | [DHAT]: https://www.valgrind.org/docs/manual/dh-manual.html 13 | 14 | It also provides heap usage testing capabilities, which let you write tests 15 | that check things like: 16 | - "This code should do exactly 96 heap allocations". 17 | - "The peak heap usage of this code should be less than 10 MiB". 18 | - "This code should free all heap allocations before finishing". 19 | 20 | It provides helpful details if these fail. 21 | 22 | See the [crate documentation] for details on how to use it. 23 | 24 | [crate documentation]: https://docs.rs/dhat 25 | 26 | ## License 27 | 28 | Licensed under either of 29 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 30 | http://www.apache.org/licenses/LICENSE-2.0) 31 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 32 | http://opensource.org/licenses/MIT) 33 | 34 | at your option. 35 | 36 | ## Contribution 37 | 38 | Unless you explicitly state otherwise, any contribution intentionally submitted 39 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall 40 | be dual licensed as above, without any additional terms or conditions. 41 | -------------------------------------------------------------------------------- /tests/ad-hoc.rs: -------------------------------------------------------------------------------- 1 | #[global_allocator] 2 | static ALLOC: dhat::Alloc = dhat::Alloc; 3 | 4 | fn f2() { 5 | dhat::ad_hoc_event(1); 6 | dhat::ad_hoc_event(2); 7 | } 8 | 9 | fn f1() { 10 | f2(); 11 | dhat::ad_hoc_event(3); 12 | } 13 | 14 | #[test] 15 | fn main() { 16 | use serde_json::Value::{self, *}; 17 | 18 | // Ignored because profiling hasn't started yet. 19 | dhat::ad_hoc_event(3); 20 | 21 | let mem = { 22 | let mut profiler = std::mem::ManuallyDrop::new( 23 | dhat::Profiler::builder() 24 | .ad_hoc() 25 | .trim_backtraces(Some(usize::MAX)) 26 | .eprint_json() 27 | .build(), 28 | ); 29 | 30 | let stats = dhat::AdHocStats::get(); 31 | assert_eq!(stats.total_events, 0); 32 | assert_eq!(stats.total_units, 0); 33 | 34 | dhat::ad_hoc_event(100); 35 | f1(); 36 | 37 | // This should have no effect, because we're ad hoc profiling. 38 | let _v: Vec = Vec::with_capacity(100); 39 | 40 | let stats = dhat::AdHocStats::get(); 41 | assert_eq!(stats.total_events, 4); 42 | assert_eq!(stats.total_units, 106); 43 | 44 | profiler.drop_and_get_memory_output() 45 | }; 46 | 47 | // Ignored because profiling has finished. 48 | dhat::ad_hoc_event(5); 49 | 50 | // Check basics. 51 | let mut v: Value = serde_json::from_str(&mem).unwrap(); 52 | assert_eq!(v["dhatFileVersion"].as_i64().unwrap(), 2); 53 | assert_eq!(v["mode"], "rust-ad-hoc"); 54 | assert_eq!(v["verb"], "Allocated"); 55 | assert_eq!(v["bklt"], Bool(false)); 56 | assert_eq!(v["bkacc"], Bool(false)); 57 | assert!(matches!(&v["bu"], String(s) if s == "unit")); 58 | assert!(matches!(&v["bsu"], String(s) if s == "units")); 59 | assert!(matches!(&v["bksu"], String(s) if s == "events")); 60 | assert_eq!(v["tu"], "µs"); 61 | assert_eq!(v["Mtu"], "s"); 62 | assert!(matches!(&v["cmd"], String(s) if s.contains("ad_hoc"))); 63 | assert!(matches!(v["pid"], Number(_))); 64 | assert_ne!(v["te"].as_i64().unwrap(), 0); 65 | assert_eq!(v["tg"], Null); 66 | 67 | // Order PPs by "tb" field. 68 | let pps = v["pps"].as_array_mut().unwrap(); 69 | pps.sort_unstable_by_key(|pp| pp["tb"].as_i64().unwrap()); 70 | pps.reverse(); 71 | 72 | // main 73 | let pp0 = &pps[0]; 74 | assert_eq!(pp0["tb"].as_i64().unwrap(), 100); 75 | assert_eq!(pp0["tbk"].as_i64().unwrap(), 1); 76 | assert_eq!(pp0["tl"], Null); 77 | assert_eq!(pp0["mb"], Null); 78 | assert_eq!(pp0["mbk"], Null); 79 | assert_eq!(pp0["gb"], Null); 80 | assert_eq!(pp0["gbk"], Null); 81 | assert_eq!(pp0["eb"], Null); 82 | assert_eq!(pp0["ebk"], Null); 83 | assert!(matches!(pp0["fs"], Array(_))); 84 | 85 | // f1 (second) 86 | let pp1 = &pps[1]; 87 | assert_eq!(pp1["tb"].as_i64().unwrap(), 3); 88 | assert_eq!(pp1["tbk"].as_i64().unwrap(), 1); 89 | assert_eq!(pp1["tl"], Null); 90 | assert_eq!(pp1["mb"], Null); 91 | assert_eq!(pp1["mbk"], Null); 92 | assert_eq!(pp1["gb"], Null); 93 | assert_eq!(pp1["gbk"], Null); 94 | assert_eq!(pp1["eb"], Null); 95 | assert_eq!(pp1["ebk"], Null); 96 | assert!(matches!(pp1["fs"], Array(_))); 97 | 98 | // f2 (second) 99 | let pp2 = &pps[2]; 100 | assert_eq!(pp2["tb"].as_i64().unwrap(), 2); 101 | assert_eq!(pp2["tbk"].as_i64().unwrap(), 1); 102 | assert_eq!(pp2["tl"], Null); 103 | assert_eq!(pp2["mb"], Null); 104 | assert_eq!(pp2["mbk"], Null); 105 | assert_eq!(pp2["gb"], Null); 106 | assert_eq!(pp2["gbk"], Null); 107 | assert_eq!(pp2["eb"], Null); 108 | assert_eq!(pp2["ebk"], Null); 109 | assert!(matches!(pp2["fs"], Array(_))); 110 | 111 | // f2 (first) 112 | let pp3 = &pps[3]; 113 | assert_eq!(pp3["tb"].as_i64().unwrap(), 1); 114 | assert_eq!(pp3["tbk"].as_i64().unwrap(), 1); 115 | assert_eq!(pp3["tl"], Null); 116 | assert_eq!(pp3["mb"], Null); 117 | assert_eq!(pp3["mbk"], Null); 118 | assert_eq!(pp3["gb"], Null); 119 | assert_eq!(pp3["gbk"], Null); 120 | assert_eq!(pp3["eb"], Null); 121 | assert_eq!(pp3["ebk"], Null); 122 | assert!(matches!(pp3["fs"], Array(_))); 123 | 124 | let ftbl = &v["ftbl"].as_array().unwrap(); 125 | let y = |s| { 126 | ftbl.iter() 127 | .find(|&f| f.as_str().unwrap().contains(s)) 128 | .is_some() 129 | }; 130 | let n = |s| { 131 | ftbl.iter() 132 | .find(|&f| f.as_str().unwrap().contains(s)) 133 | .is_none() 134 | }; 135 | assert!(y("[root]")); 136 | assert!(y("dhat::ad_hoc_event")); 137 | 138 | // These tests will fail if the repo directory isn't called `dhat-rs`. 139 | if cfg!(windows) { 140 | // Some frames are missing on Windows, not sure why. 141 | assert!(y("ad_hoc::f2 (dhat-rs\\tests\\ad-hoc.rs:5:0)")); 142 | assert!(y("ad_hoc::f2 (dhat-rs\\tests\\ad-hoc.rs:6:0)")); 143 | //assert!(y("ad_hoc::f1 (dhat-rs\\tests\\ad-hoc.rs:10:0)")); 144 | assert!(y("ad_hoc::f1 (dhat-rs\\tests\\ad-hoc.rs:11:0)")); 145 | assert!(y("ad_hoc::main (dhat-rs\\tests\\ad-hoc.rs:34:0)")); 146 | //assert!(y("ad_hoc::main (dhat-rs\\tests\\ad-hoc.rs:35:0)")); 147 | } else { 148 | assert!(y("ad_hoc::f2 (dhat-rs/tests/ad-hoc.rs:5:5)")); 149 | assert!(y("ad_hoc::f2 (dhat-rs/tests/ad-hoc.rs:6:5)")); 150 | assert!(y("ad_hoc::f1 (dhat-rs/tests/ad-hoc.rs:10:5)")); 151 | assert!(y("ad_hoc::f1 (dhat-rs/tests/ad-hoc.rs:11:5)")); 152 | assert!(y("ad_hoc::main (dhat-rs/tests/ad-hoc.rs:34:9)")); 153 | assert!(y("ad_hoc::main (dhat-rs/tests/ad-hoc.rs:35:9)")); 154 | } 155 | 156 | // This stuff should be removed by backtrace trimming. 157 | assert!(n("backtrace::")); 158 | assert!(n("lang_start::")); 159 | assert!(n("call_once::")); 160 | assert!(n("catch_unwind::")); 161 | assert!(n("panic")); 162 | 163 | // A trivial second profiler in the same run, albeit a heap profiler. 164 | let _profiler = dhat::Profiler::new_heap(); 165 | } 166 | -------------------------------------------------------------------------------- /tests/heap.rs: -------------------------------------------------------------------------------- 1 | #[global_allocator] 2 | static ALLOC: dhat::Alloc = dhat::Alloc; 3 | 4 | #[test] 5 | fn main() { 6 | use serde_json::Value::{self, *}; 7 | 8 | // Allocated before heap profiling starts. 9 | let v1 = vec![1u32, 2, 3, 4]; 10 | let v2 = vec![1u32, 2, 3, 4]; 11 | let mut v3 = vec![1u32, 2, 3, 4]; 12 | let mut v4 = vec![1u32, 2, 3, 4]; 13 | 14 | let mem = { 15 | let mut profiler = std::mem::ManuallyDrop::new( 16 | dhat::Profiler::builder() 17 | .trim_backtraces(Some(usize::MAX)) 18 | .eprint_json() 19 | .build(), 20 | ); 21 | 22 | // Things allocated beforehand aren't counted. 23 | let stats = dhat::HeapStats::get(); 24 | assert_eq!(stats.total_blocks, 0); 25 | assert_eq!(stats.total_bytes, 0); 26 | assert_eq!(stats.curr_blocks, 0); 27 | assert_eq!(stats.curr_bytes, 0); 28 | assert_eq!(stats.max_blocks, 0); 29 | assert_eq!(stats.max_bytes, 0); 30 | 31 | // Allocated before, freed during. 32 | drop(v1); 33 | 34 | // Allocated before, reallocated during. Treated like a fresh alloc. 35 | v3.push(5); 36 | 37 | // Allocated during, reallocated/dropped during. 38 | let v5 = vec![0u8; 1000]; 39 | let mut v6 = vec![0u8; 200]; 40 | v6.reserve(200); // global peak 41 | drop(v5); 42 | 43 | // Need to play some games with variable addresses here to prevent 44 | // rustc from either (a) constant folding this loop into nothing, or 45 | // (b) unrolling it. 46 | let mut x = 0usize; 47 | let addr = &x as *const usize as usize; // always be bigger than 10 48 | for i in 0..std::cmp::min(10, addr) { 49 | let v7 = vec![i as u8]; 50 | x += v7.as_ptr() as usize; // can't unroll this 51 | } 52 | assert_ne!(x, 0); 53 | 54 | let stats = dhat::HeapStats::get(); 55 | assert_eq!(stats.total_blocks, 14); 56 | assert_eq!(stats.total_bytes, 1642); 57 | assert_eq!(stats.curr_blocks, 2); 58 | assert_eq!(stats.curr_bytes, 432); 59 | assert_eq!(stats.max_blocks, 3); 60 | assert_eq!(stats.max_bytes, 1432); 61 | 62 | drop(v6); 63 | profiler.drop_and_get_memory_output() 64 | }; 65 | 66 | // Allocated before, freed after. 67 | drop(v2); 68 | 69 | // Allocated before, reallocated after. 70 | v4.push(5); 71 | 72 | // Check basics. 73 | let mut v: Value = serde_json::from_str(&mem).unwrap(); 74 | assert_eq!(v["dhatFileVersion"].as_i64().unwrap(), 2); 75 | assert_eq!(v["mode"], "rust-heap"); 76 | assert_eq!(v["verb"], "Allocated"); 77 | assert_eq!(v["bklt"], Bool(true)); 78 | assert_eq!(v["bkacc"], Bool(false)); 79 | assert_eq!(v["bu"], Null); 80 | assert_eq!(v["bsu"], Null); 81 | assert_eq!(v["bksu"], Null); 82 | assert_eq!(v["tu"], "µs"); 83 | assert_eq!(v["Mtu"], "s"); 84 | assert_eq!(v["tuth"].as_i64().unwrap(), 10); 85 | assert!(matches!(&v["cmd"], String(s) if s.contains("heap"))); 86 | assert!(matches!(v["pid"], Number(_))); 87 | assert!(v["te"].as_i64().unwrap() >= v["tg"].as_i64().unwrap()); 88 | 89 | // Order PPs by "tb" field. 90 | let pps = v["pps"].as_array_mut().unwrap(); 91 | pps.sort_unstable_by_key(|pp| pp["tb"].as_i64().unwrap()); 92 | pps.reverse(); 93 | 94 | // v5 95 | let pp0 = &pps[0]; 96 | assert_eq!(pp0["tb"].as_i64().unwrap(), 1000); 97 | assert_eq!(pp0["tbk"].as_i64().unwrap(), 1); 98 | assert!(pp0["tl"].as_i64().unwrap() > 0); 99 | assert_eq!(pp0["mb"].as_i64().unwrap(), 1000); 100 | assert_eq!(pp0["mbk"].as_i64().unwrap(), 1); 101 | assert_eq!(pp0["gb"].as_i64().unwrap(), 1000); 102 | assert_eq!(pp0["gbk"].as_i64().unwrap(), 1); 103 | assert_eq!(pp0["eb"].as_i64().unwrap(), 0); 104 | assert_eq!(pp0["ebk"].as_i64().unwrap(), 0); 105 | assert!(matches!(pp0["fs"], Array(_))); 106 | 107 | // v6 108 | let pp1 = &pps[1]; 109 | assert_eq!(pp1["tb"].as_i64().unwrap(), 600); 110 | assert_eq!(pp1["tbk"].as_i64().unwrap(), 2); 111 | assert!(pp1["tl"].as_i64().unwrap() > 0); 112 | assert_eq!(pp1["mb"].as_i64().unwrap(), 400); 113 | assert_eq!(pp1["mbk"].as_i64().unwrap(), 1); 114 | assert_eq!(pp1["gb"].as_i64().unwrap(), 400); 115 | assert_eq!(pp1["gbk"].as_i64().unwrap(), 1); 116 | assert_eq!(pp1["eb"].as_i64().unwrap(), 0); 117 | assert_eq!(pp1["ebk"].as_i64().unwrap(), 0); 118 | assert!(matches!(pp1["fs"], Array(_))); 119 | 120 | // v3 121 | let pp2 = &pps[2]; 122 | assert_eq!(pp2["tb"].as_i64().unwrap(), 32); 123 | assert_eq!(pp2["tbk"].as_i64().unwrap(), 1); 124 | assert!(pp2["tl"].as_i64().unwrap() > 0); 125 | assert_eq!(pp2["mb"].as_i64().unwrap(), 32); 126 | assert_eq!(pp2["mbk"].as_i64().unwrap(), 1); 127 | assert_eq!(pp2["gb"].as_i64().unwrap(), 32); 128 | assert_eq!(pp2["gbk"].as_i64().unwrap(), 1); 129 | assert_eq!(pp2["eb"].as_i64().unwrap(), 32); 130 | assert_eq!(pp2["ebk"].as_i64().unwrap(), 1); 131 | assert!(matches!(pp2["fs"], Array(_))); 132 | 133 | // _v7 134 | let pp3 = &pps[3]; 135 | assert_eq!(pp3["tb"].as_i64().unwrap(), 10); 136 | assert_eq!(pp3["tbk"].as_i64().unwrap(), 10); 137 | assert!(pp3["tl"].as_i64().unwrap() >= 0); 138 | assert_eq!(pp3["mb"].as_i64().unwrap(), 1); 139 | assert_eq!(pp3["mbk"].as_i64().unwrap(), 1); 140 | assert_eq!(pp3["gb"].as_i64().unwrap(), 0); 141 | assert_eq!(pp3["gbk"].as_i64().unwrap(), 0); 142 | assert_eq!(pp3["eb"].as_i64().unwrap(), 0); 143 | assert_eq!(pp3["ebk"].as_i64().unwrap(), 0); 144 | assert!(matches!(pp3["fs"], Array(_))); 145 | 146 | // Look for parts of some expected frames. 147 | let ftbl = &v["ftbl"].as_array().unwrap(); 148 | let y = |s| { 149 | ftbl.iter() 150 | .find(|&f| f.as_str().unwrap().contains(s)) 151 | .is_some() 152 | }; 153 | let n = |s| { 154 | ftbl.iter() 155 | .find(|&f| f.as_str().unwrap().contains(s)) 156 | .is_none() 157 | }; 158 | assert!(y("[root]")); 159 | 160 | // These tests will fail if the repo directory isn't called `dhat-rs`. 161 | if cfg!(windows) { 162 | if cfg!(debug_assertions) { 163 | assert!(y( 164 | "alloc::vec::Vec::push" 165 | )); 166 | assert!(y("heap::main (dhat-rs\\tests\\heap.rs:35:0)")); // v3 167 | assert!(y("heap::main (dhat-rs\\tests\\heap.rs:38:0)")); // v5 168 | assert!(y("heap::main (dhat-rs\\tests\\heap.rs:39:0)")); // v6 169 | assert!(y("heap::main (dhat-rs\\tests\\heap.rs:49:0)")); // _v7 170 | } else { 171 | // Stack traces are terrible in Windows release builds. 172 | assert!(y("RawVec")); 173 | } 174 | } else { 175 | assert!(y("alloc::vec::Vec::push")); 176 | assert!(y("heap::main (dhat-rs/tests/heap.rs:35:9)")); // v3 177 | assert!(y("heap::main (dhat-rs/tests/heap.rs:38:18)")); // v5 178 | assert!(y("heap::main (dhat-rs/tests/heap.rs:39:22)")); // v6 179 | assert!(y("heap::main (dhat-rs/tests/heap.rs:49:22)")); // _v7 180 | } 181 | 182 | // This stuff should be removed by backtrace trimming. 183 | assert!(n("backtrace::")); 184 | assert!(n("alloc::alloc::Global::alloc_impl")); 185 | assert!(n("")); 186 | assert!(n("alloc::alloc::Global::alloc_impl")); 187 | assert!(n("__rg_")); 188 | assert!(n("lang_start::")); 189 | assert!(n("call_once::")); 190 | assert!(n("catch_unwind::")); 191 | assert!(n("panic")); 192 | 193 | // A trivial second profiler in the same run, albeit an ad hoc profiler. 194 | let _profiler = dhat::Profiler::new_ad_hoc(); 195 | } 196 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![deny(rustdoc::missing_doc_code_examples)] 3 | #![deny(missing_debug_implementations)] 4 | 5 | //! **Warning:** *This crate is experimental. It relies on implementation 6 | //! techniques that are hard to keep working for 100% of configurations. It may 7 | //! work fine for you, or it may crash, hang, or otherwise do the wrong thing. 8 | //! Its maintenance is not a high priority of the author. Support requests such 9 | //! as issues and pull requests may receive slow responses, or no response at 10 | //! all. Sorry!* 11 | //! 12 | //! This crate provides heap profiling and [ad hoc profiling] capabilities to 13 | //! Rust programs, similar to those provided by [DHAT]. 14 | //! 15 | //! [ad hoc profiling]: https://github.com/nnethercote/counts/#ad-hoc-profiling 16 | //! [DHAT]: https://www.valgrind.org/docs/manual/dh-manual.html 17 | //! 18 | //! The heap profiling works by using a global allocator that wraps the system 19 | //! allocator, tracks all heap allocations, and on program exit writes data to 20 | //! file so it can be viewed with DHAT's viewer. This corresponds to DHAT's 21 | //! `--mode=heap` mode. 22 | //! 23 | //! The ad hoc profiling is via a second mode of operation, where ad hoc events 24 | //! can be manually inserted into a Rust program for aggregation and viewing. 25 | //! This corresponds to DHAT's `--mode=ad-hoc` mode. 26 | //! 27 | //! `dhat` also supports *heap usage testing*, where you can write tests and 28 | //! then check that they allocated as much heap memory as you expected. This 29 | //! can be useful for performance-sensitive code. 30 | //! 31 | //! # Motivation 32 | //! 33 | //! DHAT is a powerful heap profiler that comes with Valgrind. This crate is a 34 | //! related but alternative choice for heap profiling Rust programs. DHAT and 35 | //! this crate have the following differences. 36 | //! - This crate works on any platform, while DHAT only works on some platforms 37 | //! (Linux, mostly). (Note that DHAT's viewer is just HTML+JS+CSS and should 38 | //! work in any modern web browser on any platform.) 39 | //! - This crate typically causes a smaller slowdown than DHAT. 40 | //! - This crate requires some modifications to a program's source code and 41 | //! recompilation, while DHAT does not. 42 | //! - This crate cannot track memory accesses the way DHAT does, because it does 43 | //! not instrument all memory loads and stores. 44 | //! - This crate does not provide profiling of copy functions such as `memcpy` 45 | //! and `strcpy`, unlike DHAT. 46 | //! - The backtraces produced by this crate may be better than those produced 47 | //! by DHAT. 48 | //! - DHAT measures a program's entire execution, but this crate only measures 49 | //! what happens within `main`. It will miss the small number of allocations 50 | //! that occur before or after `main`, within the Rust runtime. 51 | //! - This crate enables heap usage testing. 52 | //! 53 | //! # Configuration (profiling and testing) 54 | //! 55 | //! In your `Cargo.toml` file, as well as specifying `dhat` as a dependency, 56 | //! you should (a) enable source line debug info, and (b) create a feature or 57 | //! two that lets you easily switch profiling on and off: 58 | //! ```toml 59 | //! [profile.release] 60 | //! debug = 1 61 | //! 62 | //! [features] 63 | //! dhat-heap = [] # if you are doing heap profiling 64 | //! dhat-ad-hoc = [] # if you are doing ad hoc profiling 65 | //! ``` 66 | //! You should only use `dhat` in release builds. Debug builds are too slow to 67 | //! be useful. 68 | //! 69 | //! # Setup (heap profiling) 70 | //! 71 | //! For heap profiling, enable the global allocator by adding this code to your 72 | //! program: 73 | //! ``` 74 | //! # // Tricky: comment out the `cfg` so it shows up in docs but the following 75 | //! # // line is still tsted by `cargo test`. 76 | //! # /* 77 | //! #[cfg(feature = "dhat-heap")] 78 | //! # */ 79 | //! #[global_allocator] 80 | //! static ALLOC: dhat::Alloc = dhat::Alloc; 81 | //! ``` 82 | //! Then add the following code to the very start of your `main` function: 83 | //! ``` 84 | //! # // Tricky: comment out the `cfg` so it shows up in docs but the following 85 | //! # // line is still tsted by `cargo test`. 86 | //! # /* 87 | //! #[cfg(feature = "dhat-heap")] 88 | //! # */ 89 | //! let _profiler = dhat::Profiler::new_heap(); 90 | //! ``` 91 | //! Then run this command to enable heap profiling during the lifetime of the 92 | //! [`Profiler`] instance: 93 | //! ```text 94 | //! cargo run --features dhat-heap 95 | //! ``` 96 | //! [`dhat::Alloc`](Alloc) is slower than the normal allocator, so it should 97 | //! only be enabled while profiling. 98 | //! 99 | //! # Setup (ad hoc profiling) 100 | //! 101 | //! [Ad hoc profiling] involves manually annotating hot code points and then 102 | //! aggregating the executed annotations in some fashion. 103 | //! 104 | //! [Ad hoc profiling]: https://github.com/nnethercote/counts/#ad-hoc-profiling 105 | //! 106 | //! To do this, add the following code to the very start of your `main` 107 | //! function: 108 | //!``` 109 | //! # // Tricky: comment out the `cfg` so it shows up in docs but the following 110 | //! # // line is still tsted by `cargo test`. 111 | //! # /* 112 | //! #[cfg(feature = "dhat-ad-hoc")] 113 | //! # */ 114 | //! let _profiler = dhat::Profiler::new_ad_hoc(); 115 | //! ``` 116 | //! Then insert calls like this at points of interest: 117 | //! ``` 118 | //! # // Tricky: comment out the `cfg` so it shows up in docs but the following 119 | //! # // line is still tsted by `cargo test`. 120 | //! # /* 121 | //! #[cfg(feature = "dhat-ad-hoc")] 122 | //! # */ 123 | //! dhat::ad_hoc_event(100); 124 | //! ``` 125 | //! Then run this command to enable ad hoc profiling during the lifetime of the 126 | //! [`Profiler`] instance: 127 | //! ```text 128 | //! cargo run --features dhat-ad-hoc 129 | //! ``` 130 | //! For example, imagine you have a hot function that is called from many call 131 | //! sites. You might want to know how often it is called and which other 132 | //! functions called it the most. In that case, you would add an 133 | //! [`ad_hoc_event`] call to that function, and the data collected by this 134 | //! crate and viewed with DHAT's viewer would show you exactly what you want to 135 | //! know. 136 | //! 137 | //! The meaning of the integer argument to `ad_hoc_event` will depend on 138 | //! exactly what you are measuring. If there is no meaningful weight to give to 139 | //! an event, you can just use `1`. 140 | //! 141 | //! # Running 142 | //! 143 | //! For both heap profiling and ad hoc profiling, the program will run more 144 | //! slowly than normal. The exact slowdown is hard to predict because it 145 | //! depends greatly on the program being profiled, but it can be large. (Even 146 | //! more so on Windows, because backtrace gathering can be drastically slower 147 | //! on Windows than on other platforms.) 148 | //! 149 | //! When the [`Profiler`] is dropped at the end of `main`, some basic 150 | //! information will be printed to `stderr`. For heap profiling it will look 151 | //! like the following. 152 | //! ```text 153 | //! dhat: Total: 1,256 bytes in 6 blocks 154 | //! dhat: At t-gmax: 1,256 bytes in 6 blocks 155 | //! dhat: At t-end: 1,256 bytes in 6 blocks 156 | //! dhat: The data has been saved to dhat-heap.json, and is viewable with dhat/dh_view.html 157 | //! ``` 158 | //! ("Blocks" is a synonym for "allocations".) 159 | //! 160 | //! For ad hoc profiling it will look like the following. 161 | //! ```text 162 | //! dhat: Total: 141 units in 11 events 163 | //! dhat: The data has been saved to dhat-ad-hoc.json, and is viewable with dhat/dh_view.html 164 | //! ``` 165 | //! A file called `dhat-heap.json` (for heap profiling) or `dhat-ad-hoc.json` 166 | //! (for ad hoc profiling) will be written. It can be viewed in DHAT's viewer. 167 | //! 168 | //! If you don't see this output, it may be because your program called 169 | //! [`std::process::exit`], which exits a program without running any 170 | //! destructors. To work around this, explicitly call `drop` on the 171 | //! [`Profiler`] value just before exiting. 172 | //! 173 | //! When doing heap profiling, if you unexpectedly see zero allocations in the 174 | //! output it may be because you forgot to set [`dhat::Alloc`](Alloc) as the 175 | //! global allocator. 176 | //! 177 | //! When doing heap profiling it is recommended that the lifetime of the 178 | //! [`Profiler`] value cover all of `main`. But it is still possible for 179 | //! allocations and deallocations to occur outside of its lifetime. Such cases 180 | //! are handled in the following ways. 181 | //! - Allocated before, untouched within: ignored. 182 | //! - Allocated before, freed within: ignored. 183 | //! - Allocated before, reallocated within: treated like a new allocation 184 | //! within. 185 | //! - Allocated after: ignored. 186 | //! 187 | //! These cases are not ideal, but it is impossible to do better. `dhat` 188 | //! deliberately provides no way to reset the heap profiling state mid-run 189 | //! precisely because it leaves open the possibility of many such occurrences. 190 | //! 191 | //! # Viewing 192 | //! 193 | //! Open a copy of DHAT's viewer, version 3.17 or later. There are two ways to 194 | //! do this. 195 | //! - Easier: Use the [online version]. 196 | //! - Harder: Clone the [Valgrind repository] with `git clone 197 | //! git://sourceware.org/git/valgrind.git` and open `dhat/dh_view.html`. 198 | //! There is no need to build any code in this repository. 199 | //! 200 | //! [online version]: https://nnethercote.github.io/dh_view/dh_view.html 201 | //! [Valgrind repository]: https://www.valgrind.org/downloads/repository.html 202 | //! 203 | //! Then click on the "Load…" button to load `dhat-heap.json` or 204 | //! `dhat-ad-hoc.json`. 205 | //! 206 | //! DHAT's viewer shows a tree with nodes that look like this. 207 | //! ```text 208 | //! PP 1.1/2 { 209 | //! Total: 1,024 bytes (98.46%, 14,422,535.21/s) in 1 blocks (50%, 14,084.51/s), avg size 1,024 bytes, avg lifetime 35 µs (49.3% of program duration) 210 | //! Max: 1,024 bytes in 1 blocks, avg size 1,024 bytes 211 | //! At t-gmax: 1,024 bytes (98.46%) in 1 blocks (50%), avg size 1,024 bytes 212 | //! At t-end: 1,024 bytes (100%) in 1 blocks (100%), avg size 1,024 bytes 213 | //! Allocated at { 214 | //! #1: 0x10ae8441b: ::allocate (alloc/src/alloc.rs:226:9) 215 | //! #2: 0x10ae8441b: alloc::raw_vec::RawVec::allocate_in (alloc/src/raw_vec.rs:207:45) 216 | //! #3: 0x10ae8441b: alloc::raw_vec::RawVec::with_capacity_in (alloc/src/raw_vec.rs:146:9) 217 | //! #4: 0x10ae8441b: alloc::vec::Vec::with_capacity_in (src/vec/mod.rs:609:20) 218 | //! #5: 0x10ae8441b: alloc::vec::Vec::with_capacity (src/vec/mod.rs:470:9) 219 | //! #6: 0x10ae8441b: std::io::buffered::bufwriter::BufWriter::with_capacity (io/buffered/bufwriter.rs:115:33) 220 | //! #7: 0x10ae8441b: std::io::buffered::linewriter::LineWriter::with_capacity (io/buffered/linewriter.rs:109:29) 221 | //! #8: 0x10ae8441b: std::io::buffered::linewriter::LineWriter::new (io/buffered/linewriter.rs:89:9) 222 | //! #9: 0x10ae8441b: std::io::stdio::stdout::{{closure}} (src/io/stdio.rs:680:58) 223 | //! #10: 0x10ae8441b: std::lazy::SyncOnceCell::get_or_init_pin::{{closure}} (std/src/lazy.rs:375:25) 224 | //! #11: 0x10ae8441b: std::sync::once::Once::call_once_force::{{closure}} (src/sync/once.rs:320:40) 225 | //! #12: 0x10aea564c: std::sync::once::Once::call_inner (src/sync/once.rs:419:21) 226 | //! #13: 0x10ae81b1b: std::sync::once::Once::call_once_force (src/sync/once.rs:320:9) 227 | //! #14: 0x10ae81b1b: std::lazy::SyncOnceCell::get_or_init_pin (std/src/lazy.rs:374:9) 228 | //! #15: 0x10ae81b1b: std::io::stdio::stdout (src/io/stdio.rs:679:16) 229 | //! #16: 0x10ae81b1b: std::io::stdio::print_to (src/io/stdio.rs:1196:21) 230 | //! #17: 0x10ae81b1b: std::io::stdio::_print (src/io/stdio.rs:1209:5) 231 | //! #18: 0x10ae2fe20: dhatter::main (dhatter/src/main.rs:8:5) 232 | //! } 233 | //! } 234 | //! ``` 235 | //! Full details about the output are in the [DHAT documentation]. Note that 236 | //! DHAT uses the word "block" as a synonym for "allocation". 237 | //! 238 | //! [DHAT documentation]: https://valgrind.org/docs/manual/dh-manual.html 239 | //! 240 | //! When heap profiling, this crate doesn't track memory accesses (unlike DHAT) 241 | //! and so the "reads" and "writes" measurements are not shown within DHAT's 242 | //! viewer, and "sort metric" views involving reads, writes, or accesses are 243 | //! not available. 244 | //! 245 | //! The backtraces produced by this crate are trimmed to reduce output file 246 | //! sizes and improve readability in DHAT's viewer, in the following ways. 247 | //! - Only one allocation-related frame will be shown at the top of the 248 | //! backtrace. That frame may be a function within `alloc::alloc`, a function 249 | //! within this crate, or a global allocation function like `__rg_alloc`. 250 | //! - Common frames at the bottom of all backtraces, below `main`, are omitted. 251 | //! 252 | //! Backtrace trimming is inexact and if the above heuristics fail more frames 253 | //! will be shown. [`ProfilerBuilder::trim_backtraces`] allows (approximate) 254 | //! control of how deep backtraces will be. 255 | //! 256 | //! # Heap usage testing 257 | //! 258 | //! `dhat` lets you write tests that check that a certain piece of code does a 259 | //! certain amount of heap allocation when it runs. This is sometimes called 260 | //! "high water mark" testing. Sometimes it is precise (e.g. "this code should 261 | //! do exactly 96 allocations" or "this code should free all allocations before 262 | //! finishing") and sometimes it is less precise (e.g. "the peak heap usage of 263 | //! this code should be less than 10 MiB"). 264 | //! 265 | //! These tests are somewhat fragile, because heap profiling involves global 266 | //! state (allocation stats), which introduces complications. 267 | //! - `dhat` will panic if more than one `Profiler` is running at a time, but 268 | //! Rust tests run in parallel by default. So parallel running of heap usage 269 | //! tests must be prevented. 270 | //! - If you use something like the 271 | //! [`serial_test`](https://docs.rs/serial_test/) crate to run heap usage 272 | //! tests in serial, Rust's test runner code by default still runs in 273 | //! parallel with those tests, and it allocates memory. These allocations 274 | //! will be counted by the `Profiler` as if they are part of the test, which 275 | //! will likely cause test failures. 276 | //! 277 | //! Therefore, the best approach is to put each heap usage test in its own 278 | //! integration test file. Each integration test runs in its own process, and 279 | //! so cannot interfere with any other test. Also, if there is only one test in 280 | //! an integration test file, Rust's test runner code does not use any 281 | //! parallelism, and so will not interfere with the test. If you do this, a 282 | //! simple `cargo test` will work as expected. 283 | //! 284 | //! Alternatively, if you really want multiple heap usage tests in a single 285 | //! integration test file you can write your own [custom test harness], which 286 | //! is simpler than it sounds. 287 | //! 288 | //! [custom test harness]: https://www.infinyon.com/blog/2021/04/rust-custom-test-harness/ 289 | //! 290 | //! But integration tests have some limits. For example, they only be used to 291 | //! test items from libraries, not binaries. One way to get around this is to 292 | //! restructure things so that most of the functionality is in a library, and 293 | //! the binary is a thin wrapper around the library. 294 | //! 295 | //! Failing that, a blunt fallback is to run `cargo tests -- --test-threads=1`. 296 | //! This disables all parallelism in tests, avoiding all the problems. This 297 | //! allows the use of unit tests and multiples tests per integration test file, 298 | //! at the cost of a non-standard invocation and slower test execution. 299 | //! 300 | //! With all that in mind, configuration of `Cargo.toml` is much the same as 301 | //! for the profiling use case. 302 | //! 303 | //! Here is an example showing what is possible. This code would go in an 304 | //! integration test within a crate's `tests/` directory: 305 | //! ``` 306 | //! #[global_allocator] 307 | //! static ALLOC: dhat::Alloc = dhat::Alloc; 308 | //! 309 | //! # // Tricky: comment out the `#[test]` because it's needed in an actual 310 | //! # // test but messes up things here. 311 | //! # /* 312 | //! #[test] 313 | //! # */ 314 | //! fn test() { 315 | //! let _profiler = dhat::Profiler::builder().testing().build(); 316 | //! 317 | //! let _v1 = vec![1, 2, 3, 4]; 318 | //! let v2 = vec![5, 6, 7, 8]; 319 | //! drop(v2); 320 | //! let v3 = vec![9, 10, 11, 12]; 321 | //! drop(v3); 322 | //! 323 | //! let stats = dhat::HeapStats::get(); 324 | //! 325 | //! // Three allocations were done in total. 326 | //! dhat::assert_eq!(stats.total_blocks, 3); 327 | //! dhat::assert_eq!(stats.total_bytes, 48); 328 | //! 329 | //! // At the point of peak heap size, two allocations totalling 32 bytes existed. 330 | //! dhat::assert_eq!(stats.max_blocks, 2); 331 | //! dhat::assert_eq!(stats.max_bytes, 32); 332 | //! 333 | //! // Now a single allocation remains alive. 334 | //! dhat::assert_eq!(stats.curr_blocks, 1); 335 | //! dhat::assert_eq!(stats.curr_bytes, 16); 336 | //! } 337 | //! # test() 338 | //! ``` 339 | //! The [`testing`](ProfilerBuilder::testing) call puts the profiler into 340 | //! testing mode, which allows the stats provided by [`HeapStats::get`] to be 341 | //! checked with [`dhat::assert!`](assert) and similar assertions. These 342 | //! assertions work much the same as normal assertions, except that if any of 343 | //! them fail a heap profile will be saved. 344 | //! 345 | //! When viewing the heap profile after a test failure, the best choice of sort 346 | //! metric in the viewer will depend on which stat was involved in the 347 | //! assertion failure. 348 | //! - `total_blocks`: "Total (blocks)" 349 | //! - `total_bytes`: "Total (bytes)" 350 | //! - `max_blocks` or `max_bytes`: "At t-gmax (bytes)" 351 | //! - `curr_blocks` or `curr_bytes`: "At t-end (bytes)" 352 | //! 353 | //! This should give you a good understanding of why the assertion failed. 354 | //! 355 | //! Note: if you try this example test it may work in a debug build but fail in 356 | //! a release build. This is because the compiler may optimize away some of the 357 | //! allocations that are unused. This is a common problem for contrived 358 | //! examples but less common for real tests. The unstable 359 | //! [`std::hint::black_box`](std::hint::black_box) function may also be helpful 360 | //! in this situation. 361 | //! 362 | //! # Ad hoc usage testing 363 | //! 364 | //! Ad hoc usage testing is also possible. It can be used to ensure certain 365 | //! code points in your program are hit a particular number of times during 366 | //! execution. It works in much the same way as heap usage testing, but 367 | //! [`ProfilerBuilder::ad_hoc`] must be specified, [`AdHocStats::get`] is 368 | //! used instead of [`HeapStats::get`], and there is no possibility of Rust's 369 | //! test runner code interfering with the tests. 370 | 371 | use backtrace::SymbolName; 372 | use lazy_static::lazy_static; 373 | // In normal Rust code, the allocator is on a lower level than the mutex 374 | // implementation, which means the mutex implementation can use the allocator. 375 | // But DHAT implements an allocator which requires a mutex. If we're not 376 | // careful, this can easily result in deadlocks from circular sequences of 377 | // operations, E.g. see #18 and #25 from when we used `parking_lot::Mutex`. We 378 | // now use `mintex::Mutex`, which is guaranteed to not allocate, effectively 379 | // making the mutex implementation on a lower level than the allocator, 380 | // allowing the allocator to depend on it. 381 | use mintex::Mutex; 382 | use rustc_hash::FxHashMap; 383 | use serde::Serialize; 384 | use std::alloc::{GlobalAlloc, Layout, System}; 385 | use std::cell::Cell; 386 | use std::fs::File; 387 | use std::hash::{Hash, Hasher}; 388 | use std::io::BufWriter; 389 | use std::ops::AddAssign; 390 | use std::path::{Path, PathBuf}; 391 | use std::time::{Duration, Instant}; 392 | use thousands::Separable; 393 | 394 | lazy_static! { 395 | static ref TRI_GLOBALS: Mutex> = Mutex::new(Phase::Ready); 396 | } 397 | 398 | // State transition diagram: 399 | // 400 | // +---------------> Ready 401 | // | | 402 | // | Profiler:: | ProfilerBuilder:: 403 | // | drop_inner() | build() 404 | // | v 405 | // +---------------- Running 406 | // | | 407 | // | | check_assert_condition() 408 | // | | [if the check fails] 409 | // | v 410 | // +---------------- PostAssert 411 | // 412 | // Note: the use of `std::process::exit` or `std::mem::forget` (on the 413 | // `Profiler`) can result in termination while the profiler is still running, 414 | // i.e. it won't produce output. 415 | #[derive(PartialEq)] 416 | enum Phase { 417 | // We are ready to start running a `Profiler`. 418 | Ready, 419 | 420 | // A `Profiler` is running. 421 | Running(T), 422 | 423 | // The current `Profiler` has stopped due to as assertion failure, but 424 | // hasn't been dropped yet. 425 | PostAssert, 426 | } 427 | 428 | // Type used in frame trimming. 429 | #[derive(PartialEq)] 430 | enum TB { 431 | Top, 432 | Bottom, 433 | } 434 | 435 | // Global state that can be accessed from any thread and is therefore protected 436 | // by a `Mutex`. 437 | struct Globals { 438 | // The file name for the saved data. 439 | file_name: PathBuf, 440 | 441 | // Are we in testing mode? 442 | testing: bool, 443 | 444 | // How should we trim backtraces? 445 | trim_backtraces: Option, 446 | 447 | // Print the JSON to stderr when saving it? 448 | eprint_json: bool, 449 | 450 | // The backtrace at startup. Used for backtrace trimmming. 451 | start_bt: Backtrace, 452 | 453 | // Frames to trim at the top and bottom of backtraces. Computed once the 454 | // first backtrace is obtained during profiling; that backtrace is then 455 | // compared to `start_bt`. 456 | // 457 | // Each element is the address of a frame, and thus actually a `*mut 458 | // c_void`, but we store it as a `usize` because (a) we never dereference 459 | // it, and (b) using `*mut c_void` leads to compile errors because raw 460 | // pointers don't implement `Send`. 461 | frames_to_trim: Option>, 462 | 463 | // When `Globals` is created, which is when the `Profiler` is created. 464 | start_instant: Instant, 465 | 466 | // All the `PpInfos` gathered during execution. Elements are never deleted. 467 | // Each element is referred to by exactly one `Backtrace` from 468 | // `backtraces`, and referred to by any number of live blocks from 469 | // `live_blocks`. Storing all the `PpInfos` in a `Vec` is a bit clumsy, but 470 | // allows multiple references from `backtraces` and `live_blocks` without 471 | // requiring any unsafety, because the references are just indices rather 472 | // than `Rc`s or raw pointers or whatever. 473 | pp_infos: Vec, 474 | 475 | // Each `Backtrace` is associated with a `PpInfo`. The `usize` is an index 476 | // into `pp_infos`. Entries are not deleted during execution. 477 | backtraces: FxHashMap, 478 | 479 | // Counts for the entire run. 480 | total_blocks: u64, // For ad hoc profiling it's actually `total_events`. 481 | total_bytes: u64, // For ad hoc profiling it's actually `total_units`. 482 | 483 | // Extra things kept when heap profiling. 484 | heap: Option, 485 | } 486 | 487 | struct HeapGlobals { 488 | // Each live block is associated with a `PpInfo`. An element is deleted 489 | // when the corresponding allocation is freed. 490 | // 491 | // Each key is the address of a live block, and thus actually a `*mut u8`, 492 | // but we store it as a `usize` because (a) we never dereference it, and 493 | // (b) using `*mut u8` leads to compile errors because raw pointers don't 494 | // implement `Send`. 495 | live_blocks: FxHashMap, 496 | 497 | // Current counts. 498 | curr_blocks: usize, 499 | curr_bytes: usize, 500 | 501 | // Counts at the global max, i.e. when `curr_bytes` peaks. 502 | max_blocks: usize, 503 | max_bytes: usize, 504 | 505 | // Time of the global max. 506 | tgmax_instant: Instant, 507 | } 508 | 509 | impl Globals { 510 | fn new( 511 | testing: bool, 512 | file_name: PathBuf, 513 | trim_backtraces: Option, 514 | eprint_json: bool, 515 | heap: Option, 516 | ) -> Self { 517 | Self { 518 | testing, 519 | file_name, 520 | trim_backtraces, 521 | eprint_json, 522 | // `None` here because we don't want any frame trimming for this 523 | // backtrace. 524 | start_bt: new_backtrace_inner(None, &FxHashMap::default()), 525 | frames_to_trim: None, 526 | start_instant: Instant::now(), 527 | pp_infos: Vec::default(), 528 | backtraces: FxHashMap::default(), 529 | total_blocks: 0, 530 | total_bytes: 0, 531 | heap, 532 | } 533 | } 534 | 535 | // Get the PpInfo for this backtrace, creating it if necessary. 536 | fn get_pp_info PpInfo>(&mut self, bt: Backtrace, new: F) -> usize { 537 | let pp_infos = &mut self.pp_infos; 538 | *self.backtraces.entry(bt).or_insert_with(|| { 539 | let pp_info_idx = pp_infos.len(); 540 | pp_infos.push(new()); 541 | pp_info_idx 542 | }) 543 | } 544 | 545 | fn record_block(&mut self, ptr: *mut u8, pp_info_idx: usize, now: Instant) { 546 | let h = self.heap.as_mut().unwrap(); 547 | let old = h.live_blocks.insert( 548 | ptr as usize, 549 | LiveBlock { 550 | pp_info_idx, 551 | allocation_instant: now, 552 | }, 553 | ); 554 | std::assert!(matches!(old, None)); 555 | } 556 | 557 | fn update_counts_for_alloc( 558 | &mut self, 559 | pp_info_idx: usize, 560 | size: usize, 561 | delta: Option, 562 | now: Instant, 563 | ) { 564 | self.total_blocks += 1; 565 | self.total_bytes += size as u64; 566 | 567 | let h = self.heap.as_mut().unwrap(); 568 | if let Some(delta) = delta { 569 | // realloc 570 | h.curr_blocks += 0; // unchanged 571 | h.curr_bytes += delta; 572 | } else { 573 | // alloc 574 | h.curr_blocks += 1; 575 | h.curr_bytes += size; 576 | } 577 | 578 | // The use of `>=` not `>` means that if there are multiple equal peaks 579 | // we record the latest one, like `check_for_global_peak` does. 580 | if h.curr_bytes >= h.max_bytes { 581 | h.max_blocks = h.curr_blocks; 582 | h.max_bytes = h.curr_bytes; 583 | h.tgmax_instant = now; 584 | } 585 | 586 | self.pp_infos[pp_info_idx].update_counts_for_alloc(size, delta); 587 | } 588 | 589 | fn update_counts_for_dealloc( 590 | &mut self, 591 | pp_info_idx: usize, 592 | size: usize, 593 | alloc_duration: Duration, 594 | ) { 595 | let h = self.heap.as_mut().unwrap(); 596 | h.curr_blocks -= 1; 597 | h.curr_bytes -= size; 598 | 599 | self.pp_infos[pp_info_idx].update_counts_for_dealloc(size, alloc_duration); 600 | } 601 | 602 | fn update_counts_for_ad_hoc_event(&mut self, pp_info_idx: usize, weight: usize) { 603 | std::assert!(self.heap.is_none()); 604 | self.total_blocks += 1; 605 | self.total_bytes += weight as u64; 606 | 607 | self.pp_infos[pp_info_idx].update_counts_for_ad_hoc_event(weight); 608 | } 609 | 610 | // If we are at peak memory, update `at_tgmax_{blocks,bytes}` in all 611 | // `PpInfo`s. This is somewhat expensive so we avoid calling it on every 612 | // allocation; instead we call it upon a deallocation (when we might be 613 | // coming down from a global peak) and at termination (when we might be at 614 | // a global peak). 615 | fn check_for_global_peak(&mut self) { 616 | let h = self.heap.as_mut().unwrap(); 617 | if h.curr_bytes == h.max_bytes { 618 | // It's a peak. (If there are multiple equal peaks we record the 619 | // latest one.) Record it in every PpInfo. 620 | for pp_info in self.pp_infos.iter_mut() { 621 | let h = pp_info.heap.as_mut().unwrap(); 622 | h.at_tgmax_blocks = h.curr_blocks; 623 | h.at_tgmax_bytes = h.curr_bytes; 624 | } 625 | } 626 | } 627 | 628 | fn get_heap_stats(&self) -> HeapStats { 629 | match &self.heap { 630 | Some(heap) => HeapStats { 631 | total_blocks: self.total_blocks, 632 | total_bytes: self.total_bytes, 633 | curr_blocks: heap.curr_blocks, 634 | curr_bytes: heap.curr_bytes, 635 | max_blocks: heap.max_blocks, 636 | max_bytes: heap.max_bytes, 637 | }, 638 | None => panic!("dhat: getting heap stats while doing ad hoc profiling"), 639 | } 640 | } 641 | 642 | fn get_ad_hoc_stats(&self) -> AdHocStats { 643 | match self.heap { 644 | None => AdHocStats { 645 | total_events: self.total_blocks, 646 | total_units: self.total_bytes, 647 | }, 648 | Some(_) => panic!("dhat: getting ad hoc stats while doing heap profiling"), 649 | } 650 | } 651 | 652 | // Finish tracking allocations and deallocations, print a summary message 653 | // to `stderr` and save the profile to file/memory if requested. 654 | fn finish(mut self, memory_output: Option<&mut String>) { 655 | let now = Instant::now(); 656 | 657 | if self.heap.is_some() { 658 | // Total bytes is at a possible peak. 659 | self.check_for_global_peak(); 660 | 661 | let h = self.heap.as_ref().unwrap(); 662 | 663 | // Account for the lifetimes of all remaining live blocks. 664 | for &LiveBlock { 665 | pp_info_idx, 666 | allocation_instant, 667 | } in h.live_blocks.values() 668 | { 669 | self.pp_infos[pp_info_idx] 670 | .heap 671 | .as_mut() 672 | .unwrap() 673 | .total_lifetimes_duration += now.duration_since(allocation_instant); 674 | } 675 | } 676 | 677 | // We give each unique frame an index into `ftbl`, starting with 0 678 | // for the special frame "[root]". 679 | let mut ftbl_indices: FxHashMap = FxHashMap::default(); 680 | ftbl_indices.insert("[root]".to_string(), 0); 681 | let mut next_ftbl_idx = 1; 682 | 683 | // Because `self` is being consumed, we can consume `self.backtraces` 684 | // and replace it with an empty `FxHashMap`. (This is necessary because 685 | // we modify the *keys* here with `resolve`, which isn't allowed with a 686 | // non-consuming iterator.) 687 | let pps: Vec<_> = std::mem::take(&mut self.backtraces) 688 | .into_iter() 689 | .map(|(mut bt, pp_info_idx)| { 690 | // Do the potentially expensive debug info lookups to get 691 | // symbol names, line numbers, etc. 692 | bt.0.resolve(); 693 | 694 | // Trim boring frames at the top and bottom of the backtrace. 695 | let first_symbol_to_show = if self.trim_backtraces.is_some() { 696 | if self.heap.is_some() { 697 | bt.first_heap_symbol_to_show() 698 | } else { 699 | bt.first_ad_hoc_symbol_to_show() 700 | } 701 | } else { 702 | 0 703 | }; 704 | 705 | // Determine the frame indices for this backtrace. This 706 | // involves getting the string for each frame and adding a 707 | // new entry to `ftbl_indices` if it hasn't been seen 708 | // before. 709 | let mut fs = vec![]; 710 | let mut i = 0; 711 | for frame in bt.0.frames().iter() { 712 | for symbol in frame.symbols().iter() { 713 | i += 1; 714 | if (i - 1) < first_symbol_to_show { 715 | continue; 716 | } 717 | let s = Backtrace::frame_to_string(frame, symbol); 718 | let &mut ftbl_idx = ftbl_indices.entry(s).or_insert_with(|| { 719 | next_ftbl_idx += 1; 720 | next_ftbl_idx - 1 721 | }); 722 | fs.push(ftbl_idx); 723 | } 724 | } 725 | 726 | PpInfoJson::new(&self.pp_infos[pp_info_idx], fs) 727 | }) 728 | .collect(); 729 | 730 | // We pre-allocate `ftbl` with empty strings, and then fill it in. 731 | let mut ftbl = vec![String::new(); ftbl_indices.len()]; 732 | for (frame, ftbl_idx) in ftbl_indices.into_iter() { 733 | ftbl[ftbl_idx] = frame; 734 | } 735 | 736 | let h = self.heap.as_ref(); 737 | let is_heap = h.is_some(); 738 | let json = DhatJson { 739 | dhatFileVersion: 2, 740 | mode: if is_heap { "rust-heap" } else { "rust-ad-hoc" }, 741 | verb: "Allocated", 742 | bklt: is_heap, 743 | bkacc: false, 744 | bu: if is_heap { None } else { Some("unit") }, 745 | bsu: if is_heap { None } else { Some("units") }, 746 | bksu: if is_heap { None } else { Some("events") }, 747 | tu: "µs", 748 | Mtu: "s", 749 | tuth: if is_heap { Some(10) } else { None }, 750 | cmd: std::env::args().collect::>().join(" "), 751 | pid: std::process::id(), 752 | tg: h.map(|h| { 753 | h.tgmax_instant 754 | .saturating_duration_since(self.start_instant) 755 | .as_micros() 756 | }), 757 | te: now.duration_since(self.start_instant).as_micros(), 758 | pps, 759 | ftbl, 760 | }; 761 | 762 | eprintln!( 763 | "dhat: Total: {} {} in {} {}", 764 | self.total_bytes.separate_with_commas(), 765 | json.bsu.unwrap_or("bytes"), 766 | self.total_blocks.separate_with_commas(), 767 | json.bksu.unwrap_or("blocks"), 768 | ); 769 | if let Some(h) = &self.heap { 770 | eprintln!( 771 | "dhat: At t-gmax: {} bytes in {} blocks", 772 | h.max_bytes.separate_with_commas(), 773 | h.max_blocks.separate_with_commas(), 774 | ); 775 | eprintln!( 776 | "dhat: At t-end: {} bytes in {} blocks", 777 | h.curr_bytes.separate_with_commas(), 778 | h.curr_blocks.separate_with_commas(), 779 | ); 780 | } 781 | 782 | if let Some(memory_output) = memory_output { 783 | // Default pretty printing is fine here, it's only used for small 784 | // tests. 785 | *memory_output = serde_json::to_string_pretty(&json).unwrap(); 786 | eprintln!("dhat: The data has been saved to the memory buffer"); 787 | } else { 788 | let write = || -> std::io::Result<()> { 789 | let buffered_file = BufWriter::new(File::create(&self.file_name)?); 790 | // `to_writer` produces JSON that is compact. 791 | // `to_writer_pretty` produces JSON that is readable. This code 792 | // gives us JSON that is fairly compact and fairly readable. 793 | // Ideally it would be more like what DHAT produces, e.g. one 794 | // space indents, no spaces after `:` and `,`, and `fs` arrays 795 | // on a single line, but this is as good as we can easily 796 | // achieve. 797 | let formatter = serde_json::ser::PrettyFormatter::with_indent(b""); 798 | let mut ser = serde_json::Serializer::with_formatter(buffered_file, formatter); 799 | json.serialize(&mut ser)?; 800 | Ok(()) 801 | }; 802 | match write() { 803 | Ok(()) => eprintln!( 804 | "dhat: The data has been saved to {}, and is viewable with dhat/dh_view.html", 805 | self.file_name.to_string_lossy() 806 | ), 807 | Err(e) => eprintln!( 808 | "dhat: error: Writing to {} failed: {}", 809 | self.file_name.to_string_lossy(), 810 | e 811 | ), 812 | } 813 | } 814 | if self.eprint_json { 815 | eprintln!( 816 | "dhat: json = `{}`", 817 | serde_json::to_string_pretty(&json).unwrap() 818 | ); 819 | } 820 | } 821 | } 822 | 823 | impl HeapGlobals { 824 | fn new() -> Self { 825 | Self { 826 | live_blocks: FxHashMap::default(), 827 | curr_blocks: 0, 828 | curr_bytes: 0, 829 | max_blocks: 0, 830 | max_bytes: 0, 831 | tgmax_instant: Instant::now(), 832 | } 833 | } 834 | } 835 | 836 | struct PpInfo { 837 | // The total number of blocks and bytes allocated by this PP. 838 | total_blocks: u64, 839 | total_bytes: u64, 840 | 841 | heap: Option, 842 | } 843 | 844 | #[derive(Default)] 845 | struct HeapPpInfo { 846 | // The current number of blocks and bytes allocated by this PP. 847 | curr_blocks: usize, 848 | curr_bytes: usize, 849 | 850 | // The number of blocks and bytes at the PP max, i.e. when this PP's 851 | // `curr_bytes` peaks. 852 | max_blocks: usize, 853 | max_bytes: usize, 854 | 855 | // The number of blocks and bytes at the global max, i.e. when 856 | // `Globals::curr_bytes` peaks. 857 | at_tgmax_blocks: usize, 858 | at_tgmax_bytes: usize, 859 | 860 | // Total lifetimes of all blocks allocated by this PP. Includes blocks 861 | // explicitly freed and blocks implicitly freed at termination. 862 | total_lifetimes_duration: Duration, 863 | } 864 | 865 | impl PpInfo { 866 | fn new_heap() -> Self { 867 | Self { 868 | total_blocks: 0, 869 | total_bytes: 0, 870 | heap: Some(HeapPpInfo::default()), 871 | } 872 | } 873 | 874 | fn new_ad_hoc() -> Self { 875 | Self { 876 | total_blocks: 0, 877 | total_bytes: 0, 878 | heap: None, 879 | } 880 | } 881 | 882 | fn update_counts_for_alloc(&mut self, size: usize, delta: Option) { 883 | self.total_blocks += 1; 884 | self.total_bytes += size as u64; 885 | 886 | let h = self.heap.as_mut().unwrap(); 887 | if let Some(delta) = delta { 888 | // realloc 889 | h.curr_blocks += 0; // unchanged 890 | h.curr_bytes += delta; 891 | } else { 892 | // alloc 893 | h.curr_blocks += 1; 894 | h.curr_bytes += size; 895 | } 896 | 897 | // The use of `>=` not `>` means that if there are multiple equal peaks 898 | // we record the latest one, like `check_for_global_peak` does. 899 | if h.curr_bytes >= h.max_bytes { 900 | h.max_blocks = h.curr_blocks; 901 | h.max_bytes = h.curr_bytes; 902 | } 903 | } 904 | 905 | fn update_counts_for_dealloc(&mut self, size: usize, alloc_duration: Duration) { 906 | let h = self.heap.as_mut().unwrap(); 907 | h.curr_blocks -= 1; 908 | h.curr_bytes -= size; 909 | h.total_lifetimes_duration += alloc_duration; 910 | } 911 | 912 | fn update_counts_for_ad_hoc_event(&mut self, weight: usize) { 913 | std::assert!(self.heap.is_none()); 914 | self.total_blocks += 1; 915 | self.total_bytes += weight as u64; 916 | } 917 | } 918 | 919 | struct LiveBlock { 920 | // The index of the PpInfo for this block. 921 | pp_info_idx: usize, 922 | 923 | // When the block was allocated. 924 | allocation_instant: Instant, 925 | } 926 | 927 | // We record info about allocations and deallocations. A wrinkle: the recording 928 | // done may trigger additional allocations. We must ignore these because (a) 929 | // they're part of `dhat`'s execution, not the original program's execution, 930 | // and (b) they would be intercepted and trigger additional allocations, which 931 | // would be intercepted and trigger additional allocations, and so on, leading 932 | // to infinite loops. 933 | // 934 | // With this type we can run one code path if we are already ignoring 935 | // allocations. Otherwise, we can a second code path while ignoring 936 | // allocations. In practice, the first code path is unreachable except within 937 | // the `GlobalAlloc` methods. 938 | // 939 | // WARNING: This type must be used for any code within this crate that can 940 | // trigger allocations. 941 | struct IgnoreAllocs { 942 | was_already_ignoring_allocs: bool, 943 | } 944 | 945 | thread_local!(static IGNORE_ALLOCS: Cell = Cell::new(false)); 946 | 947 | impl IgnoreAllocs { 948 | fn new() -> Self { 949 | Self { 950 | was_already_ignoring_allocs: IGNORE_ALLOCS.with(|b| b.replace(true)), 951 | } 952 | } 953 | } 954 | 955 | /// If code panics while `IgnoreAllocs` is live, this will still reset 956 | /// `IGNORE_ALLOCS` so that it can be used again. 957 | impl Drop for IgnoreAllocs { 958 | fn drop(&mut self) { 959 | if !self.was_already_ignoring_allocs { 960 | IGNORE_ALLOCS.with(|b| b.set(false)); 961 | } 962 | } 963 | } 964 | 965 | /// A type whose lifetime dictates the start and end of profiling. 966 | /// 967 | /// Profiling starts when the first value of this type is created. Profiling 968 | /// stops when (a) this value is dropped or (b) a `dhat` assertion fails, 969 | /// whichever comes first. When that happens, profiling data may be written to 970 | /// file, depending on how the `Profiler` has been configured. Only one 971 | /// `Profiler` can be running at any point in time. 972 | // 973 | // The actual profiler state is stored in `Globals`, so it can be accessed from 974 | // places like `Alloc::alloc` and `ad_hoc_event()` when the `Profiler` 975 | // instance isn't within reach. 976 | #[derive(Debug)] 977 | pub struct Profiler; 978 | 979 | impl Profiler { 980 | /// Initiates allocation profiling. 981 | /// 982 | /// Typically the first thing in `main`. Its result should be assigned to a 983 | /// variable whose lifetime ends at the end of `main`. 984 | /// 985 | /// # Panics 986 | /// 987 | /// Panics if another `Profiler` is running. 988 | /// 989 | /// # Examples 990 | /// ``` 991 | /// let _profiler = dhat::Profiler::new_heap(); 992 | /// ``` 993 | pub fn new_heap() -> Self { 994 | Self::builder().build() 995 | } 996 | 997 | /// Initiates ad hoc profiling. 998 | /// 999 | /// Typically the first thing in `main`. Its result should be assigned to a 1000 | /// variable whose lifetime ends at the end of `main`. 1001 | /// 1002 | /// # Panics 1003 | /// 1004 | /// Panics if another `Profiler` is running. 1005 | /// 1006 | /// # Examples 1007 | /// ``` 1008 | /// let _profiler = dhat::Profiler::new_ad_hoc(); 1009 | /// ``` 1010 | pub fn new_ad_hoc() -> Self { 1011 | Self::builder().ad_hoc().build() 1012 | } 1013 | 1014 | /// Creates a new [`ProfilerBuilder`], which defaults to heap profiling. 1015 | pub fn builder() -> ProfilerBuilder { 1016 | ProfilerBuilder { 1017 | ad_hoc: false, 1018 | testing: false, 1019 | file_name: None, 1020 | trim_backtraces: Some(10), 1021 | eprint_json: false, 1022 | } 1023 | } 1024 | } 1025 | 1026 | /// A builder for [`Profiler`], for cases beyond the basic ones provided by 1027 | /// [`Profiler`]. 1028 | /// 1029 | /// Created with [`Profiler::builder`]. 1030 | #[derive(Debug)] 1031 | pub struct ProfilerBuilder { 1032 | ad_hoc: bool, 1033 | testing: bool, 1034 | file_name: Option, 1035 | trim_backtraces: Option, 1036 | eprint_json: bool, 1037 | } 1038 | 1039 | impl ProfilerBuilder { 1040 | /// Requests ad hoc profiling. 1041 | /// 1042 | /// # Examples 1043 | /// ``` 1044 | /// let _profiler = dhat::Profiler::builder().ad_hoc().build(); 1045 | /// ``` 1046 | pub fn ad_hoc(mut self) -> Self { 1047 | self.ad_hoc = true; 1048 | self 1049 | } 1050 | 1051 | /// Requests testing mode, which allows the use of 1052 | /// [`dhat::assert!`](assert) and related macros, and disables saving of 1053 | /// profile data on [`Profiler`] drop. 1054 | /// 1055 | /// # Examples 1056 | /// ``` 1057 | /// let _profiler = dhat::Profiler::builder().testing().build(); 1058 | /// ``` 1059 | pub fn testing(mut self) -> Self { 1060 | self.testing = true; 1061 | self 1062 | } 1063 | 1064 | /// Sets the name of the file in which profiling data will be saved. 1065 | /// 1066 | /// # Examples 1067 | /// ``` 1068 | /// let file_name = format!("heap-{}.json", std::process::id()); 1069 | /// let _profiler = dhat::Profiler::builder().file_name(file_name).build(); 1070 | /// # std::mem::forget(_profiler); // Don't write the file in `cargo tests` 1071 | /// ``` 1072 | pub fn file_name>(mut self, file_name: P) -> Self { 1073 | self.file_name = Some(file_name.as_ref().to_path_buf()); 1074 | self 1075 | } 1076 | 1077 | /// Sets how backtrace trimming is performed. 1078 | /// 1079 | /// `dhat` can use heuristics to trim uninteresting frames from the top and 1080 | /// bottom of backtraces, which makes the output easier to read. It can 1081 | /// also limit the number of frames, which improves performance. 1082 | /// 1083 | /// The argument can be specified in several ways. 1084 | /// - `None`: no backtrace trimming will be performed, and there is no 1085 | /// frame count limit. This makes profiling much slower and increases the 1086 | /// size of saved data files. 1087 | /// - `Some(n)`: top and bottom trimming will be performed, and the number 1088 | /// of frames will be limited by `n`. Values of `n` less than 4 will be 1089 | /// clamped to 4. 1090 | /// - `Some(usize::MAX)`: top and bottom trimming with be performed, but 1091 | /// there is no frame count limit. This makes profiling much slower and 1092 | /// increases the size of saved data files. 1093 | /// 1094 | /// The default value (used if this function is not called) is `Some(10)`. 1095 | /// 1096 | /// The number of frames shown in viewed profiles may differ from the 1097 | /// number requested here, for two reasons. 1098 | /// - Inline frames do not count towards this length. In release builds it 1099 | /// is common for the number of inline frames to equal or even exceed the 1100 | /// number of "real" frames. 1101 | /// - Backtrace trimming will remove a small number of frames from heap 1102 | /// profile backtraces. The number removed will likely be more in a debug 1103 | /// build than in a release build. 1104 | /// 1105 | /// # Examples 1106 | /// ``` 1107 | /// let _profiler = dhat::Profiler::builder().trim_backtraces(None).build(); 1108 | /// ``` 1109 | pub fn trim_backtraces(mut self, max_frames: Option) -> Self { 1110 | self.trim_backtraces = max_frames.map(|m| std::cmp::max(m, 4)); 1111 | self 1112 | } 1113 | 1114 | // For testing purposes only. Useful for seeing what went wrong if a test 1115 | // fails on CI. 1116 | #[doc(hidden)] 1117 | pub fn eprint_json(mut self) -> Self { 1118 | self.eprint_json = true; 1119 | self 1120 | } 1121 | 1122 | /// Creates a [`Profiler`] from the builder and initiates profiling. 1123 | /// 1124 | /// # Panics 1125 | /// 1126 | /// Panics if another [`Profiler`] is running. 1127 | pub fn build(self) -> Profiler { 1128 | let ignore_allocs = IgnoreAllocs::new(); 1129 | std::assert!(!ignore_allocs.was_already_ignoring_allocs); 1130 | 1131 | let phase: &mut Phase = &mut TRI_GLOBALS.lock(); 1132 | match phase { 1133 | Phase::Ready => { 1134 | let file_name = if let Some(file_name) = self.file_name { 1135 | file_name 1136 | } else if !self.ad_hoc { 1137 | PathBuf::from("dhat-heap.json") 1138 | } else { 1139 | PathBuf::from("dhat-ad-hoc.json") 1140 | }; 1141 | let h = if !self.ad_hoc { 1142 | Some(HeapGlobals::new()) 1143 | } else { 1144 | None 1145 | }; 1146 | *phase = Phase::Running(Globals::new( 1147 | self.testing, 1148 | file_name, 1149 | self.trim_backtraces, 1150 | self.eprint_json, 1151 | h, 1152 | )); 1153 | } 1154 | Phase::Running(_) | Phase::PostAssert => { 1155 | panic!("dhat: creating a profiler while a profiler is already running") 1156 | } 1157 | } 1158 | Profiler 1159 | } 1160 | } 1161 | 1162 | // Get a backtrace according to `$g`'s settings. A macro rather than a `Global` 1163 | // method to avoid putting an extra frame into backtraces. 1164 | macro_rules! new_backtrace { 1165 | ($g:expr) => {{ 1166 | if $g.frames_to_trim.is_none() { 1167 | // This is the first backtrace from profiling. Work out what we 1168 | // will be trimming from the top and bottom of all backtraces. 1169 | // `None` here because we don't want any frame trimming for this 1170 | // backtrace. 1171 | let bt = new_backtrace_inner(None, &FxHashMap::default()); 1172 | $g.frames_to_trim = Some(bt.get_frames_to_trim(&$g.start_bt)); 1173 | } 1174 | 1175 | // Get the backtrace. 1176 | new_backtrace_inner($g.trim_backtraces, $g.frames_to_trim.as_ref().unwrap()) 1177 | }}; 1178 | } 1179 | 1180 | // Get a backtrace, possibly trimmed. 1181 | // 1182 | // Note: it's crucial that there only be a single call to `backtrace::trace()` 1183 | // that is used everywhere, so that all traces will have the same backtrace 1184 | // function IPs in their top frames. (With multiple call sites we would have 1185 | // multiple closures, giving multiple instances of `backtrace::trace`, and 1186 | // monomorphisation would put them into different functions in the binary.) 1187 | // Without this, top frame trimming wouldn't work. That's why this is a 1188 | // function (with `inline(never)` just to be safe) rather than a macro like 1189 | // `new_backtrace`. The frame for this function will be removed by top frame 1190 | // trimming. 1191 | #[inline(never)] 1192 | fn new_backtrace_inner( 1193 | trim_backtraces: Option, 1194 | frames_to_trim: &FxHashMap, 1195 | ) -> Backtrace { 1196 | // Get the backtrace, trimming if necessary at the top and bottom and for 1197 | // length. 1198 | let mut frames = Vec::new(); 1199 | backtrace::trace(|frame| { 1200 | let ip = frame.ip() as usize; 1201 | if trim_backtraces.is_some() { 1202 | match frames_to_trim.get(&ip) { 1203 | Some(TB::Top) => return true, // ignore frame and continue 1204 | Some(TB::Bottom) => return false, // ignore frame and stop 1205 | _ => {} // use this frame 1206 | } 1207 | } 1208 | 1209 | frames.push(frame.clone().into()); 1210 | 1211 | if let Some(max_frames) = trim_backtraces { 1212 | frames.len() < max_frames // stop if we have enough frames 1213 | } else { 1214 | true // continue 1215 | } 1216 | }); 1217 | Backtrace(frames.into()) 1218 | } 1219 | 1220 | /// A global allocator that tracks allocations and deallocations on behalf of 1221 | /// the [`Profiler`] type. 1222 | /// 1223 | /// It must be set as the global allocator (via `#[global_allocator]`) when 1224 | /// doing heap profiling. 1225 | #[derive(Debug)] 1226 | pub struct Alloc; 1227 | 1228 | unsafe impl GlobalAlloc for Alloc { 1229 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 { 1230 | let ignore_allocs = IgnoreAllocs::new(); 1231 | if ignore_allocs.was_already_ignoring_allocs { 1232 | System.alloc(layout) 1233 | } else { 1234 | let phase: &mut Phase = &mut TRI_GLOBALS.lock(); 1235 | let ptr = System.alloc(layout); 1236 | if ptr.is_null() { 1237 | return ptr; 1238 | } 1239 | 1240 | if let Phase::Running(g @ Globals { heap: Some(_), .. }) = phase { 1241 | let size = layout.size(); 1242 | let bt = new_backtrace!(g); 1243 | let pp_info_idx = g.get_pp_info(bt, PpInfo::new_heap); 1244 | 1245 | let now = Instant::now(); 1246 | g.record_block(ptr, pp_info_idx, now); 1247 | g.update_counts_for_alloc(pp_info_idx, size, None, now); 1248 | } 1249 | ptr 1250 | } 1251 | } 1252 | 1253 | unsafe fn realloc(&self, old_ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { 1254 | let ignore_allocs = IgnoreAllocs::new(); 1255 | if ignore_allocs.was_already_ignoring_allocs { 1256 | System.realloc(old_ptr, layout, new_size) 1257 | } else { 1258 | let phase: &mut Phase = &mut TRI_GLOBALS.lock(); 1259 | let new_ptr = System.realloc(old_ptr, layout, new_size); 1260 | if new_ptr.is_null() { 1261 | return new_ptr; 1262 | } 1263 | 1264 | if let Phase::Running(g @ Globals { heap: Some(_), .. }) = phase { 1265 | let old_size = layout.size(); 1266 | let delta = Delta::new(old_size, new_size); 1267 | 1268 | if delta.shrinking { 1269 | // Total bytes is coming down from a possible peak. 1270 | g.check_for_global_peak(); 1271 | } 1272 | 1273 | // Remove the record of the existing live block and get the 1274 | // `PpInfo`. If it's not in the live block table, it must 1275 | // have been allocated before `TRI_GLOBALS` was set up, and 1276 | // we treat it like an `alloc`. 1277 | let h = g.heap.as_mut().unwrap(); 1278 | let live_block = h.live_blocks.remove(&(old_ptr as usize)); 1279 | let (pp_info_idx, delta) = if let Some(live_block) = live_block { 1280 | (live_block.pp_info_idx, Some(delta)) 1281 | } else { 1282 | let bt = new_backtrace!(g); 1283 | let pp_info_idx = g.get_pp_info(bt, PpInfo::new_heap); 1284 | (pp_info_idx, None) 1285 | }; 1286 | 1287 | let now = Instant::now(); 1288 | g.record_block(new_ptr, pp_info_idx, now); 1289 | g.update_counts_for_alloc(pp_info_idx, new_size, delta, now); 1290 | } 1291 | new_ptr 1292 | } 1293 | } 1294 | 1295 | unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { 1296 | let ignore_allocs = IgnoreAllocs::new(); 1297 | if ignore_allocs.was_already_ignoring_allocs { 1298 | System.dealloc(ptr, layout) 1299 | } else { 1300 | let phase: &mut Phase = &mut TRI_GLOBALS.lock(); 1301 | System.dealloc(ptr, layout); 1302 | 1303 | if let Phase::Running(g @ Globals { heap: Some(_), .. }) = phase { 1304 | let size = layout.size(); 1305 | 1306 | // Remove the record of the live block and get the 1307 | // `PpInfo`. If it's not in the live block table, it must 1308 | // have been allocated before `TRI_GLOBALS` was set up, and 1309 | // we just ignore it. 1310 | let h = g.heap.as_mut().unwrap(); 1311 | if let Some(LiveBlock { 1312 | pp_info_idx, 1313 | allocation_instant, 1314 | }) = h.live_blocks.remove(&(ptr as usize)) 1315 | { 1316 | // Total bytes is coming down from a possible peak. 1317 | g.check_for_global_peak(); 1318 | 1319 | let alloc_duration = allocation_instant.elapsed(); 1320 | g.update_counts_for_dealloc(pp_info_idx, size, alloc_duration); 1321 | } 1322 | } 1323 | } 1324 | } 1325 | } 1326 | 1327 | /// Registers an event during ad hoc profiling. 1328 | /// 1329 | /// The meaning of the weight argument is determined by the user. A call to 1330 | /// this function has no effect if a [`Profiler`] is not running or not doing ad 1331 | /// hoc profiling. 1332 | pub fn ad_hoc_event(weight: usize) { 1333 | let ignore_allocs = IgnoreAllocs::new(); 1334 | std::assert!(!ignore_allocs.was_already_ignoring_allocs); 1335 | 1336 | let phase: &mut Phase = &mut TRI_GLOBALS.lock(); 1337 | if let Phase::Running(g @ Globals { heap: None, .. }) = phase { 1338 | let bt = new_backtrace!(g); 1339 | let pp_info_idx = g.get_pp_info(bt, PpInfo::new_ad_hoc); 1340 | 1341 | // Update counts. 1342 | g.update_counts_for_ad_hoc_event(pp_info_idx, weight); 1343 | } 1344 | } 1345 | 1346 | impl Profiler { 1347 | fn drop_inner(&mut self, memory_output: Option<&mut String>) { 1348 | let ignore_allocs = IgnoreAllocs::new(); 1349 | std::assert!(!ignore_allocs.was_already_ignoring_allocs); 1350 | 1351 | let phase: &mut Phase = &mut TRI_GLOBALS.lock(); 1352 | match std::mem::replace(phase, Phase::Ready) { 1353 | Phase::Ready => unreachable!(), 1354 | Phase::Running(g) => { 1355 | if !g.testing { 1356 | g.finish(memory_output) 1357 | } 1358 | } 1359 | Phase::PostAssert => {} 1360 | } 1361 | } 1362 | 1363 | // For testing purposes only. 1364 | #[doc(hidden)] 1365 | pub fn drop_and_get_memory_output(&mut self) -> String { 1366 | let mut memory_output = String::new(); 1367 | self.drop_inner(Some(&mut memory_output)); 1368 | memory_output 1369 | } 1370 | } 1371 | 1372 | impl Drop for Profiler { 1373 | fn drop(&mut self) { 1374 | self.drop_inner(None); 1375 | } 1376 | } 1377 | 1378 | // A wrapper for `backtrace::Backtrace` that implements `Eq` and `Hash`, which 1379 | // only look at the frame IPs. This assumes that any two 1380 | // `backtrace::Backtrace`s with the same frame IPs are equivalent. 1381 | #[derive(Debug)] 1382 | struct Backtrace(backtrace::Backtrace); 1383 | 1384 | impl Backtrace { 1385 | // The top frame symbols in a backtrace (those relating to backtracing 1386 | // itself) are typically the same, and look something like this (Mac or 1387 | // Linux release build, Dec 2021): 1388 | // - 0x10fca200a: backtrace::backtrace::libunwind::trace 1389 | // - 0x10fca200a: backtrace::backtrace::trace_unsynchronized 1390 | // - 0x10fca200a: backtrace::backtrace::trace 1391 | // - 0x10fc97350: dhat::new_backtrace_inner 1392 | // - 0x10fc97984: [interesting function] 1393 | // 1394 | // We compare the top frames of a stack obtained while profiling with those 1395 | // in `start_bt`. Those that overlap are the frames relating to backtracing 1396 | // that can be discarded. 1397 | // 1398 | // The bottom frame symbols in a backtrace (those below `main`) are 1399 | // typically the same, and look something like this (Mac or Linux release 1400 | // build, Dec 2021): 1401 | // - 0x1060f70e8: dhatter::main 1402 | // - 0x1060f7026: core::ops::function::FnOnce::call_once 1403 | // - 0x1060f7026: std::sys_common::backtrace::__rust_begin_short_backtrace 1404 | // - 0x1060f703c: std::rt::lang_start::{{closure}} 1405 | // - 0x10614b79a: core::ops::function::impls:: for &F>::call_once 1406 | // - 0x10614b79a: std::panicking::try::do_call 1407 | // - 0x10614b79a: std::panicking::try 1408 | // - 0x10614b79a: std::panic::catch_unwind 1409 | // - 0x10614b79a: std::rt::lang_start_internal::{{closure}} 1410 | // - 0x10614b79a: std::panicking::try::do_call 1411 | // - 0x10614b79a: std::panicking::try 1412 | // - 0x10614b79a: std::panic::catch_unwind 1413 | // - 0x10614b79a: std::rt::lang_start_internal 1414 | // - 0x1060f7259: ??? 1415 | // 1416 | // We compare the bottom frames of a stack obtained while profiling with 1417 | // those in `start_bt`. Those that overlap are the frames below main that 1418 | // can be discarded. 1419 | fn get_frames_to_trim(&self, start_bt: &Backtrace) -> FxHashMap { 1420 | let mut frames_to_trim = FxHashMap::default(); 1421 | let frames1 = self.0.frames(); 1422 | let frames2 = start_bt.0.frames(); 1423 | 1424 | let (mut i1, mut i2) = (0, 0); 1425 | loop { 1426 | if i1 == frames1.len() - 1 || i2 == frames2.len() - 1 { 1427 | // This should never happen in practice, it's too much 1428 | // similarity between the backtraces. If it does happen, 1429 | // abandon top trimming entirely. 1430 | frames_to_trim.retain(|_, v| *v == TB::Bottom); 1431 | break; 1432 | } 1433 | if frames1[i1].ip() != frames2[i2].ip() { 1434 | break; 1435 | } 1436 | frames_to_trim.insert(frames1[i1].ip() as usize, TB::Top); 1437 | i1 += 1; 1438 | i2 += 1; 1439 | } 1440 | 1441 | let (mut i1, mut i2) = (frames1.len() - 1, frames2.len() - 1); 1442 | loop { 1443 | if i1 == 0 || i2 == 0 { 1444 | // This should never happen in practice, it's too much 1445 | // similarity between the backtraces. If it does happen, 1446 | // abandon bottom trimming entirely. 1447 | frames_to_trim.retain(|_, v| *v == TB::Top); 1448 | break; 1449 | } 1450 | if frames1[i1].ip() != frames2[i2].ip() { 1451 | break; 1452 | } 1453 | frames_to_trim.insert(frames1[i1].ip() as usize, TB::Bottom); 1454 | i1 -= 1; 1455 | i2 -= 1; 1456 | } 1457 | 1458 | frames_to_trim 1459 | } 1460 | 1461 | // The top frame symbols in a trimmed heap profiling backtrace vary 1462 | // significantly, depending on build configuration, platform, and program 1463 | // point, and look something like this (Mac or Linux release build, Dec 1464 | // 2021): 1465 | // - 0x103ad464c: ::alloc 1466 | // - 0x103acac99: __rg_alloc // sometimes missing 1467 | // - 0x103acfe47: alloc::alloc::alloc // sometimes missing 1468 | // - 0x103acfe47: alloc::alloc::Global::alloc_impl 1469 | // - 0x103acfe47: ::allocate 1470 | // - 0x103acfe47: alloc::alloc::exchange_malloc // sometimes missing 1471 | // - 0x103acfe47: [allocation point in program being profiled] 1472 | // 1473 | // We scan backwards for the first frame that looks like it comes from 1474 | // allocator code, and all frames before it. If we don't find any such 1475 | // frames, we show from frame 0, i.e. all frames. 1476 | // 1477 | // Note: this is a little dangerous. When deciding if a new backtrace has 1478 | // been seen before, we consider all the IP addresses within it. And then 1479 | // we trim some of those. It's possible that this will result in some 1480 | // previously distinct traces becoming the same, which makes dh_view.html 1481 | // abort. If that ever happens, look to see if something is going wrong 1482 | // here. 1483 | fn first_heap_symbol_to_show(&self) -> usize { 1484 | // Examples of symbols that this search will match: 1485 | // - alloc::alloc::{alloc,realloc,exchange_malloc} 1486 | // - ::{allocate,grow} 1487 | // - ::alloc 1488 | // - __rg_{alloc,realloc} 1489 | // 1490 | // Be careful when changing this, because to do it properly requires 1491 | // testing both debug and release builds on multiple platforms. 1492 | self.first_symbol_to_show(|s| { 1493 | s.starts_with("alloc::alloc::") 1494 | || s.starts_with(" usize { 1507 | 0 1508 | } 1509 | 1510 | // Find the first symbol to show, based on the predicate `p`. 1511 | fn first_symbol_to_show bool>(&self, p: P) -> usize { 1512 | // Get the symbols into a vector so we can reverse iterate over them. 1513 | let symbols: Vec<_> = self 1514 | .0 1515 | .frames() 1516 | .iter() 1517 | .flat_map(|f| f.symbols().iter()) 1518 | .collect(); 1519 | 1520 | for (i, symbol) in symbols.iter().enumerate().rev() { 1521 | // Use `{:#}` to print the "alternate" form of the symbol name, 1522 | // which omits the trailing hash (e.g. `::ha68e4508a38cc95a`). 1523 | if let Some(s) = symbol.name().map(|name| format!("{:#}", name)) { 1524 | if p(&s) { 1525 | return i; 1526 | } 1527 | } 1528 | } 1529 | 0 1530 | } 1531 | 1532 | // Useful for debugging. 1533 | #[allow(dead_code)] 1534 | fn eprint(&self) { 1535 | for frame in self.0.frames().iter() { 1536 | for symbol in frame.symbols().iter() { 1537 | eprintln!("{}", Backtrace::frame_to_string(frame, symbol)); 1538 | } 1539 | } 1540 | } 1541 | 1542 | fn frame_to_string( 1543 | frame: &backtrace::BacktraceFrame, 1544 | symbol: &backtrace::BacktraceSymbol, 1545 | ) -> String { 1546 | format!( 1547 | // Use `{:#}` to print the "alternate" form of the symbol name, 1548 | // which omits the trailing hash (e.g. `::ha68e4508a38cc95a`). 1549 | "{:?}: {:#} ({:#}:{}:{})", 1550 | frame.ip(), 1551 | symbol.name().unwrap_or_else(|| SymbolName::new(b"???")), 1552 | match symbol.filename() { 1553 | Some(path) => trim_path(path), 1554 | None => Path::new("???"), 1555 | } 1556 | .display(), 1557 | symbol.lineno().unwrap_or(0), 1558 | symbol.colno().unwrap_or(0), 1559 | ) 1560 | } 1561 | } 1562 | 1563 | impl PartialEq for Backtrace { 1564 | fn eq(&self, other: &Self) -> bool { 1565 | let mut frames1 = self.0.frames().iter(); 1566 | let mut frames2 = other.0.frames().iter(); 1567 | loop { 1568 | let ip1 = frames1.next().map(|f| f.ip()); 1569 | let ip2 = frames2.next().map(|f| f.ip()); 1570 | if ip1 != ip2 { 1571 | return false; 1572 | } 1573 | if ip1 == None { 1574 | return true; 1575 | } 1576 | // Otherwise, continue. 1577 | } 1578 | } 1579 | } 1580 | 1581 | impl Eq for Backtrace {} 1582 | 1583 | impl Hash for Backtrace { 1584 | fn hash(&self, state: &mut H) { 1585 | for frame in self.0.frames().iter() { 1586 | frame.ip().hash(state); 1587 | } 1588 | } 1589 | } 1590 | 1591 | // Trims a path with more than three components down to three (e.g. 1592 | // `/aa/bb/cc/dd.rs` becomes `bb/cc/dd.rs`), otherwise returns `path` 1593 | // unchanged. 1594 | fn trim_path(path: &Path) -> &Path { 1595 | const N: usize = 3; 1596 | let len = path.components().count(); 1597 | if len > N { 1598 | let mut c = path.components(); 1599 | c.nth(len - (N + 1)); 1600 | c.as_path() 1601 | } else { 1602 | path 1603 | } 1604 | } 1605 | 1606 | /// Stats from heap profiling. 1607 | #[derive(Clone, Debug, PartialEq, Eq)] 1608 | #[non_exhaustive] 1609 | pub struct HeapStats { 1610 | /// Number of blocks (a.k.a. allocations) allocated over the entire run. 1611 | pub total_blocks: u64, 1612 | 1613 | /// Number of bytes allocated over the entire run. 1614 | pub total_bytes: u64, 1615 | 1616 | /// Number of blocks (a.k.a. allocations) currently allocated. 1617 | pub curr_blocks: usize, 1618 | 1619 | /// Number of bytes currently allocated. 1620 | pub curr_bytes: usize, 1621 | 1622 | /// Number of blocks (a.k.a. allocations) allocated at the global peak, 1623 | /// i.e. when `curr_bytes` peaked. 1624 | pub max_blocks: usize, 1625 | 1626 | /// Number of bytes allocated at the global peak, i.e. when `curr_bytes` 1627 | /// peaked. 1628 | pub max_bytes: usize, 1629 | } 1630 | 1631 | /// Stats from ad hoc profiling. 1632 | #[derive(Clone, Debug, PartialEq, Eq)] 1633 | #[non_exhaustive] 1634 | pub struct AdHocStats { 1635 | /// Number of events recorded for the entire run. 1636 | pub total_events: u64, 1637 | 1638 | /// Number of units recorded for the entire run. 1639 | pub total_units: u64, 1640 | } 1641 | 1642 | impl HeapStats { 1643 | /// Gets the current heap stats. 1644 | /// 1645 | /// # Panics 1646 | /// 1647 | /// Panics if called when a [`Profiler`] is not running or not doing heap 1648 | /// profiling. 1649 | pub fn get() -> Self { 1650 | let ignore_allocs = IgnoreAllocs::new(); 1651 | std::assert!(!ignore_allocs.was_already_ignoring_allocs); 1652 | 1653 | let phase: &mut Phase = &mut TRI_GLOBALS.lock(); 1654 | match phase { 1655 | Phase::Ready => { 1656 | panic!("dhat: getting heap stats when no profiler is running") 1657 | } 1658 | Phase::Running(g) => g.get_heap_stats(), 1659 | Phase::PostAssert => { 1660 | panic!("dhat: getting heap stats after the profiler has asserted") 1661 | } 1662 | } 1663 | } 1664 | } 1665 | 1666 | impl AdHocStats { 1667 | /// Gets the current ad hoc stats. 1668 | /// 1669 | /// # Panics 1670 | /// 1671 | /// Panics if called when a [`Profiler`] is not running or not doing ad hoc 1672 | /// profiling. 1673 | pub fn get() -> Self { 1674 | let ignore_allocs = IgnoreAllocs::new(); 1675 | std::assert!(!ignore_allocs.was_already_ignoring_allocs); 1676 | 1677 | let phase: &mut Phase = &mut TRI_GLOBALS.lock(); 1678 | match phase { 1679 | Phase::Ready => { 1680 | panic!("dhat: getting ad hoc stats when no profiler is running") 1681 | } 1682 | Phase::Running(g) => g.get_ad_hoc_stats(), 1683 | Phase::PostAssert => { 1684 | panic!("dhat: getting ad hoc stats after the profiler has asserted") 1685 | } 1686 | } 1687 | } 1688 | } 1689 | 1690 | // Just an implementation detail of the assert macros. 1691 | // njn: invert sense of the return value? 1692 | #[doc(hidden)] 1693 | pub fn check_assert_condition(cond: F) -> bool 1694 | where 1695 | F: FnOnce() -> bool, 1696 | { 1697 | // We do the test within `check_assert_condition` (as opposed to within the 1698 | // `assert*` macros) so that we'll always detect if the profiler isn't 1699 | // running. 1700 | let ignore_allocs = IgnoreAllocs::new(); 1701 | std::assert!(!ignore_allocs.was_already_ignoring_allocs); 1702 | 1703 | let phase: &mut Phase = &mut TRI_GLOBALS.lock(); 1704 | match phase { 1705 | Phase::Ready => panic!("dhat: asserting when no profiler is running"), 1706 | Phase::Running(g) => { 1707 | if !g.testing { 1708 | panic!("dhat: asserting while not in testing mode"); 1709 | } 1710 | if cond() { 1711 | return false; 1712 | } 1713 | } 1714 | Phase::PostAssert => panic!("dhat: asserting after the profiler has asserted"), 1715 | } 1716 | 1717 | // Failure. 1718 | match std::mem::replace(phase, Phase::PostAssert) { 1719 | Phase::Ready => unreachable!(), 1720 | Phase::Running(g) => { 1721 | g.finish(None); 1722 | true 1723 | } 1724 | Phase::PostAssert => unreachable!(), 1725 | } 1726 | } 1727 | 1728 | /// Asserts that an expression is true. 1729 | /// 1730 | /// Like [`std::assert!`], additional format arguments are supported. On 1731 | /// failure, this macro will save the profile data and panic. 1732 | /// 1733 | /// # Panics 1734 | /// 1735 | /// Panics immediately (without saving the profile data) in the following 1736 | /// circumstances. 1737 | /// - If called when a [`Profiler`] is not running or is not in testing mode. 1738 | /// - If called after a previous `dhat` assertion has failed with the current 1739 | /// [`Profiler`]. This is possible if [`std::panic::catch_unwind`] is used. 1740 | #[macro_export] 1741 | macro_rules! assert { 1742 | ($cond:expr) => ({ 1743 | if $crate::check_assert_condition(|| $cond) { 1744 | panic!("dhat: assertion failed: {}", stringify!($cond)); 1745 | } 1746 | }); 1747 | ($cond:expr, $($arg:tt)+) => ({ 1748 | if $crate::check_assert_condition(|| $cond) { 1749 | panic!("dhat: assertion failed: {}: {}", stringify!($cond), format_args!($($arg)+)); 1750 | } 1751 | }); 1752 | } 1753 | 1754 | /// Asserts that two expressions are equal. 1755 | /// 1756 | /// Like [`std::assert_eq!`], additional format arguments are supported. On 1757 | /// failure, this macro will save the profile data and panic. 1758 | /// 1759 | /// # Panics 1760 | /// 1761 | /// Panics immediately (without saving the profile data) in the following 1762 | /// circumstances. 1763 | /// - If called when a [`Profiler`] is not running or is not in testing mode. 1764 | /// - If called after a previous `dhat` assertion has failed with the current 1765 | /// [`Profiler`]. This is possible if [`std::panic::catch_unwind`] is used. 1766 | #[macro_export] 1767 | macro_rules! assert_eq { 1768 | ($left:expr, $right:expr $(,)?) => ({ 1769 | if $crate::check_assert_condition( || $left == $right) { 1770 | panic!( 1771 | "dhat: assertion failed: `(left == right)`\n left: `{:?}`,\n right: `{:?}`", 1772 | $left, $right 1773 | ); 1774 | } 1775 | }); 1776 | ($left:expr, $right:expr, $($arg:tt)+) => ({ 1777 | if $crate::check_assert_condition(|| $left == $right) { 1778 | panic!( 1779 | "dhat: assertion failed: `(left == right)`\n left: `{:?}`,\n right: `{:?}`: {}", 1780 | $left, $right, format_args!($($arg)+) 1781 | ); 1782 | } 1783 | }); 1784 | } 1785 | 1786 | /// Asserts that two expressions are not equal. 1787 | /// 1788 | /// Like [`std::assert_ne!`], additional format arguments are supported. On 1789 | /// failure, this macro will save the profile data and panic. 1790 | /// 1791 | /// # Panics 1792 | /// 1793 | /// Panics immediately (without saving the profile data) in the following 1794 | /// circumstances. 1795 | /// - If called when a [`Profiler`] is not running or is not in testing mode. 1796 | /// - If called after a previous `dhat` assertion has failed with the current 1797 | /// [`Profiler`]. This is possible if [`std::panic::catch_unwind`] is used. 1798 | #[macro_export] 1799 | macro_rules! assert_ne { 1800 | ($left:expr, $right:expr) => ({ 1801 | if $crate::check_assert_condition(|| $left != $right) { 1802 | panic!( 1803 | "dhat: assertion failed: `(left != right)`\n left: `{:?}`,\n right: `{:?}`", 1804 | $left, $right 1805 | ); 1806 | } 1807 | }); 1808 | ($left:expr, $right:expr, $($arg:tt)+) => ({ 1809 | if $crate::check_assert_condition(|| $left != $right) { 1810 | panic!( 1811 | "dhat: assertion failed: `(left != right)`\n left: `{:?}`,\n right: `{:?}`: {}", 1812 | $left, $right, format_args!($($arg)+) 1813 | ); 1814 | } 1815 | }); 1816 | } 1817 | 1818 | // A Rust representation of DHAT's JSON file format, which is described in 1819 | // comments in dhat/dh_main.c in Valgrind's source code. 1820 | // 1821 | // Building this structure in order to serialize does take up some memory. We 1822 | // could instead stream the JSON output directly to file ourselves. This would 1823 | // be more efficient but make the code uglier. 1824 | #[derive(Serialize)] 1825 | #[allow(non_snake_case)] 1826 | struct DhatJson { 1827 | dhatFileVersion: u32, 1828 | mode: &'static str, 1829 | verb: &'static str, 1830 | bklt: bool, 1831 | bkacc: bool, 1832 | #[serde(skip_serializing_if = "Option::is_none")] 1833 | bu: Option<&'static str>, 1834 | #[serde(skip_serializing_if = "Option::is_none")] 1835 | bsu: Option<&'static str>, 1836 | #[serde(skip_serializing_if = "Option::is_none")] 1837 | bksu: Option<&'static str>, 1838 | tu: &'static str, 1839 | Mtu: &'static str, 1840 | #[serde(skip_serializing_if = "Option::is_none")] 1841 | tuth: Option, 1842 | cmd: String, 1843 | pid: u32, 1844 | #[serde(skip_serializing_if = "Option::is_none")] 1845 | tg: Option, 1846 | te: u128, 1847 | pps: Vec, 1848 | ftbl: Vec, 1849 | } 1850 | 1851 | // A Rust representation of a PpInfo within DHAT's JSON file format. 1852 | #[derive(Serialize)] 1853 | struct PpInfoJson { 1854 | // `PpInfo::total_bytes and `PpInfo::total_blocks. 1855 | tb: u64, 1856 | tbk: u64, 1857 | 1858 | // Derived from `PpInfo::total_lifetimes_duration`. 1859 | #[serde(skip_serializing_if = "Option::is_none")] 1860 | tl: Option, 1861 | 1862 | // `PpInfo::max_bytes` and `PpInfo::max_blocks`. 1863 | #[serde(skip_serializing_if = "Option::is_none")] 1864 | mb: Option, 1865 | #[serde(skip_serializing_if = "Option::is_none")] 1866 | mbk: Option, 1867 | 1868 | // `PpInfo::at_tgmax_bytes` and `PpInfo::at_tgmax_blocks`. 1869 | #[serde(skip_serializing_if = "Option::is_none")] 1870 | gb: Option, 1871 | #[serde(skip_serializing_if = "Option::is_none")] 1872 | gbk: Option, 1873 | 1874 | // `PpInfo::curr_bytes` and `PpInfo::curr_blocks` (at termination, i.e. 1875 | // "end"). 1876 | #[serde(skip_serializing_if = "Option::is_none")] 1877 | eb: Option, 1878 | #[serde(skip_serializing_if = "Option::is_none")] 1879 | ebk: Option, 1880 | 1881 | // Frames. Each element is an index into `ftbl`. 1882 | fs: Vec, 1883 | } 1884 | 1885 | impl PpInfoJson { 1886 | fn new(pp_info: &PpInfo, fs: Vec) -> Self { 1887 | if let Some(h) = &pp_info.heap { 1888 | Self { 1889 | tb: pp_info.total_bytes, 1890 | tbk: pp_info.total_blocks, 1891 | tl: Some(h.total_lifetimes_duration.as_micros()), 1892 | mb: Some(h.max_bytes), 1893 | mbk: Some(h.max_blocks), 1894 | gb: Some(h.at_tgmax_bytes), 1895 | gbk: Some(h.at_tgmax_blocks), 1896 | eb: Some(h.curr_bytes), 1897 | ebk: Some(h.curr_blocks), 1898 | fs, 1899 | } 1900 | } else { 1901 | Self { 1902 | tb: pp_info.total_bytes, 1903 | tbk: pp_info.total_blocks, 1904 | tl: None, 1905 | mb: None, 1906 | mbk: None, 1907 | gb: None, 1908 | gbk: None, 1909 | eb: None, 1910 | ebk: None, 1911 | fs, 1912 | } 1913 | } 1914 | } 1915 | } 1916 | 1917 | // A change in size. Used for `realloc`. 1918 | #[derive(Clone, Copy)] 1919 | struct Delta { 1920 | shrinking: bool, 1921 | size: usize, 1922 | } 1923 | 1924 | impl Delta { 1925 | fn new(old_size: usize, new_size: usize) -> Delta { 1926 | if new_size < old_size { 1927 | Delta { 1928 | shrinking: true, 1929 | size: old_size - new_size, 1930 | } 1931 | } else { 1932 | Delta { 1933 | shrinking: false, 1934 | size: new_size - old_size, 1935 | } 1936 | } 1937 | } 1938 | } 1939 | 1940 | impl AddAssign for usize { 1941 | fn add_assign(&mut self, rhs: Delta) { 1942 | if rhs.shrinking { 1943 | *self -= rhs.size; 1944 | } else { 1945 | *self += rhs.size; 1946 | } 1947 | } 1948 | } 1949 | 1950 | impl AddAssign for u64 { 1951 | fn add_assign(&mut self, rhs: Delta) { 1952 | if rhs.shrinking { 1953 | *self -= rhs.size as u64; 1954 | } else { 1955 | *self += rhs.size as u64; 1956 | } 1957 | } 1958 | } 1959 | 1960 | // For testing purposes only. 1961 | #[doc(hidden)] 1962 | pub fn assert_is_panic R + std::panic::UnwindSafe>(f: F, expected: &str) { 1963 | let res = std::panic::catch_unwind(f); 1964 | if let Err(err) = res { 1965 | if let Some(actual) = err.downcast_ref::<&str>() { 1966 | std::assert_eq!(expected, *actual); 1967 | } else if let Some(actual) = err.downcast_ref::() { 1968 | std::assert_eq!(expected, actual); 1969 | } else { 1970 | panic!("assert_is_panic: Not a string: {:?}", err); 1971 | } 1972 | } else { 1973 | panic!("assert_is_panic: Not an error"); 1974 | } 1975 | } 1976 | 1977 | #[cfg(test)] 1978 | mod test { 1979 | use super::trim_path; 1980 | use std::path::Path; 1981 | 1982 | #[test] 1983 | fn test_trim_path() { 1984 | std::assert_eq!(trim_path(Path::new("")), Path::new("")); 1985 | std::assert_eq!(trim_path(Path::new("/")), Path::new("/")); 1986 | std::assert_eq!(trim_path(Path::new("aa.rs")), Path::new("aa.rs")); 1987 | std::assert_eq!(trim_path(Path::new("/aa.rs")), Path::new("/aa.rs")); 1988 | std::assert_eq!(trim_path(Path::new("bb/aa.rs")), Path::new("bb/aa.rs")); 1989 | std::assert_eq!(trim_path(Path::new("/bb/aa.rs")), Path::new("/bb/aa.rs")); 1990 | std::assert_eq!( 1991 | trim_path(Path::new("cc/bb/aa.rs")), 1992 | Path::new("cc/bb/aa.rs") 1993 | ); 1994 | std::assert_eq!( 1995 | trim_path(Path::new("/cc/bb/aa.rs")), 1996 | Path::new("cc/bb/aa.rs") 1997 | ); 1998 | std::assert_eq!( 1999 | trim_path(Path::new("dd/cc/bb/aa.rs")), 2000 | Path::new("cc/bb/aa.rs") 2001 | ); 2002 | std::assert_eq!( 2003 | trim_path(Path::new("/dd/cc/bb/aa.rs")), 2004 | Path::new("cc/bb/aa.rs") 2005 | ); 2006 | } 2007 | } 2008 | --------------------------------------------------------------------------------