Determine the size in bytes an object occupies inside RAM.
[`size_of`](core::mem::size_of) already reports how many bytes a value occupies on the stack. Applications like caches or memory budgets also need to know how many bytes a value owns on the *heap*, which is what the [`GetSize`] trait adds.
```toml
get-size2 = { version = "^0.10", features = ["derive"] }
```
# Quick start
```rust
use get_size2::GetSize;
let value = String::from("Hello World!");
// The 12 bytes of the string data live on the heap.
assert_eq!(value.get_heap_size(), 12);
assert_eq!(value.get_size(), String::get_stack_size() + 12);
// Only the allocated capacity is accounted for, not the length.
let mut buffer = Vec::with_capacity(1024);
buffer.push(1u8);
assert_eq!(buffer.get_heap_size(), 1024);
```
The trait provides three methods:
| [`get_stack_size`](GetSize::get_stack_size) | The bytes occupied on the stack, by default [`size_of::<Self>()`](core::mem::size_of) |
| [`get_heap_size`](GetSize::get_heap_size) | The bytes this value owns on the heap |
| [`get_size`](GetSize::get_size) | The sum of both |
It is implemented for the primitives and for most types of [`core`], [`alloc`](https://doc.rust-lang.org/alloc/) and [`std`], for your own types via [the derive macro](#deriving-getsize), and for a number of popular third party types behind [feature flags](#feature-flags).
# Ownership based accounting
Only bytes *owned* by a value are accounted for. Borrowed bytes belong to their owner, so anything reached through a reference or a raw pointer counts as zero:
```rust
use get_size2::GetSize;
#[derive(GetSize)]
struct Borrowing<'a> {
value: &'a String,
}
let value = String::from("hello");
assert_eq!(value.get_heap_size(), 5);
// The struct only borrows the string, so its heap bytes are not counted.
let borrowing = Borrowing { value: &value };
assert_eq!(borrowing.get_heap_size(), 0);
```
Beware of auto-dereferencing when calling the method on a reference yourself, since the compiler will resolve it to the referenced value:
```rust
use get_size2::GetSize;
let value = String::from("hello");
assert_eq!((&value).get_heap_size(), 5); // Rewritten to `value.get_heap_size()`!
assert_eq!(GetSize::get_heap_size(&&value), 0); // Fully qualified syntax, no deref.
assert_eq!(get_size2::heap_size(&&value), 0); // Or use the free function.
```
The derive macro always uses fully qualified syntax, so derived implementations are not affected.
Shared ownership is the one exception to the borrowing rule: an `Rc` or `Arc` genuinely owns its allocation, so its contents *are* counted, including the stack bytes of the value it points to.
```rust
use get_size2::GetSize;
use std::sync::Arc;
let value = Arc::new(String::from("hello"));
assert_eq!(value.get_heap_size(), String::get_stack_size() + 5);
```
# Tracking shared ownership
Because shared ownership is counted, a structure holding several handles to the *same* allocation would account for it more than once. A [`GetSizeTracker`] prevents that by remembering which addresses it has already seen:
```rust
use get_size2::{GetSize, StandardTracker};
use std::sync::Arc;
let shared = Arc::new(String::from("hello"));
let pair = (Arc::clone(&shared), Arc::clone(&shared));
let single = String::get_stack_size() + 5;
// Without a tracker, both handles are accounted for.
assert_eq!(pair.get_heap_size(), 2 * single);
// With a tracker, the shared allocation is only counted once.
let (size, _tracker) = pair.get_heap_size_with_tracker(StandardTracker::new());
assert_eq!(size, single);
```
Derived implementations track by default: the [`get_heap_size`](GetSize::get_heap_size) generated by the derive macro starts a fresh [`default_tracker`] and passes it through all fields.
```rust
use get_size2::GetSize;
use std::sync::Arc;
#[derive(GetSize)]
struct Shared {
first: Arc<String>,
second: Arc<String>,
}
let shared = Arc::new(String::from("hello"));
let value = Shared {
first: Arc::clone(&shared),
second: Arc::clone(&shared),
};
assert_eq!(value.get_heap_size(), String::get_stack_size() + 5);
```
Three trackers ship with this crate:
| [`StandardTracker`] | Remembers every address it has seen. Requires the `alloc` feature. |
| [`NoTracker`] | Tracks nothing and always returns the same answer. Used by the untracked methods. |
| [`DefaultTracker`] | Alias for whichever of the two the derive macro can use, see [`default_tracker`]. |
[`GetSizeTracker`] is also implemented for `&mut T`, `Box<T>`, `Mutex<T>`, `RwLock<T>`, `Arc<Mutex<T>>` and `Arc<RwLock<T>>` of any tracker, so a single tracker can be reused across calls or shared between threads. Implement the trait yourself if you need different bookkeeping.
# Deriving `GetSize`
Enable the `derive` feature and add `#[derive(GetSize)]` to your struct or enum. The generated implementation sums up the heap size of every field:
```rust
use get_size2::GetSize;
#[derive(GetSize)]
struct Data {
name: String,
id: u64,
}
let data = Data { name: "Adam".into(), id: 1 };
assert_eq!(data.get_heap_size(), 4);
```
Fields whose type does not implement [`GetSize`] can be ignored, given a fixed size, or measured by a helper function using the `#[get_size(...)]` attribute. See the [`get-size-derive2` documentation](https://docs.rs/get-size-derive2) for the full attribute reference. Unions are not supported.
# Implementing `GetSize` manually
Only [`get_heap_size_with_tracker`](GetSize::get_heap_size_with_tracker) needs to be implemented; the other methods have suitable default implementations. The default returns `0`, which is already correct for any type that does not own heap memory.
```rust
use get_size2::{GetSize, GetSizeTracker};
struct Buffer {
data: Vec<u8>,
len: usize,
}
impl GetSize for Buffer {
fn get_heap_size_with_tracker<T: GetSizeTracker>(&self, tracker: T) -> (usize, T) {
// Pass the tracker on so shared ownership deeper in the structure stays deduplicated.
self.data.get_heap_size_with_tracker(tracker)
}
}
let buffer = Buffer { data: Vec::with_capacity(64), len: 0 };
assert_eq!(buffer.get_heap_size(), 64);
```
# Feature flags
## `no_std` support
This crate is `#![no_std]`. Which standard library implementations are available is controlled by two features, both enabled by the `std` default feature:
| _(none)_ | [`core`] types, like the primitives, [`Option`], [`Result`], ranges, tuples, arrays, references and raw pointers |
| `alloc` | [`alloc`](https://doc.rust-lang.org/alloc/) types, like [`Vec`](alloc::vec::Vec), [`String`](alloc::string::String), [`Box`](alloc::boxed::Box), `Rc` and `Arc` |
| `std` | Types only provided by [`std`], like [`HashMap`](std::collections::HashMap), [`PathBuf`](std::path::PathBuf) or `Mutex` |
To build for a `no_std` target, disable the default features:
```toml
get-size2 = { version = "^0.10", default-features = false, features = ["alloc", "derive"] }
```
Without `alloc` there is nothing to allocate the set of seen addresses with, so [`StandardTracker`] is unavailable and [`default_tracker`] falls back to [`NoTracker`]. That is sufficient, because there is no shared ownership type to track in the first place.
## Integrations
Each of these features implements [`GetSize`] for the types of the respective crate:
| `bytes` | `Bytes`, `BytesMut` | |
| `chrono` | `NaiveDate`, `NaiveTime`, `NaiveDateTime`, `DateTime`, `TimeDelta`, ... | |
| `chrono-tz` | `TzOffset` | |
| `compact-str` | `CompactString` | |
| `dashmap` | `DashMap`, `DashSet` | yes |
| `half` | `f16`, `bf16` | |
| `hashbrown` | `HashMap`, `HashSet`, `HashTable` | |
| `indexmap` | `IndexMap`, `IndexSet` | |
| `ordermap` | `OrderMap`, `OrderSet` | |
| `orx-concurrent-vec` | `ConcurrentVec` | |
| `parking_lot` | `Mutex`, `RwLock`, also as trackers | yes |
| `portable-atomic` | All `portable_atomic` atomics, including the 128 bit ones | |
| `roaring` | `RoaringBitmap`, `RoaringTreemap` | |
| `smallvec` | `SmallVec` | |
| `thin-vec` | `ThinVec` | |
| `url` | `Url` | |
Features marked as needing `std` enable it implicitly. `all-features-no-std` is a convenience pack activating everything which works without `std`, that is every feature except `dashmap` and `parking_lot`.
# Accuracy
The reported sizes are a best effort based on the public API of the measured types:
- Unused capacity is counted, since it is allocated. Small string and small vector optimizations are respected, so inline data is not double counted.
- Allocator bookkeeping, padding between allocations and the internal node overhead of tree and hash based collections are not counted, so results are a lower bound on what the allocator actually reserved.
- Values behind a raw pointer are not counted, since ownership cannot be determined.
- A locking type ([`Mutex`](std::sync::Mutex), [`RwLock`](std::sync::RwLock), `RefCell`) is measured by reading its content. A poisoned lock is measured anyway, while an already mutably borrowed `RefCell` is reported as `0` instead of panicking.