Skip to content

Commit

Permalink
chore: tests for Lazy and moving out of unstable (#1268)
Browse files Browse the repository at this point in the history
* chore: tests for Lazy and moving out of unstable

* re-enable os-based testing

* fix import

* address comments

* fix clippy

* tune for linux

* more tuning

* tune

* tune

* tune

* tune

* tuning

* override

* fix

* fix

* fix

* more tuning

---------

Co-authored-by: dj8yf0μl <[email protected]>
  • Loading branch information
ruseinov and dj8yfo authored Dec 5, 2024
1 parent 5a4c595 commit 7bfd315
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 23 deletions.
4 changes: 0 additions & 4 deletions near-sdk/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,10 @@
//! place of a type [`Option<T>`](Option). Will only be loaded when interacted with and will
//! persist on [`Drop`].
#[cfg(feature = "unstable")]
mod lazy;
#[cfg(feature = "unstable")]
pub use lazy::Lazy;

#[cfg(feature = "unstable")]
mod lazy_option;
#[cfg(feature = "unstable")]
pub use lazy_option::LazyOption;

pub mod vec;
Expand Down
117 changes: 98 additions & 19 deletions near-sdk/tests/store_performance_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use near_workspaces::types::{KeyType, SecretKey};
use near_workspaces::{Account, Worker};
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::sync::Arc;
use strum_macros::Display;

Expand All @@ -27,6 +28,11 @@ pub enum Collection {
Vector,
}

pub enum Contract {
StoreContract,
LazyContract,
}

fn random_account_id(collection: Collection, seed: &str) -> AccountId {
let mut rng = rand::thread_rng();
let random_num = rng.gen_range(10000000000000usize..99999999999999);
Expand All @@ -53,16 +59,20 @@ async fn dev_generate(
Ok((account.into_result()?, collection))
}

async fn setup_worker() -> anyhow::Result<(Arc<Worker<Sandbox>>, AccountId)> {
async fn setup_worker(contract: Contract) -> anyhow::Result<(Arc<Worker<Sandbox>>, AccountId)> {
let contract_path = match contract {
Contract::StoreContract => "./tests/test-contracts/store",
Contract::LazyContract => "./tests/test-contracts/lazy",
};
let worker = Arc::new(near_workspaces::sandbox().await?);
let wasm = near_workspaces::compile_project("./tests/test-contracts/store").await?;
let wasm = near_workspaces::compile_project(contract_path).await?;
let contract = worker.dev_deploy(&wasm).await?;
let res = contract.call("new").max_gas().transact().await?;
assert!(res.is_success());
Ok((worker, contract.id().clone()))
}

fn perform_asserts(total_gas: u64, col: &Collection) {
fn perform_asserts(total_gas: u64, col: impl Display, override_min_gas: Option<u64>) {
// Constraints a bit relaxed to account for binary differences due to on-demand compilation.
assert!(
total_gas < NearGas::from_tgas(110).as_gas(),
Expand All @@ -71,7 +81,7 @@ fn perform_asserts(total_gas: u64, col: &Collection) {
NearGas::from_gas(total_gas)
);
assert!(
total_gas > NearGas::from_tgas(90).as_gas(),
total_gas > NearGas::from_tgas(override_min_gas.unwrap_or(90)).as_gas(),
"not enough gas consumed {}: {}, adjust the number of iterations to spot regressions",
col,
NearGas::from_gas(total_gas)
Expand All @@ -80,7 +90,7 @@ fn perform_asserts(total_gas: u64, col: &Collection) {

#[allow(unused)]
async fn setup_several(num: usize) -> anyhow::Result<(Vec<Account>, AccountId)> {
let (worker, contract_id) = setup_worker().await?;
let (worker, contract_id) = setup_worker(Contract::StoreContract).await?;
let mut accounts = Vec::new();

for acc_seed in 0..num {
Expand All @@ -92,8 +102,8 @@ async fn setup_several(num: usize) -> anyhow::Result<(Vec<Account>, AccountId)>
Ok((accounts, contract_id))
}

async fn setup() -> anyhow::Result<(Account, AccountId)> {
let (worker, contract_id) = setup_worker().await?;
async fn setup(contract: Contract) -> anyhow::Result<(Account, AccountId)> {
let (worker, contract_id) = setup_worker(contract).await?;

let (account, _) =
dev_generate(worker.clone(), Collection::IterableSet, "seed".to_string()).await?;
Expand All @@ -114,7 +124,7 @@ async fn insert_and_remove() -> anyhow::Result<()> {
Collection::Vector,
];

let (account, contract_id) = setup().await?;
let (account, contract_id) = setup(Contract::StoreContract).await?;
// insert test, max_iterations here is the number of elements to insert. It's used to measure
// relative performance.
for (col, max_iterations) in collection_types.map(|col| match col {
Expand All @@ -137,7 +147,7 @@ async fn insert_and_remove() -> anyhow::Result<()> {
.total_gas_burnt
.as_gas();

perform_asserts(total_gas, &col);
perform_asserts(total_gas, col, None);
}

// remove test, max_iterations here is the number of elements to remove. It's used to measure
Expand All @@ -162,7 +172,7 @@ async fn insert_and_remove() -> anyhow::Result<()> {
.total_gas_burnt
.as_gas();

perform_asserts(total_gas, &col);
perform_asserts(total_gas, col, None);
}

Ok(())
Expand All @@ -181,7 +191,7 @@ async fn iter() -> anyhow::Result<()> {
];

let element_number = 100;
let (account, contract_id) = setup().await?;
let (account, contract_id) = setup(Contract::StoreContract).await?;

// pre-populate
for col in collection_types {
Expand Down Expand Up @@ -215,7 +225,7 @@ async fn iter() -> anyhow::Result<()> {
.total_gas_burnt
.as_gas();

perform_asserts(total_gas, &col);
perform_asserts(total_gas, col, None);
}

Ok(())
Expand All @@ -234,7 +244,7 @@ async fn random_access() -> anyhow::Result<()> {
Collection::Vector,
];
let element_number = 100;
let (account, contract_id) = setup().await?;
let (account, contract_id) = setup(Contract::StoreContract).await?;

// pre-populate
for col in collection_types {
Expand Down Expand Up @@ -277,7 +287,7 @@ async fn random_access() -> anyhow::Result<()> {
.total_gas_burnt
.as_gas();

perform_asserts(total_gas, &col);
perform_asserts(total_gas, col, None);
}

Ok(())
Expand All @@ -297,7 +307,7 @@ async fn contains() -> anyhow::Result<()> {
];
// Each collection gets the same number of elements.
let element_number = 100;
let (account, contract_id) = setup().await?;
let (account, contract_id) = setup(Contract::StoreContract).await?;

// prepopulate
for col in collection_types {
Expand Down Expand Up @@ -332,7 +342,7 @@ async fn contains() -> anyhow::Result<()> {
.total_gas_burnt
.as_gas();

perform_asserts(total_gas, &col);
perform_asserts(total_gas, col, None);
}

Ok(())
Expand All @@ -344,7 +354,7 @@ async fn contains() -> anyhow::Result<()> {
async fn iterable_vs_unordered() -> anyhow::Result<()> {
let element_number = 300;
let deleted_element_number = 299;
let (account, contract_id) = setup().await?;
let (account, contract_id) = setup(Contract::StoreContract).await?;

// We only care about Unordered* and Iterable* collections.
let collection_types = &[
Expand Down Expand Up @@ -395,7 +405,7 @@ async fn iterable_vs_unordered() -> anyhow::Result<()> {
.total_gas_burnt
.as_gas();

perform_asserts(total_gas, &col);
perform_asserts(total_gas, col, None);
}

// random access, repeat here is the number of times we try to access an element in the
Expand All @@ -417,8 +427,77 @@ async fn iterable_vs_unordered() -> anyhow::Result<()> {
.total_gas_burnt
.as_gas();

perform_asserts(total_gas, col);
perform_asserts(total_gas, col, None);
}

Ok(())
}

#[tokio::test]
async fn test_lazy() -> anyhow::Result<()> {
let (account, contract_id) = setup(Contract::LazyContract).await?;

let res = account
.call(&contract_id, "insert_delete")
.args_json((700,))
.max_gas()
.transact()
.await?
.unwrap();

perform_asserts(res.total_gas_burnt.as_gas(), "lazy:insert_delete", None);

let res = account
.call(&contract_id, "insert_delete_flush_once")
.args_json((1700,))
.max_gas()
.transact()
.await?
.unwrap();

perform_asserts(res.total_gas_burnt.as_gas(), "lazy:insert_delete_flush_once", None);

let res = account
.call(&contract_id, "flush")
.args_json((2350000,))
.max_gas()
.transact()
.await?
.unwrap();

// Override min gas to avoid constant tuning, it's pretty clear this is performant. Somehow
// this is pretty flaky.
perform_asserts(res.total_gas_burnt.as_gas(), "lazy:flush", Some(60));

let res = account
.call(&contract_id, "get")
.args_json((2400000,))
.max_gas()
.transact()
.await?
.unwrap();

// Override min gas to avoid constant tuning, it's pretty clear this is performant.
perform_asserts(res.total_gas_burnt.as_gas(), "lazy:get", Some(70));

let res = account
.call(&contract_id, "insert_flush")
.args_json((1200,))
.max_gas()
.transact()
.await?
.unwrap();

perform_asserts(res.total_gas_burnt.as_gas(), "lazy:insert_flush", None);

let res = account
.call(&contract_id, "insert_take")
.args_json((700,))
.max_gas()
.transact()
.await?
.unwrap();

perform_asserts(res.total_gas_burnt.as_gas(), "lazy:insert_take", None);
Ok(())
}
12 changes: 12 additions & 0 deletions near-sdk/tests/test-contracts/lazy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "lazy"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
near-sdk = { path = "../../../../near-sdk", features = ["default", "unstable"] }

[workspace]
96 changes: 96 additions & 0 deletions near-sdk/tests/test-contracts/lazy/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use near_sdk::borsh::{BorshDeserialize, BorshSerialize};
use near_sdk::{near, store::LazyOption, PanicOnDefault};

#[derive(BorshSerialize, BorshDeserialize, Ord, PartialOrd, Eq, PartialEq, Clone)]
#[borsh(crate = "near_sdk::borsh")]
pub struct Insertable {
pub index: u32,
pub data: String,
pub is_valid: bool,
}

#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct LazyContract {
pub lazy_opt: LazyOption<Insertable>,
}

#[near]
impl LazyContract {
#[init]
pub fn new() -> Self {
let lazy_opt = LazyOption::new(b"a", None);
Self { lazy_opt }
}

fn insertable(&self) -> Insertable {
Insertable { index: 0, data: "scatter cinnamon wheel useless please rough situate iron eager noise try evolve runway neglect onion".to_string(), is_valid: true }
}

/// This should only write to the underlying storage once.
#[payable]
pub fn flush(&mut self, iterations: usize) {
let insertable = self.insertable();
self.lazy_opt.set(Some(insertable));

for _ in 0..=iterations {
self.lazy_opt.flush();
}
}

#[payable]
pub fn get(&mut self, iterations: u32) {
let insertable = self.insertable();
self.lazy_opt.set(Some(insertable));
for _ in 0..=iterations {
self.lazy_opt.get();
}
}

/// This should write on each iteration.
#[payable]
pub fn insert_flush(&mut self, iterations: u32) {
let mut insertable = self.insertable();
for idx in 0..=iterations {
insertable.index = idx as u32;
self.lazy_opt.set(Some(insertable.clone()));
self.lazy_opt.flush();
}
}

/// This should write twice on each iteration.
#[payable]
pub fn insert_take(&mut self, iterations: u32) {
let mut insertable = self.insertable();
for idx in 0..=iterations {
insertable.index = idx as u32;
self.lazy_opt.set(Some(insertable.clone()));
self.lazy_opt.flush();
self.lazy_opt.take();
self.lazy_opt.flush();
}
}

/// This should write and delete on each iteration.
#[payable]
pub fn insert_delete(&mut self, iterations: u32) {
let insertable = self.insertable();
for _ in 0..=iterations {
self.lazy_opt.set(Some(insertable.clone()));
self.lazy_opt.flush();
self.lazy_opt.set(None);
self.lazy_opt.flush();
}
}

/// This should write once on each iteration.
#[payable]
pub fn insert_delete_flush_once(&mut self, iterations: u32) {
let insertable = self.insertable();
for _ in 0..=iterations {
self.lazy_opt.set(Some(insertable.clone()));
self.lazy_opt.set(None);
self.lazy_opt.flush();
}
}
}

0 comments on commit 7bfd315

Please sign in to comment.