Unverified Commit 75ad19a2 authored by Andrew Jones's avatar Andrew Jones Committed by GitHub
Browse files

[core] storage2::BinaryHeap (#481)

* BinaryHeap::new()

* Failing FromIterator test

* Dumb push impl

* Dumb peek impl

* Naive push implementation

* Copy in some reference impls for inspiration

* WIP: use storage2::Vec and implement pop

* Fix sift_up

* Fix sift_down

* Test with heap from slice

* Add pass through clear impl

* Add storage trait impls

* Add binary_heap bench, start experimentation

# Conflicts:
#	core/Cargo.toml

* Add tests for push/pull from storage

* Get basic bench working for pushing largest value

* Fmt

* Prevent storage from being cleared in test env

* iter batched

* Make inline setup to test consistency

* Revert to iter_batch tests

* Add populated cache benches

* Fmt

* Factor out bench push

* Refactor benchmark loop on push

* Configure warm-up and measurement times to improve consistency

* Remove redundant test

* Refactor push benches

* Refactor push benches again

* Add Pop benchmark

* Fmt

* Add some comments to the benchmarks

* Remove some todos

* Use iter_batched_ref

* Fix pop benchmark name

* Add test for worst case O(log n) push

* Fmt

* Add push test

* Add peek and pop test

* Fix up sift comments and log tests

* Comments

* Add Reverse wrapper type for min heaps

* Fmt

* Peek mut

* Fmt

* Add empty tests

* Check complexity of pushing smallest value

* Test drop clears cells

* Fix stable build

* Basic property test

* Fix wasm build

* Fmt

* Derive Default, PartialEq, Eq

* Add property test to feature

* Fix doc links

* Fmt

* Remove commented out println

* Avoid potential false positive

* Add punctuation

* Comments

* Fmt

* Move binary_heap to crates

* Make it compile

* Fix up doc test and benches

* Fmt

* Fix license headers
parent 771f1d04
Pipeline #111253 passed with stages
in 10 minutes and 6 seconds
......@@ -137,9 +137,11 @@ impl EnvBackend for EnvInstance {
}
fn clear_contract_storage(&mut self, key: &Key) {
self.callee_account_mut()
.clear_storage(*key)
.expect("callee account is not a smart contract");
if !self.clear_storage_disabled {
self.callee_account_mut()
.clear_storage(*key)
.expect("callee account is not a smart contract");
}
}
fn decode_input<T>(&mut self) -> Result<T>
......
......@@ -93,6 +93,8 @@ pub struct EnvInstance {
chain_extension_handler: ChainExtensionHandler,
/// Emitted events recorder.
emitted_events: EmittedEventsRecorder,
/// Set to true to disable clearing storage
clear_storage_disabled: bool,
}
impl EnvInstance {
......@@ -107,6 +109,7 @@ impl EnvInstance {
#[cfg(feature = "ink-unstable-chain-extensions")]
chain_extension_handler: ChainExtensionHandler::new(),
emitted_events: EmittedEventsRecorder::new(),
clear_storage_disabled: false,
}
}
......@@ -138,6 +141,7 @@ impl EnvInstance {
#[cfg(feature = "ink-unstable-chain-extensions")]
self.chain_extension_handler.reset();
self.emitted_events.reset();
self.clear_storage_disabled = false;
}
/// Initializes the whole off-chain environment.
......
......@@ -260,6 +260,18 @@ where
<EnvInstance as OnInstance>::on_instance(|instance| instance.advance_block::<T>())
}
/// Set to true to disable clearing storage
///
/// # Note
///
/// Useful for benchmarking because it ensures the initialized storage is maintained across runs,
/// because lazy storage structures automatically clear their associated cells when they are dropped.
pub fn set_clear_storage_disabled(disable: bool) {
<EnvInstance as OnInstance>::on_instance(|instance| {
instance.clear_storage_disabled = disable
})
}
/// The default accounts.
pub struct DefaultAccounts<T>
where
......@@ -339,3 +351,15 @@ where
.and_then(|account| account.get_storage_rw().map_err(Into::into))
})
}
/// Returns the account id of the currently executing contract.
pub fn get_current_contract_account_id<T>() -> Result<T::AccountId>
where
T: Environment,
{
<EnvInstance as OnInstance>::on_instance(|instance| {
let exec_context = instance.exec_context()?;
let callee = exec_context.callee.decode()?;
Ok(callee)
})
}
......@@ -79,3 +79,7 @@ harness = false
name = "bench_hashmap"
path = "benches/bench_hashmap.rs"
harness = false
[[bench]]
name = "bench_binary_heap"
harness = false
// Copyright 2018-2020 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use criterion::{
criterion_group,
criterion_main,
measurement::WallTime,
BatchSize,
BenchmarkGroup,
BenchmarkId,
Criterion,
};
use ink_primitives::Key;
use ink_storage::{
collections::BinaryHeap,
traits::{
KeyPtr,
SpreadLayout,
},
};
use std::time::Duration;
criterion_group!(push, bench_push_empty_cache, bench_push_populated_cache);
criterion_group!(pop, bench_pop_empty_cache, bench_pop_populated_cache);
criterion_main!(push, pop);
/// Returns some test values for use in benchmarks.
fn test_values(n: u32) -> Vec<u32> {
std::iter::repeat(0u32)
.take(n as usize)
.enumerate()
.map(|(i, _)| i as u32 + 1)
.collect()
}
mod binary_heap {
use super::*;
/// Initialize the contract storage at the given key with the provided values.
///
/// Use for testing lazy loading of a binary heap: an instance with an associated key which is
/// yet to load any elements from storage. This is the state a binary heap instance will be in
/// at the start of contract execution.
pub fn init_storage(root_key: Key, values: &[u32]) {
let heap = from_slice(values);
SpreadLayout::push_spread(&heap, &mut KeyPtr::from(root_key));
// prevents storage for the test heap being cleared when the heap is dropped after each
// benchmark iteration
ink_env::test::set_clear_storage_disabled(true);
}
/// Creates a binary heap from the given slice.
pub fn from_slice(slice: &[u32]) -> BinaryHeap<u32> {
slice.iter().copied().collect::<BinaryHeap<u32>>()
}
}
fn bench_push_empty_cache(c: &mut Criterion) {
bench_heap_sizes::<_, _, Push>(
c,
"BinaryHeap::push (empty cache)",
binary_heap::init_storage,
NewHeap::lazy,
);
}
fn bench_push_populated_cache(c: &mut Criterion) {
bench_heap_sizes::<_, _, Push>(
c,
"BinaryHeap::push (populated cache)",
|_: Key, _: &[u32]| {},
NewHeap::populated,
);
}
fn bench_pop_empty_cache(c: &mut Criterion) {
bench_heap_sizes::<_, _, Pop>(
c,
"BinaryHeap::pop (empty cache)",
binary_heap::init_storage,
NewHeap::lazy,
);
}
fn bench_pop_populated_cache(c: &mut Criterion) {
bench_heap_sizes::<_, _, Pop>(
c,
"BinaryHeap::pop (populated cache)",
|_: Key, _: &[u32]| {},
NewHeap::populated,
);
}
fn bench_heap_sizes<I, H, B>(c: &mut Criterion, name: &str, init: I, new_test_heap: H)
where
I: Fn(Key, &[u32]),
H: Fn(Key, Vec<u32>) -> NewHeap,
B: Benchmark,
{
let _ = ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
let mut group = c.benchmark_group(name);
group.warm_up_time(Duration::from_secs(6));
group.measurement_time(Duration::from_secs(10));
for (key, size) in [(0u8, 8u32), (1, 16), (2, 32), (3, 64)].iter() {
let root_key = Key::from([*key; 32]);
let test_values = test_values(*size);
// perform one time initialization for this heap size
init(root_key, &test_values);
let test_heap = new_test_heap(root_key, test_values);
<B as Benchmark>::bench(&mut group, *size, test_heap)
}
group.finish();
Ok(())
})
.unwrap();
}
/// Strategies for constructing a binary heap instance for a benchmark routine
enum NewHeap {
/// Create a binary heap with an empty cache, values at the given key are loaded from storage
/// upon access.
///
/// This simulates the state of a binary heap at the beginning of a smart
/// contract's execution.
Lazy(Key),
/// Create a binary heap with all the values loaded into the cache.
///
/// This simulates the state of a binary heap once all elements have been accessed during smart
/// contract execution and loaded into the cache.
Populated(Vec<u32>),
}
impl NewHeap {
pub fn lazy(key: Key, _values: Vec<u32>) -> Self {
Self::Lazy(key)
}
pub fn populated(_key: Key, values: Vec<u32>) -> Self {
Self::Populated(values)
}
pub fn create_heap(&self) -> BinaryHeap<u32> {
match self {
NewHeap::Lazy(root_key) => {
<BinaryHeap<u32> as SpreadLayout>::pull_spread(&mut KeyPtr::from(
*root_key,
))
}
NewHeap::Populated(ref values) => binary_heap::from_slice(values),
}
}
}
/// Define a benchmark for an operation to be run against different size binary heaps
trait Benchmark {
fn bench(group: &mut BenchmarkGroup<WallTime>, size: u32, new_heap: NewHeap);
}
/// Benchmark [`BinaryHeap::push`]
enum Push {}
impl Benchmark for Push {
fn bench(group: &mut BenchmarkGroup<WallTime>, size: u32, new_heap: NewHeap) {
let largest_value = size + 1;
group.bench_with_input(
BenchmarkId::new("largest value", size),
&largest_value,
|b, &value| {
b.iter_batched_ref(
|| new_heap.create_heap(),
|heap| heap.push(value),
BatchSize::SmallInput,
);
},
);
let smallest_value = 0;
group.bench_with_input(
BenchmarkId::new("smallest value", size),
&smallest_value,
|b, &value| {
b.iter_batched_ref(
|| new_heap.create_heap(),
|heap| heap.push(value),
BatchSize::SmallInput,
);
},
);
}
}
/// Benchmark [`BinaryHeap::pop`]
enum Pop {}
impl Benchmark for Pop {
fn bench(group: &mut BenchmarkGroup<WallTime>, size: u32, new_heap: NewHeap) {
group.bench_function(BenchmarkId::from_parameter(size), |b| {
b.iter_batched_ref(
|| new_heap.create_heap(),
|heap| heap.pop(),
BatchSize::SmallInput,
);
});
}
}
// Copyright 2018-2020 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::BinaryHeap;
use crate::traits::PackedLayout;
use core::iter::{
Extend,
FromIterator,
};
impl<T> Extend<T> for BinaryHeap<T>
where
T: PackedLayout + Ord,
{
fn extend<I>(&mut self, iter: I)
where
I: IntoIterator<Item = T>,
{
for item in iter {
self.push(item)
}
}
}
impl<T> FromIterator<T> for BinaryHeap<T>
where
T: PackedLayout + Ord,
{
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = T>,
{
let mut vec = Self::new();
vec.extend(iter);
vec
}
}
// Copyright 2018-2020 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! A priority queue implemented with a binary heap.
//!
//! Insertion and popping the largest element have `O(log(n))` complexity.
//! Checking the largest element is `O(1)`.
mod impls;
mod reverse;
mod storage;
#[cfg(test)]
mod tests;
use super::vec::{
Iter,
IterMut,
Vec as StorageVec,
};
use crate::traits::PackedLayout;
pub use reverse::Reverse;
/// A priority queue implemented with a binary heap.
///
/// # Note
///
/// The heap is a *max-heap* by default, i.e. the first element is the largest.
/// Either [`Reverse`] or a custom `Ord` implementation can be used to
/// make `BinaryHeap` a *min-heap*. This makes `heap.pop()` return the smallest
/// value instead of the largest one.
#[derive(Default, PartialEq, Eq, Debug)]
pub struct BinaryHeap<T>
where
T: PackedLayout + Ord,
{
/// The underlying storage vec.
elems: StorageVec<T>,
}
impl<T> BinaryHeap<T>
where
T: PackedLayout + Ord,
{
/// Creates a new empty storage heap.
pub fn new() -> Self {
Self {
elems: StorageVec::new(),
}
}
/// Returns the number of elements in the heap, also referred to as its 'length'.
pub fn len(&self) -> u32 {
self.elems.len()
}
/// Returns `true` if the heap contains no elements.
pub fn is_empty(&self) -> bool {
self.elems.is_empty()
}
}
impl<T> BinaryHeap<T>
where
T: PackedLayout + Ord,
{
/// Returns an iterator yielding shared references to all elements of the heap.
///
/// # Note
///
/// Avoid unbounded iteration over large heaps.
/// Prefer using methods like `Iterator::take` in order to limit the number
/// of yielded elements.
pub fn iter(&self) -> Iter<T> {
self.elems.iter()
}
/// Returns an iterator yielding exclusive references to all elements of the heap.
///
/// # Note
///
/// Avoid unbounded iteration over big heaps.
/// Prefer using methods like `Iterator::take` in order to limit the number
/// of yielded elements.
pub fn iter_mut(&mut self) -> IterMut<T> {
self.elems.iter_mut()
}
/// Returns a shared reference to the greatest element of the heap
///
/// Returns `None` if the heap is empty
pub fn peek(&self) -> Option<&T> {
self.elems.first()
}
/// Returns an exclusive reference to the greatest element of the heap
///
/// Returns `None` if the heap is empty
///
/// # Note:
///
/// If the `PeekMut` value is leaked, the heap may be in an inconsistent state.
///
/// # Example
///
/// ```
/// use ink_storage::collections::BinaryHeap;
/// let mut heap = BinaryHeap::new();
/// assert!(heap.peek_mut().is_none());
///
/// heap.push(1);
/// heap.push(5);
/// heap.push(2);
/// {
/// let mut val = heap.peek_mut().unwrap();
/// *val = 0;
/// }
/// assert_eq!(heap.peek(), Some(&2));
/// ```
pub fn peek_mut(&mut self) -> Option<PeekMut<'_, T>> {
if self.is_empty() {
None
} else {
Some(PeekMut {
heap: self,
sift: true,
})
}
}
/// Take an element at `pos` and move it down the heap, while its children
/// are smaller.
fn sift_down(&mut self, mut pos: u32) {
let end = self.len();
let mut child = 2 * pos + 1;
while child < end {
let right = child + 1;
// compare with the greater of the two children
if right < end && self.elems.get(child) <= self.elems.get(right) {
child = right;
}
// if we are already in order, stop.
if self.elems.get(pos) >= self.elems.get(child) {
break
}
self.elems.swap(child, pos);
pos = child;
child = 2 * pos + 1;
}
}
/// Pops greatest element from the heap and returns it
///
/// Returns `None` if the heap is empty
pub fn pop(&mut self) -> Option<T> {
// replace the root of the heap with the last element
let elem = self.elems.swap_remove(0);
self.sift_down(0);
elem
}
/// Removes all elements from this heap.
///
/// # Note
///
/// Use this method to clear the vector instead of e.g. iterative `pop()`.
/// This method performs significantly better and does not actually read
/// any of the elements (whereas `pop()` does).
pub fn clear(&mut self) {
self.elems.clear()
}
}
impl<T> BinaryHeap<T>
where
T: PackedLayout + Ord,
{
/// Take an element at `pos` and move it up the heap, while its parent is
/// larger.
fn sift_up(&mut self, mut pos: u32) {
while pos > 0 {
let parent = (pos - 1) / 2;
if self.elems.get(pos) <= self.elems.get(parent) {
break
}
self.elems.swap(parent, pos);
pos = parent;
}
}
/// Pushes the given element to the binary heap.
pub fn push(&mut self, value: T) {
let old_len = self.len();
self.elems.push(value);
self.sift_up(old_len)
}
}
/// Structure wrapping a mutable reference to the greatest item on a
/// [`BinaryHeap`].
///
/// This `struct` is created by the [`BinaryHeap::peek_mut`] method.
pub struct PeekMut<'a, T>
where
T: 'a + PackedLayout + Ord,
{
heap: &'a mut BinaryHeap<T>,
/// If `true`, on `drop()` will sift the peeked value down the tree if after mutation it is no
/// longer the largest value, in order to keep the heap in a consistent state.
///
/// If the peeked value is consumed via `PeekMut::pop()` then this is set to false to prevent
/// a redundant reorg which would already have happened via `BinaryHeap::pop()`.
sift: bool,
}
impl<T> Drop for PeekMut<'_, T>
where
T: PackedLayout + Ord,
{
fn drop(&mut self) {
if self.sift {
self.heap.sift_down(0);
}
}
}
impl<T> core::ops::Deref for PeekMut<'_, T>
where
T: PackedLayout + Ord,
{
type Target = T;
fn deref(&self) -> &T {
self.heap
.elems
.first()
.expect("PeekMut is only instantiated for non-empty heaps")