Skip to content

Commit 19fe191

Browse files
authored
Initial implementation of the global buffer arena (#270)
* [core] initial implementation of the global buffer arena * [core] add license header to buffer arena * [core] add module level docs to buffer arena * [core] move license header where it belongs (to the top) * [core] add docs for diagnostic fields and getters * [core] add tests to buffer arena * [core] apply rust fmt * [core] remove allocated field from BufferArena The information is redundant since it can be computed as free + in_use. * [core] improve buffer arena tests * [core] export buffer arena public symbols from core::env2 * [core] fix doc comment link to AsRef and AsMut * [core] remove nightly cell-update feature * [core] enable no_std for BufferArena and mirror thread_local interfacing * [core] fix some obvious no_std mis-compilations * [core] apply rustfmt * [core] apply rustfmt #2 * [core] fix clippy warning in buffer_arena * [core] fix typo Co-Authored-By: Michael Müller <[email protected]> * [core] slightly improve get_buffer impl Co-Authored-By: Michael Müller <[email protected]> * [core] slight improvements * [core] rename LocalKey to GlobalBufferArena * [core] fix no_std build
1 parent 1601b96 commit 19fe191

File tree

2 files changed

+381
-0
lines changed

2 files changed

+381
-0
lines changed

core/src/env2/buffer_arena.rs

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
// Copyright 2018-2019 Parity Technologies (UK) Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! This file defines the global buffer arena that is accessible globally
16+
//! and acts as a cache for allocated heap memory to avoid heap memory
17+
//! throttling resulting from heap usage for intermediate computations.
18+
//!
19+
//! Exactly this happens a lot in the boundaries between SRML contracts
20+
//! and ink! since encoding and decoding of SCALE values has to be done
21+
//! in such an intermediate buffer.
22+
//!
23+
//! Users and systems are advised to share a common set of allocated buffers
24+
//! provided by the global buffer arena.
25+
26+
use crate::{
27+
env2::utils::{
28+
EnlargeTo,
29+
Reset,
30+
},
31+
memory::vec::Vec,
32+
};
33+
use cfg_if::cfg_if;
34+
use core::cell::{
35+
Cell,
36+
RefCell,
37+
};
38+
39+
/// The maximum amount of used byte buffers at the same time.
40+
///
41+
/// Since the whole point behind this byte buffer arena is to cache
42+
/// allocated heap memory the number of concurrent byte buffers that are
43+
/// in use at the same time should be kept small.
44+
const IN_USE_LIMIT: usize = 1000;
45+
46+
cfg_if! {
47+
if #[cfg(feature = "std")] {
48+
thread_local! {
49+
/// Global buffer arena that provides shared recycled buffers to avoid
50+
/// constantly allocating and deallocating heap memory during contract execution.
51+
///
52+
/// # Note
53+
///
54+
/// This is mainly useful to interact with the environment because
55+
/// it requires intermediate buffers for its encoding and decoding.
56+
///
57+
/// # API
58+
///
59+
/// - Can be accessed through [`std::thread::LocalKey::with`].
60+
/// - Provides recycled buffers through the `get_buffer` call.
61+
pub static BUFFER_ARENA: BufferArena = BufferArena::new();
62+
}
63+
} else {
64+
/// Global buffer arena that provides shared recycled buffers to avoid
65+
/// constantly allocating and deallocating heap memory during contract execution.
66+
///
67+
/// # Note
68+
///
69+
/// This is mainly useful to interact with the environment because
70+
/// it requires intermediate buffers for its encoding and decoding.
71+
///
72+
/// # API
73+
///
74+
/// - Can be accessed through [`std::thread::LocalKey::with`].
75+
/// - Provides recycled buffers through the `get_buffer` call.
76+
pub static BUFFER_ARENA: GlobalBufferArena = GlobalBufferArena::new(BufferArena::new());
77+
78+
/// Wrapper around `BufferArena` to provide similar interface
79+
/// as `std::thread::LocalKey` provided by `thread_local` does.
80+
///
81+
/// Also acts as safety guard to prevent references to `BufferRef`
82+
/// escape the closure using the [`GlobalBufferArena::with`] API.
83+
pub struct GlobalBufferArena {
84+
/// The wrapped buffer arena.
85+
arena: BufferArena,
86+
}
87+
88+
/// CRITICAL NOTE
89+
/// =============
90+
///
91+
/// The wrapped `BufferArena` type itself is __NOT__ `Sync` since it is using
92+
/// `Cell` and `RefCell` internally instead of the thread-safe alternatives.
93+
/// However, since Wasm smart contracts are guaranteed to operate single
94+
/// threaded we can allow for this unsafe `Sync` implementation to allow
95+
/// for having the global static `BUFFER_ARENA` variable and as long as we
96+
/// are only operating single threaded this shouldn't be unsafe.
97+
unsafe impl Sync for GlobalBufferArena {}
98+
99+
impl GlobalBufferArena {
100+
/// Creates a new `GlobalBufferArena` from the given `BufferArena`.
101+
pub const fn new(arena: BufferArena) -> Self {
102+
Self { arena }
103+
}
104+
105+
/// Runs the given closure for the wrapped `BufferArena`.
106+
///
107+
/// This way no references may escape the closure.
108+
pub fn with<F>(&self, f: F)
109+
where
110+
F: FnOnce(&BufferArena),
111+
{
112+
f(&self.arena)
113+
}
114+
}
115+
}
116+
}
117+
118+
/// A byte buffer arena that acts as a cache for allocated heap memory.
119+
pub struct BufferArena {
120+
/// The currently available byte buffers.
121+
free: RefCell<Vec<Buffer>>,
122+
/// Counts the buffers that are currently in use at the same time.
123+
///
124+
/// # Note
125+
///
126+
/// This value is purely used as diagnostic measures to provide
127+
/// smart contract writers with feedback if their implementation
128+
/// is abusing the buffer arena.
129+
/// We might want to turn these checks off for Wasm compilation.
130+
in_use: Cell<usize>,
131+
}
132+
133+
impl BufferArena {
134+
/// Returns a new empty buffer arena.
135+
///
136+
/// Since this acts as cache we only require one instance of this type
137+
/// that we use as `thread_local` global which is safe since
138+
/// Wasm smart contracts are guaranteed to run in a single thread.
139+
pub(self) const fn new() -> Self {
140+
Self {
141+
free: RefCell::new(Vec::new()),
142+
in_use: Cell::new(0),
143+
}
144+
}
145+
146+
/// Returns a fresh buffer that can be used for intermediate computations.
147+
///
148+
/// Buffers returned through this API implement all the necessary traits
149+
/// required to use them as environment buffer.
150+
///
151+
/// - [`Reset`]: Allows resetting the buffer. This clears all the elements,
152+
/// however, it retains the memory allocation.
153+
/// - [`EnlargeTo`]: Safely enlarges the buffer to the required minimum size
154+
/// if it isn't already large enough.
155+
/// - [`core::convert::AsRef`]`<[u8]>`: Returns a shared view into the byte buffer.
156+
/// - [`core::convert::AsMut`]`<[u8]>`: Returns an exclusive view into the byte buffer.
157+
pub fn get_buffer(&self) -> BufferRef {
158+
self.in_use.set(self.in_use() + 1);
159+
if self.in_use() > IN_USE_LIMIT {
160+
panic!("too many concurrent byte buffers")
161+
}
162+
self.free
163+
.borrow_mut()
164+
.pop()
165+
.unwrap_or_else(Buffer::new)
166+
.into_ref()
167+
}
168+
169+
/// Returns the buffer to the arena.
170+
///
171+
/// This is only called from the `Drop` implementation of `BufferRef`
172+
/// to return the wrapped buffer back to the global buffer arena instance.
173+
pub(self) fn return_buffer(&self, buffer: Buffer) {
174+
self.in_use.set(self.in_use() - 1);
175+
self.free.borrow_mut().push(buffer)
176+
}
177+
178+
/// Returns the number of buffers that are currently in use at the same time.
179+
pub fn in_use(&self) -> usize {
180+
self.in_use.get()
181+
}
182+
183+
/// Returns the number of buffers that are not in use.
184+
pub fn free(&self) -> usize {
185+
self.free.borrow().len()
186+
}
187+
188+
/// Returns the current number of cached buffers.
189+
pub fn allocated(&self) -> usize {
190+
self.in_use() + self.free()
191+
}
192+
}
193+
194+
/// A byte buffer.
195+
///
196+
/// This is a thin wrapper around a byte vector providing only
197+
/// the minimal interface to be operable as environmental intermediate
198+
/// buffer.
199+
pub struct Buffer {
200+
/// The warpper internal raw byte buffer.
201+
buffer: Vec<u8>,
202+
}
203+
204+
impl<'a> Buffer
205+
where
206+
Self: 'a,
207+
{
208+
/// Returns a new empty byte buffer.
209+
pub(self) const fn new() -> Self {
210+
Self { buffer: Vec::new() }
211+
}
212+
213+
/// Wraps `self` in a buffer reference.
214+
pub(self) fn into_ref(self) -> BufferRef<'a> {
215+
BufferRef {
216+
buffer: self,
217+
lt: core::marker::PhantomData,
218+
}
219+
}
220+
}
221+
222+
impl Reset for Buffer {
223+
fn reset(&mut self) {
224+
Reset::reset(&mut self.buffer)
225+
}
226+
}
227+
228+
impl EnlargeTo for Buffer {
229+
fn enlarge_to(&mut self, new_size: usize) {
230+
EnlargeTo::enlarge_to(&mut self.buffer, new_size)
231+
}
232+
}
233+
234+
impl core::convert::AsRef<[u8]> for Buffer {
235+
fn as_ref(&self) -> &[u8] {
236+
&self.buffer
237+
}
238+
}
239+
240+
impl core::convert::AsMut<[u8]> for Buffer {
241+
fn as_mut(&mut self) -> &mut [u8] {
242+
&mut self.buffer
243+
}
244+
}
245+
246+
/// A buffer reference providing lifetime constrained access
247+
/// to the wrapper byte buffer.
248+
///
249+
/// Buffer references are created by the only global buffer arena
250+
/// instance and make sure that their wrapper buffer is returned to
251+
/// the arena upon `Drop`.
252+
pub struct BufferRef<'a> {
253+
/// The wrapped byte buffer.
254+
buffer: Buffer,
255+
/// The emulated lifetime.
256+
lt: core::marker::PhantomData<fn() -> &'a ()>,
257+
}
258+
259+
impl BufferRef<'_> {
260+
/// Takes the buffer out of the buffer reference
261+
/// leaving behind an empty byte buffer without
262+
/// associated heap allocation.
263+
///
264+
/// Also resets the byte buffer.
265+
fn take_buffer(&mut self) -> Buffer {
266+
Reset::reset(&mut self.buffer);
267+
core::mem::replace(&mut self.buffer, Buffer::new())
268+
}
269+
}
270+
271+
impl Reset for BufferRef<'_> {
272+
fn reset(&mut self) {
273+
Reset::reset(&mut self.buffer)
274+
}
275+
}
276+
277+
impl EnlargeTo for BufferRef<'_> {
278+
fn enlarge_to(&mut self, new_size: usize) {
279+
EnlargeTo::enlarge_to(&mut self.buffer, new_size)
280+
}
281+
}
282+
283+
impl core::convert::AsRef<[u8]> for BufferRef<'_> {
284+
fn as_ref(&self) -> &[u8] {
285+
core::convert::AsRef::<[u8]>::as_ref(&self.buffer)
286+
}
287+
}
288+
289+
impl core::convert::AsMut<[u8]> for BufferRef<'_> {
290+
fn as_mut(&mut self) -> &mut [u8] {
291+
core::convert::AsMut::<[u8]>::as_mut(&mut self.buffer)
292+
}
293+
}
294+
295+
impl Drop for BufferRef<'_> {
296+
fn drop(&mut self) {
297+
// Returns the byte buffer back to the global buffer arena.
298+
BUFFER_ARENA.with(|arena| arena.return_buffer(self.take_buffer()))
299+
}
300+
}
301+
302+
#[cfg(test)]
303+
mod tests {
304+
use super::*;
305+
306+
macro_rules! assert_arena {
307+
(
308+
$arena:ident,
309+
in_use: $expected_in_use:literal,
310+
free: $expected_free:literal,
311+
allocated: $expected_allocated:literal
312+
) => {{
313+
assert_eq!(
314+
$arena.in_use(),
315+
$expected_in_use,
316+
"number of buffers in use doesn't match expected"
317+
);
318+
assert_eq!(
319+
$arena.free(),
320+
$expected_free,
321+
"number of free buffers doens't match expected"
322+
);
323+
assert_eq!(
324+
$arena.allocated(),
325+
$expected_allocated,
326+
"number of allocated buffers doesn't match expected"
327+
);
328+
}};
329+
}
330+
331+
#[test]
332+
fn it_works() {
333+
BUFFER_ARENA.with(|arena| {
334+
assert_arena!(arena, in_use: 0, free: 0, allocated: 0);
335+
// Allocate a single buffer for a short time.
336+
{
337+
let _b = arena.get_buffer();
338+
assert_arena!(arena, in_use: 1, free: 0, allocated: 1);
339+
}
340+
// We should now have a single allocated buffer
341+
// but none in use.
342+
assert_arena!(arena, in_use: 0, free: 1, allocated: 1);
343+
// Allocate a single buffer again so that we see
344+
// it is being reused.
345+
{
346+
let _b = arena.get_buffer();
347+
assert_arena!(arena, in_use: 1, free: 0, allocated: 1);
348+
}
349+
assert_arena!(arena, in_use: 0, free: 1, allocated: 1);
350+
// Now we allocate 3 buffers in their own scope
351+
// and check the `in_use` and `allocated`.
352+
{
353+
let _b0 = arena.get_buffer();
354+
{
355+
let _b1 = arena.get_buffer();
356+
{
357+
// At this point we should have 3 buffers
358+
// allocated and in use.
359+
let _b2 = arena.get_buffer();
360+
assert_arena!(arena, in_use: 3, free: 0, allocated: 3);
361+
}
362+
assert_arena!(arena, in_use: 2, free: 1, allocated: 3);
363+
}
364+
assert_arena!(arena, in_use: 1, free: 2, allocated: 3);
365+
}
366+
// At this point we dropped all 3 buffers again
367+
// so none is in use but we still have 3 allocated
368+
// buffers.
369+
assert_arena!(arena, in_use: 0, free: 3, allocated: 3);
370+
});
371+
}
372+
}

core/src/env2/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
//! - For `&mut self` messages prefer using `EnvAccessMut` or `DynEnvAccessMut`.
4343
//! - Direct access to `SrmlEnv` or `TestEnv` is always the least optimal solution and generally not preferred.
4444
45+
mod buffer_arena;
4546
pub mod call;
4647
mod dyn_env;
4748
mod env_access;
@@ -86,7 +87,15 @@ cfg_if! {
8687
}
8788
}
8889

90+
#[cfg(not(feature = "std"))]
91+
pub use self::buffer_arena::GlobalBufferArena;
92+
8993
pub use self::{
94+
buffer_arena::{
95+
BufferArena,
96+
BufferRef,
97+
BUFFER_ARENA,
98+
},
9099
dyn_env::DynEnv,
91100
env_access::{
92101
AccessEnv,

0 commit comments

Comments
 (0)