From b4c027f645df562a0ac5d313a82af93713cd864d Mon Sep 17 00:00:00 2001 From: Maksym Arutyunyan Date: Mon, 14 Jul 2025 11:56:36 +0200 Subject: [PATCH] tests: add nns_vote_cascading benchmarks --- benchmarks/Cargo.toml | 4 + benchmarks/nns/canbench.yml | 3 + benchmarks/nns/canbench_results.yml | 114 ++++++ benchmarks/nns/src/main.rs | 3 + benchmarks/nns/src/nns_vote_cascading.rs | 300 +++++++++++++++ .../nns/src/nns_vote_cascading/benches.rs | 352 ++++++++++++++++++ .../nns/src/nns_vote_cascading/tests.rs | 210 +++++++++++ 7 files changed, 986 insertions(+) create mode 100644 benchmarks/nns/canbench.yml create mode 100644 benchmarks/nns/canbench_results.yml create mode 100644 benchmarks/nns/src/main.rs create mode 100644 benchmarks/nns/src/nns_vote_cascading.rs create mode 100644 benchmarks/nns/src/nns_vote_cascading/benches.rs create mode 100644 benchmarks/nns/src/nns_vote_cascading/tests.rs diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index 470c0b75..05e3b51d 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -34,6 +34,10 @@ path = "io_chunks/src/main.rs" name = "memory_manager" path = "memory_manager/src/main.rs" +[[bin]] +name = "nns" +path = "nns/src/main.rs" + [[bin]] name = "vec" path = "vec/src/main.rs" diff --git a/benchmarks/nns/canbench.yml b/benchmarks/nns/canbench.yml new file mode 100644 index 00000000..ff5c0da5 --- /dev/null +++ b/benchmarks/nns/canbench.yml @@ -0,0 +1,3 @@ +build_cmd: cargo build -p benchmarks --release --target wasm32-unknown-unknown --locked + +wasm_path: ../../target/wasm32-unknown-unknown/release/nns.wasm diff --git a/benchmarks/nns/canbench_results.yml b/benchmarks/nns/canbench_results.yml new file mode 100644 index 00000000..91fda075 --- /dev/null +++ b/benchmarks/nns/canbench_results.yml @@ -0,0 +1,114 @@ +benches: + vote_cascading_heap_centralized_10k: + total: + calls: 1 + instructions: 77758460 + heap_increase: 16 + stable_memory_increase: 0 + scopes: {} + vote_cascading_heap_centralized_1k: + total: + calls: 1 + instructions: 7815132 + heap_increase: 1 + stable_memory_increase: 0 + scopes: {} + vote_cascading_heap_chain_10k_15: + total: + calls: 1 + instructions: 1272207053 + heap_increase: 10 + stable_memory_increase: 0 + scopes: {} + vote_cascading_heap_chain_10k_5: + total: + calls: 1 + instructions: 239648964 + heap_increase: 10 + stable_memory_increase: 0 + scopes: {} + vote_cascading_heap_chain_1k_15: + total: + calls: 1 + instructions: 124352570 + heap_increase: 1 + stable_memory_increase: 0 + scopes: {} + vote_cascading_heap_chain_1k_5: + total: + calls: 1 + instructions: 23867558 + heap_increase: 0 + stable_memory_increase: 0 + scopes: {} + vote_cascading_heap_single_vote_10k: + total: + calls: 1 + instructions: 5639 + heap_increase: 0 + stable_memory_increase: 0 + scopes: {} + vote_cascading_heap_single_vote_1k: + total: + calls: 1 + instructions: 5737 + heap_increase: 0 + stable_memory_increase: 0 + scopes: {} + vote_cascading_stable_centralized_10k: + total: + calls: 1 + instructions: 1375361602 + heap_increase: 10 + stable_memory_increase: 0 + scopes: {} + vote_cascading_stable_centralized_1k: + total: + calls: 1 + instructions: 99869278 + heap_increase: 1 + stable_memory_increase: 0 + scopes: {} + vote_cascading_stable_chain_10k_15: + total: + calls: 1 + instructions: 9800292700 + heap_increase: 5 + stable_memory_increase: 0 + scopes: {} + vote_cascading_stable_chain_10k_5: + total: + calls: 1 + instructions: 3004791001 + heap_increase: 5 + stable_memory_increase: 0 + scopes: {} + vote_cascading_stable_chain_1k_15: + total: + calls: 1 + instructions: 865575963 + heap_increase: 0 + stable_memory_increase: 0 + scopes: {} + vote_cascading_stable_chain_1k_5: + total: + calls: 1 + instructions: 252822713 + heap_increase: 0 + stable_memory_increase: 0 + scopes: {} + vote_cascading_stable_single_vote_10k: + total: + calls: 1 + instructions: 91204 + heap_increase: 0 + stable_memory_increase: 0 + scopes: {} + vote_cascading_stable_single_vote_1k: + total: + calls: 1 + instructions: 66626 + heap_increase: 0 + stable_memory_increase: 0 + scopes: {} +version: 0.2.0 diff --git a/benchmarks/nns/src/main.rs b/benchmarks/nns/src/main.rs new file mode 100644 index 00000000..c81330b0 --- /dev/null +++ b/benchmarks/nns/src/main.rs @@ -0,0 +1,3 @@ +mod nns_vote_cascading; + +fn main() {} diff --git a/benchmarks/nns/src/nns_vote_cascading.rs b/benchmarks/nns/src/nns_vote_cascading.rs new file mode 100644 index 00000000..6dadf5b5 --- /dev/null +++ b/benchmarks/nns/src/nns_vote_cascading.rs @@ -0,0 +1,300 @@ +use ic_stable_structures::{ + memory_manager::{MemoryId, MemoryManager, VirtualMemory}, + DefaultMemoryImpl, StableBTreeMap, +}; +use std::{ + cell::RefCell, + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Vote { + Unspecified, + Yes, + No, +} + +type NeuronId = u64; +type Topic = u8; + +pub trait NeuronStore { + fn set_followees( + &mut self, + follower_neuron_id: NeuronId, + topic: Topic, + followee_neuron_ids: Vec, + ); + fn get_followers(&self, followee_neuron_id: NeuronId, topic: Topic) -> Vec; + fn get_followees(&self, follower_neuron_id: NeuronId, topic: Topic) -> Vec; +} + +type VM = VirtualMemory; + +thread_local! { + static MEMORY_MANAGER: RefCell> = + RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); +} + +type FolloweeNeuronId = NeuronId; +type FollowerNeuronId = NeuronId; +type FolloweeEntryTuple = (FollowerNeuronId, (Topic, u64)); +type FollowerIndexTuple = ((Topic, FolloweeNeuronId), FollowerNeuronId); + +struct StableNeuronStore { + followees_map: StableBTreeMap, + followers_index: StableBTreeMap, +} + +const FOLLOWEES_MEMORY_ID: MemoryId = MemoryId::new(0); +const FOLLOWERS_MEMORY_ID: MemoryId = MemoryId::new(1); + +impl StableNeuronStore { + fn new() -> Self { + MEMORY_MANAGER.with_borrow(|m| Self { + followees_map: StableBTreeMap::new(m.get(FOLLOWEES_MEMORY_ID)), + followers_index: StableBTreeMap::new(m.get(FOLLOWERS_MEMORY_ID)), + }) + } +} + +impl NeuronStore for StableNeuronStore { + fn set_followees( + &mut self, + follower_neuron_id: FollowerNeuronId, + topic: Topic, + followee_neuron_ids: Vec, + ) { + let min_followee_map_key = (follower_neuron_id, (topic, u64::MIN)); + let max_followee_map_key = (follower_neuron_id, (topic, u64::MAX)); + let previous_followees = self + .followees_map + .range(min_followee_map_key..=max_followee_map_key) + .map(|(_key, value)| value) + .collect::>(); + let previous_followee_keys = self + .followees_map + .range(min_followee_map_key..=max_followee_map_key) + .map(|(key, _value)| key) + .collect::>(); + for key in previous_followee_keys { + self.followees_map.remove(&key); + } + let new_followees = followee_neuron_ids.iter().cloned().collect::>(); + for (index, followee_neuron_id) in followee_neuron_ids.into_iter().enumerate() { + self.followees_map.insert( + (follower_neuron_id, (topic, index as u64)), + followee_neuron_id, + ); + } + let followees_to_remove = previous_followees.difference(&new_followees); + for followee_neuron_id in followees_to_remove { + let follower_index_key = ((topic, *followee_neuron_id), follower_neuron_id); + self.followers_index.remove(&follower_index_key); + } + let followees_to_add = new_followees.difference(&previous_followees); + for followee_neuron_id in followees_to_add { + let follower_index_key = ((topic, *followee_neuron_id), follower_neuron_id); + self.followers_index.insert(follower_index_key, ()); + } + } + + fn get_followees(&self, follower_neuron_id: FolloweeNeuronId, topic: Topic) -> Vec { + let min_key = (follower_neuron_id, (topic, u64::MIN)); + let max_key = (follower_neuron_id, (topic, u64::MAX)); + + self.followees_map + .range(min_key..=max_key) + .map(|(_key, followee_neuron_id)| followee_neuron_id) + .collect() + } + + fn get_followers(&self, followee_neuron_id: FolloweeNeuronId, topic: Topic) -> Vec { + let min_key = ((topic, followee_neuron_id), FollowerNeuronId::MIN); + let max_key = ((topic, followee_neuron_id), FollowerNeuronId::MAX); + + self.followers_index + .range(min_key..=max_key) + .map(|(((_topic, _followee_neuron_id), follower_neuron_id), _value)| follower_neuron_id) + .collect() + } +} + +struct HeapNeuronStore { + followees: HashMap>>, + followers: BTreeMap>>, +} + +impl HeapNeuronStore { + fn new() -> Self { + Self { + followees: HashMap::new(), + followers: BTreeMap::new(), + } + } +} + +impl NeuronStore for HeapNeuronStore { + fn set_followees( + &mut self, + follower_neuron_id: FollowerNeuronId, + topic: Topic, + followee_neuron_ids: Vec, + ) { + let old_followees = self + .followees + .get(&follower_neuron_id) + .and_then(|followees| followees.get(&topic).cloned()) + .unwrap_or_default() + .into_iter() + .collect::>(); + + let followees = self.followees.entry(follower_neuron_id).or_default(); + if followee_neuron_ids.is_empty() { + followees.remove(&topic); + } else { + followees.insert(topic, followee_neuron_ids.clone()); + } + + let new_followees = followee_neuron_ids.into_iter().collect::>(); + + let followees_to_remove = old_followees.difference(&new_followees); + for followee_neuron_id in followees_to_remove { + let followers = self + .followers + .get_mut(&topic) + .and_then(|followers| followers.get_mut(followee_neuron_id)); + if let Some(followers) = followers { + followers.remove(&follower_neuron_id); + } + } + + let followees_to_add = new_followees.difference(&old_followees); + for followee_neuron_id in followees_to_add { + let followers = self + .followers + .entry(topic) + .or_default() + .entry(*followee_neuron_id) + .or_default(); + followers.insert(follower_neuron_id); + } + } + + fn get_followees(&self, follower_neuron_id: FolloweeNeuronId, topic: Topic) -> Vec { + self.followees + .get(&follower_neuron_id) + .and_then(|followees| followees.get(&topic)) + .cloned() + .unwrap_or_default() + } + + fn get_followers(&self, followee_neuron_id: FolloweeNeuronId, topic: Topic) -> Vec { + self.followers + .get(&topic) + .and_then(|followers| followers.get(&followee_neuron_id)) + .map(|followers| followers.iter().copied().collect()) + .unwrap_or_default() + } +} + +fn would_follow( + neuron_id: NeuronId, + topic: Topic, + existing_votes: &HashMap, + new_votes: &HashMap, + neuron_store: &NS, +) -> Vote { + let specific_followees = neuron_store.get_followees(neuron_id, topic); + let followees = if specific_followees.is_empty() { + neuron_store.get_followees(neuron_id, 0) + } else { + specific_followees + }; + + if followees.is_empty() { + return Vote::Unspecified; + } + + let mut yes = 0; + let mut no = 0; + + for followee in &followees { + let existing_vote = existing_votes.get(followee).copied(); + match existing_vote { + Some(Vote::Yes) => yes += 1, + Some(Vote::No) => no += 1, + Some(Vote::Unspecified) | None => { + let new_vote = new_votes.get(followee).copied(); + match new_vote { + Some(Vote::Yes) => yes += 1, + Some(Vote::No) => no += 1, + None => {} + Some(Vote::Unspecified) => { + unreachable!("New vote is unspecified"); + } + } + } + } + + if 2 * yes > followees.len() { + return Vote::Yes; + } + if 2 * no >= followees.len() { + return Vote::No; + } + } + Vote::Unspecified +} + +pub fn calculate_cascaded_votes( + existing_votes: HashMap, + voter_neuron_id: NeuronId, + vote_of_neuron: Vote, + topic: Topic, + neuron_store: &NS, +) -> HashMap { + assert!(topic != 0); + + let mut induction_votes = HashMap::new(); + induction_votes.insert(voter_neuron_id, vote_of_neuron); + + let mut new_votes = HashMap::new(); + + loop { + let mut followers = BTreeSet::new(); + + for (neuron_id, vote) in induction_votes.iter() { + assert!(*vote != Vote::Unspecified); + + new_votes.insert(*neuron_id, *vote); + + followers.extend(neuron_store.get_followers(*neuron_id, topic)); + followers.extend(neuron_store.get_followers(*neuron_id, 0)); + } + + induction_votes.clear(); + + followers.retain(|neuron_id| { + let just_voted = new_votes.contains_key(neuron_id); + let previous_vote = existing_votes.get(neuron_id).copied(); + !just_voted && previous_vote == Some(Vote::Unspecified) + }); + + for follower in followers { + let induced_vote = + would_follow(follower, topic, &existing_votes, &new_votes, neuron_store); + if induced_vote != Vote::Unspecified { + induction_votes.insert(follower, induced_vote); + } + } + + if induction_votes.is_empty() { + return new_votes; + } + } +} + +#[cfg(test)] +mod tests; + +mod benches; diff --git a/benchmarks/nns/src/nns_vote_cascading/benches.rs b/benchmarks/nns/src/nns_vote_cascading/benches.rs new file mode 100644 index 00000000..db9545dd --- /dev/null +++ b/benchmarks/nns/src/nns_vote_cascading/benches.rs @@ -0,0 +1,352 @@ +use super::*; + +use canbench_rs::{bench, bench_fn, BenchResult}; +use tiny_rng::{Rand, Rng}; + +enum SetUpStrategy { + // Every neuron follows a single neuron. + Centralized { + num_neurons: u64, + }, + // Following is centralized, but the voter neuron isn't followed by any other neuron. + SingleVote { + num_neurons: u64, + }, + // Neurons follow a chain of other neurons. One end of the chain votes triggering the chain all + // the way to the other end, while every neuron has its allowed followees maximized. This is + // close to the worst case scenario. TODO: an even worse case scenario would be that the + // catch-all topic following are also maximized while contributing nothing to the voting. The + // worse scenario can be improved by just changing the following index though. + Chain { + num_neurons: u64, + num_followees: u64, + }, +} + +fn set_up( + stratygy: SetUpStrategy, + rng: &mut Rng, + neuron_store: &mut NS, + existing_votes: &mut HashMap, +) -> NeuronId { + match stratygy { + SetUpStrategy::Centralized { num_neurons } => { + set_up_centralized(num_neurons, rng, neuron_store, existing_votes) + } + SetUpStrategy::SingleVote { num_neurons } => { + set_up_single_vote(num_neurons, rng, neuron_store, existing_votes) + } + + SetUpStrategy::Chain { + num_neurons, + num_followees, + } => set_up_chain( + num_neurons, + num_followees, + rng, + neuron_store, + existing_votes, + ), + } +} + +fn set_up_centralized( + num_neurons: u64, + rng: &mut Rng, + neuron_store: &mut NS, + existing_votes: &mut HashMap, +) -> NeuronId { + assert!(num_neurons > 1); + + let start_neuron_id = NeuronId::from(rng.rand_u64()); + + neuron_store.set_followees(start_neuron_id, Topic::from(1u8), vec![]); + existing_votes.insert(start_neuron_id, Vote::Unspecified); + + for _ in 1u64..=num_neurons { + let follower_neuron_id = rng.rand_u64(); + neuron_store.set_followees( + FollowerNeuronId::from(follower_neuron_id), + Topic::from(1u8), + vec![start_neuron_id], + ); + existing_votes.insert(NeuronId::from(follower_neuron_id), Vote::Unspecified); + } + + start_neuron_id +} + +fn set_up_single_vote( + num_neurons: u64, + rng: &mut Rng, + neuron_store: &mut NS, + existing_votes: &mut HashMap, +) -> NeuronId { + assert!(num_neurons > 1); + + let start_neuron_id = NeuronId::from(rng.rand_u64()); + let central_neuron_id = NeuronId::from(rng.rand_u64()); + + existing_votes.insert(start_neuron_id, Vote::Unspecified); + existing_votes.insert(central_neuron_id, Vote::Unspecified); + + for _ in 2u64..=num_neurons { + let neuron_id = rng.rand_u64(); + neuron_store.set_followees( + FollowerNeuronId::from(neuron_id), + Topic::from(1u8), + vec![central_neuron_id], + ); + existing_votes.insert(NeuronId::from(neuron_id), Vote::Unspecified); + } + + start_neuron_id +} + +fn set_up_chain( + num_neurons: u64, + num_followees: u64, + rng: &mut Rng, + neuron_store: &mut NS, + existing_votes: &mut HashMap, +) -> NeuronId { + assert!(num_followees % 2 == 1, "Number of followees must be odd"); + assert!( + num_neurons > num_followees, + "Number of neurons must be greater than number of followees" + ); + + let num_half_followees = num_followees / 2; + + let neuron_ids: Vec = (0u64..num_neurons) + .map(|_| NeuronId::from(rng.rand_u64())) + .collect(); + + let not_voting_neuron_ids = (0u64..num_half_followees) + .map(|i| NeuronId::from(neuron_ids[i as usize])) + .collect::>(); + for not_voting_neuron_id in not_voting_neuron_ids.iter() { + existing_votes.insert(*not_voting_neuron_id, Vote::Unspecified); + } + + for voted_neuron_index in num_half_followees..(num_half_followees * 2) { + existing_votes.insert( + NeuronId::from(neuron_ids[voted_neuron_index as usize]), + Vote::Yes, + ); + } + + let start_neuron_id = NeuronId::from(neuron_ids[(num_half_followees * 2) as usize]); + existing_votes.insert(start_neuron_id, Vote::Unspecified); + + for neuron_index in num_followees..num_neurons { + let neuron_id = neuron_ids[neuron_index as usize]; + let previous_neuron_indices = (neuron_index - num_half_followees - 1)..neuron_index; + let followee_neuron_ids = previous_neuron_indices + .map(|index| NeuronId::from(neuron_ids[index as usize])) + .chain(not_voting_neuron_ids.clone().into_iter()) + .collect::>(); + neuron_store.set_followees( + FollowerNeuronId::from(neuron_id), + Topic::from(1u8), + followee_neuron_ids, + ); + existing_votes.insert(NeuronId::from(neuron_id), Vote::Unspecified); + } + + start_neuron_id +} + +fn bench_helper(neuron_store: &mut impl NeuronStore, stratygy: SetUpStrategy) -> BenchResult { + let mut existing_votes = HashMap::new(); + let mut rng = Rng::from_seed(0u64); + + let neuron_id = set_up(stratygy, &mut rng, neuron_store, &mut existing_votes); + + bench_fn(|| { + let cascaded_votes = calculate_cascaded_votes( + existing_votes, + neuron_id, + Vote::Yes, + Topic::from(1u8), + neuron_store, + ); + println!("Number of cascaded votes: {}", cascaded_votes.len()); + }) +} + +#[bench(raw)] +fn vote_cascading_stable_centralized_1k() -> BenchResult { + let mut neuron_store = StableNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Centralized { num_neurons: 1_000 }, + ) +} + +#[bench(raw)] +fn vote_cascading_stable_centralized_10k() -> BenchResult { + let mut neuron_store = StableNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Centralized { + num_neurons: 10_000, + }, + ) +} + +#[bench(raw)] +fn vote_cascading_stable_single_vote_1k() -> BenchResult { + let mut neuron_store = StableNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::SingleVote { num_neurons: 1_000 }, + ) +} + +#[bench(raw)] +fn vote_cascading_stable_single_vote_10k() -> BenchResult { + let mut neuron_store = StableNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::SingleVote { + num_neurons: 10_000, + }, + ) +} + +#[bench(raw)] +fn vote_cascading_stable_chain_1k_15() -> BenchResult { + let mut neuron_store = StableNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Chain { + num_neurons: 1_000, + num_followees: 15, + }, + ) +} + +#[bench(raw)] +fn vote_cascading_stable_chain_10k_15() -> BenchResult { + let mut neuron_store = StableNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Chain { + num_neurons: 10_000, + num_followees: 15, + }, + ) +} + +#[bench(raw)] +fn vote_cascading_stable_chain_1k_5() -> BenchResult { + let mut neuron_store = StableNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Chain { + num_neurons: 1_000, + num_followees: 5, + }, + ) +} + +#[bench(raw)] +fn vote_cascading_stable_chain_10k_5() -> BenchResult { + let mut neuron_store = StableNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Chain { + num_neurons: 10_000, + num_followees: 5, + }, + ) +} + +#[bench(raw)] +fn vote_cascading_heap_centralized_1k() -> BenchResult { + let mut neuron_store = HeapNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Centralized { num_neurons: 1_000 }, + ) +} + +#[bench(raw)] +fn vote_cascading_heap_centralized_10k() -> BenchResult { + let mut neuron_store = HeapNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Centralized { + num_neurons: 10_000, + }, + ) +} + +#[bench(raw)] +fn vote_cascading_heap_single_vote_1k() -> BenchResult { + let mut neuron_store = HeapNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::SingleVote { num_neurons: 1_000 }, + ) +} + +#[bench(raw)] +fn vote_cascading_heap_single_vote_10k() -> BenchResult { + let mut neuron_store = HeapNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::SingleVote { + num_neurons: 10_000, + }, + ) +} + +#[bench(raw)] +fn vote_cascading_heap_chain_1k_15() -> BenchResult { + let mut neuron_store = HeapNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Chain { + num_neurons: 1_000, + num_followees: 15, + }, + ) +} + +#[bench(raw)] +fn vote_cascading_heap_chain_10k_15() -> BenchResult { + let mut neuron_store = HeapNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Chain { + num_neurons: 10_000, + num_followees: 15, + }, + ) +} + +#[bench(raw)] +fn vote_cascading_heap_chain_1k_5() -> BenchResult { + let mut neuron_store = HeapNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Chain { + num_neurons: 1_000, + num_followees: 5, + }, + ) +} + +#[bench(raw)] +fn vote_cascading_heap_chain_10k_5() -> BenchResult { + let mut neuron_store = HeapNeuronStore::new(); + bench_helper( + &mut neuron_store, + SetUpStrategy::Chain { + num_neurons: 10_000, + num_followees: 5, + }, + ) +} diff --git a/benchmarks/nns/src/nns_vote_cascading/tests.rs b/benchmarks/nns/src/nns_vote_cascading/tests.rs new file mode 100644 index 00000000..a6b7fd2f --- /dev/null +++ b/benchmarks/nns/src/nns_vote_cascading/tests.rs @@ -0,0 +1,210 @@ +use super::*; + +use maplit::hashmap; + +fn combine_votes( + existing_votes: HashMap, + new_votes: HashMap, +) -> HashMap { + let mut combined_votes = existing_votes.clone(); + for (neuron_id, vote) in new_votes { + combined_votes.insert(neuron_id, vote); + } + combined_votes +} + +fn test_two_neurons_specific_topic(neuron_store: &mut impl NeuronStore) { + let existing_votes = hashmap! { + 1 => Vote::Unspecified, + 2 => Vote::Unspecified, + }; + neuron_store.set_followees(2, Topic::from(1u8), vec![1]); + + let cascaded_votes = calculate_cascaded_votes( + existing_votes, + NeuronId::from(1u64), + Vote::Yes, + Topic::from(1u8), + neuron_store, + ); + + assert_eq!( + cascaded_votes, + hashmap! { + 1 => Vote::Yes, + 2 => Vote::Yes, + } + ); +} + +#[test] +fn test_two_neurons_specific_topic_stable() { + let mut neuron_store = StableNeuronStore::new(); + test_two_neurons_specific_topic(&mut neuron_store); +} + +#[test] +fn test_two_neurons_specific_topic_heap() { + let mut neuron_store = HeapNeuronStore::new(); + test_two_neurons_specific_topic(&mut neuron_store); +} + +fn test_two_neurons_catch_all_topic(neuron_store: &mut impl NeuronStore) { + let existing_votes = hashmap! { + 1 => Vote::Unspecified, + 2 => Vote::Unspecified, + }; + neuron_store.set_followees(2, Topic::from(0u8), vec![1]); + + let cascaded_votes = calculate_cascaded_votes( + existing_votes, + NeuronId::from(1u64), + Vote::Yes, + Topic::from(1u8), + neuron_store, + ); + + assert_eq!( + cascaded_votes, + hashmap! { + 1 => Vote::Yes, + 2 => Vote::Yes, + } + ); +} + +#[test] +fn test_two_neurons_catch_all_topic_stable() { + let mut neuron_store = StableNeuronStore::new(); + test_two_neurons_catch_all_topic(&mut neuron_store); +} + +#[test] +fn test_two_neurons_catch_all_topic_heap() { + let mut neuron_store = HeapNeuronStore::new(); + test_two_neurons_catch_all_topic(&mut neuron_store); +} + +fn test_specific_override_catch_all(neuron_store: &mut impl NeuronStore) { + let existing_votes = hashmap! { + 1 => Vote::Unspecified, + 2 => Vote::Unspecified, + 3 => Vote::Unspecified, + }; + neuron_store.set_followees(3, Topic::from(0u8), vec![1]); + neuron_store.set_followees(3, Topic::from(1u8), vec![2]); + + let cascaded_votes = calculate_cascaded_votes( + existing_votes.clone(), + NeuronId::from(1u64), + Vote::Yes, + Topic::from(1u8), + neuron_store, + ); + assert_eq!( + cascaded_votes, + hashmap! { + 1 => Vote::Yes, + } + ); + + let existing_votes = combine_votes(existing_votes, cascaded_votes); + let cascaded_votes = calculate_cascaded_votes( + existing_votes, + NeuronId::from(2u64), + Vote::Yes, + Topic::from(1u8), + neuron_store, + ); + assert_eq!( + cascaded_votes, + hashmap! { + 2 => Vote::Yes, + 3 => Vote::Yes, + } + ); +} + +#[test] +fn test_specific_override_catch_all_stable() { + let mut neuron_store = StableNeuronStore::new(); + test_specific_override_catch_all(&mut neuron_store); +} + +#[test] +fn test_specific_override_catch_all_heap() { + let mut neuron_store = HeapNeuronStore::new(); + test_specific_override_catch_all(&mut neuron_store); +} + +fn set_up_worst_case( + num_neurons: u64, + num_followees: u64, + neuron_store: &mut NS, + existing_votes: &mut HashMap, +) -> NeuronId { + assert!(num_followees % 2 == 1, "Number of followees must be odd"); + assert!( + num_neurons > num_followees, + "Number of neurons must be greater than number of followees" + ); + + let num_half_followees = num_followees / 2; + + let not_voting_neuron_ids = (0u64..num_half_followees) + .map(NeuronId::from) + .collect::>(); + for not_voting_neuron_id in not_voting_neuron_ids.iter() { + existing_votes.insert(*not_voting_neuron_id, Vote::Unspecified); + } + + for voted_neuron_id in num_half_followees..(num_half_followees * 2) { + existing_votes.insert(NeuronId::from(voted_neuron_id), Vote::Yes); + } + + let start_neuron_id = NeuronId::from(num_half_followees * 2); + existing_votes.insert(start_neuron_id, Vote::Unspecified); + + for neuron_id in num_followees..num_neurons { + let previous_neuron_ids = (neuron_id - num_half_followees - 1)..neuron_id; + let followee_neuron_ids = previous_neuron_ids + .map(|id| NeuronId::from(id)) + .chain(not_voting_neuron_ids.clone().into_iter()) + .collect::>(); + neuron_store.set_followees( + FollowerNeuronId::from(neuron_id), + Topic::from(1u8), + followee_neuron_ids, + ); + existing_votes.insert(NeuronId::from(neuron_id), Vote::Unspecified); + } + + start_neuron_id +} + +fn test_chain(neuron_store: &mut impl NeuronStore) { + let mut existing_votes = HashMap::new(); + let start_neuron_id = set_up_worst_case(100, 15, neuron_store, &mut existing_votes); + + let cascaded_votes = calculate_cascaded_votes( + existing_votes, + start_neuron_id, + Vote::Yes, + Topic::from(1u8), + neuron_store, + ); + + assert_eq!(cascaded_votes.len(), 86); +} + +#[test] +fn test_chain_stable() { + let mut neuron_store = StableNeuronStore::new(); + test_chain(&mut neuron_store); +} + +#[test] +fn test_chain_heap() { + let mut neuron_store = HeapNeuronStore::new(); + test_chain(&mut neuron_store); +}