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}