async_alloc_counter/lib.rs
1//! async-alloc-counter measures max allocations in a future invocation
2//!
3//! see `examples/` for usage
4//!
5//! This allocator can be used as follows:
6//!
7//! ```rust
8//! use async_alloc_counter::*;
9//! use futures::FutureExt;
10//! use std::{alloc::System, time::Duration};
11//!
12//! // set up the counting allocator
13//! #[global_allocator]
14//! static GLOBAL: AsyncAllocatorCounter<System> = AsyncAllocatorCounter { allocator: System };
15//!
16//! #[tokio::main]
17//! async fn main() {
18//! async move {
19//! let mut v: Vec<u8> = Vec::with_capacity(1024);
20//! }.count_allocations()
21//! .map(move |(max, ())| {
22//! println!("future allocated {} max bytes", max);
23//! })
24//! .await
25//! }
26//! ```
27//!
28//! Allocation measurement can be stacked:
29//!
30//! ```rust,ignore
31//! async move {
32//! println!("wrapping future");
33//! tokio::time::sleep(std::timeDuration::from_secs(1)).await;
34//! let mut v: Vec<u8> = Vec::with_capacity(256);
35//!
36//! async move {
37//! let mut v: Vec<u8> = Vec::with_capacity(1024);
38//! }.count_allocations()
39//! .map(move |(max, ())| {
40//! println!("future allocated {} max bytes", max);
41//! })
42//! .await
43//! }.count_allocations()
44//! .map(move |(max, ())| {
45//! println!("warpping future allocated {} max bytes", max);
46//! })
47//! .await
48//! ```
49//!
50//! Design inspired by the excellent [tracing](https://crates.io/crates/tracing) crate
51use pin_project_lite::pin_project;
52use std::{
53 alloc::{GlobalAlloc, Layout},
54 cell::RefCell,
55 future::Future,
56 pin::Pin,
57 task::{Context, Poll},
58 thread_local,
59};
60
61/// Allocator wrapper
62///
63/// this allocator will measure how much data is allocated in the future
64/// that is currently executed
65///
66/// Set t up as follows:
67///
68/// ```rust
69/// use async_alloc_counter::AsyncAllocatorCounter;
70/// use std::{alloc::System, time::Duration};
71
72/// #[global_allocator]
73/// static GLOBAL: AsyncAllocatorCounter<System> = AsyncAllocatorCounter { allocator: System };
74/// ```
75pub struct AsyncAllocatorCounter<T> {
76 pub allocator: T,
77}
78
79unsafe impl<T: GlobalAlloc> GlobalAlloc for AsyncAllocatorCounter<T> {
80 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
81 CURRENT_FRAME.with(|frame| {
82 if let Some(f) = (*frame.borrow_mut()).as_mut() {
83 f.current += layout.size();
84 if f.current > f.max {
85 f.max = f.current;
86 }
87 }
88 });
89
90 self.allocator.alloc(layout)
91 }
92
93 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
94 CURRENT_FRAME.with(|frame| {
95 if let Some(f) = (*frame.borrow_mut()).as_mut() {
96 f.current -= layout.size();
97 }
98 });
99
100 self.allocator.dealloc(ptr, layout)
101 }
102}
103
104/// measure allocations for a future
105pub trait TraceAlloc: Sized {
106 /// Wraps the future and returns a new one that will resolve
107 /// to `(max allocations, future result)`
108 ///
109 /// You can measure multiple nested futures
110 fn count_allocations(self) -> TraceAllocator<Self> {
111 TraceAllocator {
112 inner: self,
113 previous: None,
114 max: 0,
115 current: 0,
116 }
117 }
118}
119
120impl<T: Sized + Future<Output = O>, O> TraceAlloc for T {}
121
122pin_project! {
123 pub struct TraceAllocator<T> {
124 #[pin]
125 inner: T,
126 max: usize,
127 current: usize,
128 previous: Option<AllocationFrame>,
129 }
130}
131
132thread_local! {
133 static CURRENT_FRAME: RefCell<Option<AllocationFrame>> = RefCell::new(None);
134}
135
136impl<T: Future> Future for TraceAllocator<T> {
137 type Output = (usize, T::Output);
138
139 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
140 let this = self.project();
141
142 assert!(this.previous.is_none());
143 let current = Some(AllocationFrame {
144 // we want to know the maximum allocation in this current call of the
145 // future, we start at the currently allocated value
146 max: *this.current,
147 current: *this.current,
148 });
149
150 // store the allocation frame from upper layers if there was one
151 *this.previous = CURRENT_FRAME.with(|frame| {
152 let prev = (*frame.borrow_mut()).take();
153 *frame.borrow_mut() = current;
154 prev
155 });
156
157 let res = this.inner.poll(cx);
158
159 let mut previous = this.previous.take();
160 if let Some(AllocationFrame { max, current }) = CURRENT_FRAME.with(|frame| {
161 let current = (*frame.borrow_mut()).take();
162
163 if let Some(prev) = previous.as_mut() {
164 if let Some(f) = current.as_ref() {
165 // prev.current contains the total current allocation in the
166 // previous frame, including allocations in the current frame
167 // to chek if we can raise the max in the previous frame,
168 // we need to substrat this.current, which was already
169 // integrated in prev.current, and add the max allocations
170 // seen in the current invocation
171 if prev.current - *this.current + f.max > prev.max {
172 prev.max = prev.current - *this.current + f.max;
173 }
174
175 if f.current > *this.current {
176 prev.current += f.current - *this.current;
177 } else {
178 prev.current -= *this.current - f.current;
179 }
180 }
181 }
182
183 *frame.borrow_mut() = previous;
184 current
185 }) {
186 if max > *this.max {
187 *this.max = max;
188 }
189 *this.current = current;
190 }
191
192 match res {
193 Poll::Pending => Poll::Pending,
194 Poll::Ready(value) => Poll::Ready((*this.max, value)),
195 }
196 }
197}
198
199/// statistics for the current allocation frame
200struct AllocationFrame {
201 max: usize,
202 current: usize,
203}