From 7bfd3155e1cb8caf82ba876c42143d090d651a38 Mon Sep 17 00:00:00 2001 From: Roman Useinov Date: Thu, 5 Dec 2024 14:06:37 +0100 Subject: [PATCH] chore: tests for Lazy and moving out of unstable (#1268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 <26653921+dj8yfo@users.noreply.github.com> --- near-sdk/src/store/mod.rs | 4 - near-sdk/tests/store_performance_tests.rs | 117 +++++++++++++++--- near-sdk/tests/test-contracts/lazy/Cargo.toml | 12 ++ near-sdk/tests/test-contracts/lazy/src/lib.rs | 96 ++++++++++++++ 4 files changed, 206 insertions(+), 23 deletions(-) create mode 100644 near-sdk/tests/test-contracts/lazy/Cargo.toml create mode 100644 near-sdk/tests/test-contracts/lazy/src/lib.rs diff --git a/near-sdk/src/store/mod.rs b/near-sdk/src/store/mod.rs index ad1f3c5f7..3dbbd1e8f 100644 --- a/near-sdk/src/store/mod.rs +++ b/near-sdk/src/store/mod.rs @@ -71,14 +71,10 @@ //! place of a type [`Option`](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; diff --git a/near-sdk/tests/store_performance_tests.rs b/near-sdk/tests/store_performance_tests.rs index cb2a7e573..ff6d210de 100644 --- a/near-sdk/tests/store_performance_tests.rs +++ b/near-sdk/tests/store_performance_tests.rs @@ -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; @@ -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); @@ -53,16 +59,20 @@ async fn dev_generate( Ok((account.into_result()?, collection)) } -async fn setup_worker() -> anyhow::Result<(Arc>, AccountId)> { +async fn setup_worker(contract: Contract) -> anyhow::Result<(Arc>, 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) { // Constraints a bit relaxed to account for binary differences due to on-demand compilation. assert!( total_gas < NearGas::from_tgas(110).as_gas(), @@ -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) @@ -80,7 +90,7 @@ fn perform_asserts(total_gas: u64, col: &Collection) { #[allow(unused)] async fn setup_several(num: usize) -> anyhow::Result<(Vec, 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 { @@ -92,8 +102,8 @@ async fn setup_several(num: usize) -> anyhow::Result<(Vec, 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?; @@ -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 { @@ -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 @@ -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(()) @@ -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 { @@ -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(()) @@ -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 { @@ -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(()) @@ -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 { @@ -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(()) @@ -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 = &[ @@ -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 @@ -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(()) +} diff --git a/near-sdk/tests/test-contracts/lazy/Cargo.toml b/near-sdk/tests/test-contracts/lazy/Cargo.toml new file mode 100644 index 000000000..42591aedc --- /dev/null +++ b/near-sdk/tests/test-contracts/lazy/Cargo.toml @@ -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] diff --git a/near-sdk/tests/test-contracts/lazy/src/lib.rs b/near-sdk/tests/test-contracts/lazy/src/lib.rs new file mode 100644 index 000000000..6d2ac49a7 --- /dev/null +++ b/near-sdk/tests/test-contracts/lazy/src/lib.rs @@ -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, +} + +#[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(); + } + } +}