Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(nns): Improve performance of stable neuron recent ballots recording #2697

Merged
merged 11 commits into from
Nov 25, 2024
32 changes: 16 additions & 16 deletions rs/nns/governance/canbench/canbench_results.yml
Original file line number Diff line number Diff line change
@@ -1,91 +1,91 @@
benches:
add_neuron_active_maximum:
total:
instructions: 35887686
instructions: 36149096
heap_increase: 1
stable_memory_increase: 0
scopes: {}
add_neuron_active_typical:
total:
instructions: 1833590
instructions: 1834158
heap_increase: 0
stable_memory_increase: 0
scopes: {}
add_neuron_inactive_maximum:
total:
instructions: 98494555
instructions: 96285952
heap_increase: 1
stable_memory_increase: 0
scopes: {}
add_neuron_inactive_typical:
total:
instructions: 7593880
instructions: 7381170
heap_increase: 0
stable_memory_increase: 0
scopes: {}
cascading_vote_all_heap:
total:
instructions: 32187827
instructions: 31887311
heap_increase: 0
stable_memory_increase: 0
scopes: {}
cascading_vote_heap_neurons_stable_index:
total:
instructions: 54527506
instructions: 54233977
heap_increase: 0
stable_memory_increase: 0
scopes: {}
cascading_vote_stable_everything:
total:
instructions: 1978888007
instructions: 160313292
heap_increase: 0
stable_memory_increase: 0
scopes: {}
cascading_vote_stable_neurons_with_heap_index:
total:
instructions: 1956237656
instructions: 138075846
heap_increase: 0
stable_memory_increase: 0
scopes: {}
centralized_following_all_stable:
total:
instructions: 2057421983
instructions: 66264382
heap_increase: 0
stable_memory_increase: 0
scopes: {}
compute_ballots_for_new_proposal_with_stable_neurons:
total:
instructions: 1685116
instructions: 1690958
heap_increase: 0
stable_memory_increase: 0
scopes: {}
neuron_metrics_calculation_heap:
total:
instructions: 840837
instructions: 837546
heap_increase: 0
stable_memory_increase: 0
scopes: {}
neuron_metrics_calculation_stable:
total:
instructions: 1834053
instructions: 1843015
heap_increase: 0
stable_memory_increase: 0
scopes: {}
range_neurons_performance:
total:
instructions: 48615662
instructions: 47360359
heap_increase: 0
stable_memory_increase: 0
scopes: {}
single_vote_all_stable:
total:
instructions: 13224969
instructions: 364240
heap_increase: 0
stable_memory_increase: 0
scopes: {}
update_recent_ballots:
update_recent_ballots_stable_memory:
total:
instructions: 16275553
instructions: 236573
heap_increase: 0
stable_memory_increase: 0
scopes: {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ message Neuron {
// refreshed, this will be set to 2024-11-05T00:00:01 UTC (1730764801 seconds
// after the UNIX epoch).
optional uint64 voting_power_refreshed_timestamp_seconds = 24;

// The index of the next entry in the recent_ballots list that will be
// used for the circular buffer. This is used to determine which entry
// to overwrite next.
optional uint32 recent_ballots_next_entry_index = 25;
}

// Subset of Neuron that has no collections or big fields that might not exist in most neurons, and
Expand All @@ -484,6 +489,7 @@ message AbridgedNeuron {
optional NeuronType neuron_type = 22;
optional Visibility visibility = 23;
optional uint64 voting_power_refreshed_timestamp_seconds = 24;
optional uint32 recent_ballots_next_entry_index = 25;

reserved 1;
reserved "id";
Expand Down
7 changes: 7 additions & 0 deletions rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@ pub struct Neuron {
/// after the UNIX epoch).
#[prost(uint64, optional, tag = "24")]
pub voting_power_refreshed_timestamp_seconds: ::core::option::Option<u64>,
/// The index of the next entry in the recent_ballots list that will be
/// used for the circular buffer. This is used to determine which entry
/// to overwrite next.
#[prost(uint32, optional, tag = "25")]
pub recent_ballots_next_entry_index: ::core::option::Option<u32>,
/// At any time, at most one of `when_dissolved` and
/// `dissolve_delay` are specified.
///
Expand Down Expand Up @@ -433,6 +438,8 @@ pub struct AbridgedNeuron {
pub visibility: ::core::option::Option<i32>,
#[prost(uint64, optional, tag = "24")]
pub voting_power_refreshed_timestamp_seconds: ::core::option::Option<u64>,
#[prost(uint32, optional, tag = "25")]
pub recent_ballots_next_entry_index: ::core::option::Option<u32>,
#[prost(oneof = "abridged_neuron::DissolveState", tags = "9, 10")]
pub dissolve_state: ::core::option::Option<abridged_neuron::DissolveState>,
}
Expand Down
8 changes: 7 additions & 1 deletion rs/nns/governance/src/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2313,7 +2313,13 @@ impl Governance {
&& neuron.visibility() == Some(Visibility::Public)
);
if let_caller_read_full_neuron {
full_neurons.push(NeuronProto::from(neuron.clone()));
let mut proto = NeuronProto::from(neuron.clone());
// We get the recent_ballots from the neuron itself, because
// we are using a circular buffer to store them. This solution is not ideal, but
// we need to do a larger refactoring to use the correct API types instead of the internal
// governance proto at this level.
proto.recent_ballots = neuron.recent_ballots();
max-dfinity marked this conversation as resolved.
Show resolved Hide resolved
full_neurons.push(proto);
}
});
}
Expand Down
64 changes: 53 additions & 11 deletions rs/nns/governance/src/neuron/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ pub struct Neuron {
/// month. After that, following is cleared, except for ManageNeuron
/// proposals.
voting_power_refreshed_timestamp_seconds: u64,
/// This field is used to store the index of the most recent ballot in the
max-dfinity marked this conversation as resolved.
Show resolved Hide resolved
/// `recent_ballots` circular buffer. This is used to optimize insertions
/// into stable memory, to avoid rewriting all the data.
pub recent_ballots_next_entry_index: Option<usize>,
}

/// This is mostly the same as the version of PartialEq generated by derive. The
Expand Down Expand Up @@ -160,6 +164,7 @@ impl PartialEq for Neuron {
voting_power_refreshed_timestamp_seconds: &'a u64,

visibility: Visibility,
recent_ballots_next_entry_index: Option<usize>,
}

impl<'a> Normalized<'a> {
Expand Down Expand Up @@ -188,6 +193,7 @@ impl PartialEq for Neuron {
voting_power_refreshed_timestamp_seconds,

visibility: _,
recent_ballots_next_entry_index,
} = src;

let visibility = src.visibility().unwrap_or(Visibility::Private);
Expand Down Expand Up @@ -216,6 +222,7 @@ impl PartialEq for Neuron {
voting_power_refreshed_timestamp_seconds,

visibility,
recent_ballots_next_entry_index: *recent_ballots_next_entry_index,
}
}
}
Expand Down Expand Up @@ -429,6 +436,21 @@ impl Neuron {
std::cmp::min(ad_stake, u64::MAX as u128) as u64
}

pub(crate) fn recent_ballots(&self) -> Vec<BallotInfo> {
if self.recent_ballots_next_entry_index.is_none() {
self.recent_ballots.clone()
} else {
let recent_ballots = &self.recent_ballots;
let index = self.recent_ballots_next_entry_index.unwrap();
max-dfinity marked this conversation as resolved.
Show resolved Hide resolved
max-dfinity marked this conversation as resolved.
Show resolved Hide resolved
recent_ballots[index..]
.iter()
.chain(recent_ballots[..index].iter())
.rev()
max-dfinity marked this conversation as resolved.
Show resolved Hide resolved
.cloned()
.collect()
}
}

/// Given the specified `ballots`: determine how this neuron would
/// vote on a proposal of `topic` based on which neurons this
/// neuron follows on this topic (or on the default topic if this
Expand Down Expand Up @@ -489,6 +511,7 @@ impl Neuron {
/// Register that this neuron has cast a ballot for a
/// proposal. Don't include votes on "real time" topics (such as
/// setting the ICP/SDR exchange rate).
/// TODO(NNS1-3479) delete this method after all neurons are migrated to stable memory
pub(crate) fn register_recent_ballot(
&mut self,
topic: Topic,
Expand All @@ -500,20 +523,28 @@ impl Neuron {
if topic == Topic::ExchangeRate {
return;
}

max-dfinity marked this conversation as resolved.
Show resolved Hide resolved
// Data migration for updating the recent ballots so we can use a circular buffer here.
if self.recent_ballots_next_entry_index.is_none() {
self.recent_ballots.reverse();
self.recent_ballots_next_entry_index =
Some(self.recent_ballots.len() % MAX_NEURON_RECENT_BALLOTS);
}

let ballot_info = BallotInfo {
proposal_id: Some(*proposal_id),
vote: vote as i32,
};
// We would really like to have a circular buffer here. As
// we're dealing with a simple vector, we insert at the
// beginning and remove at the end once we have reached
// the maximum number of votes to keep track of.
self.recent_ballots.insert(0, ballot_info);
// Pop and discard elements from the end until we reach
// the maximum allowed length of the vector.
while self.recent_ballots.len() > MAX_NEURON_RECENT_BALLOTS {
self.recent_ballots.pop();

// Vector is full
if self.recent_ballots.len() >= MAX_NEURON_RECENT_BALLOTS {
self.recent_ballots[self.recent_ballots_next_entry_index.unwrap()] = ballot_info;
max-dfinity marked this conversation as resolved.
Show resolved Hide resolved
} else {
self.recent_ballots.push(ballot_info);
}
// Advance the index
self.recent_ballots_next_entry_index =
Some((self.recent_ballots_next_entry_index.unwrap() + 1) % MAX_NEURON_RECENT_BALLOTS);
}

pub(crate) fn refresh_voting_power(&mut self, now_seconds: u64) {
Expand Down Expand Up @@ -875,7 +906,7 @@ impl Neuron {
|| self.visibility() == Some(Visibility::Public)
|| self.is_hotkey_or_controller(&requester);
if show_full {
recent_ballots.append(&mut self.recent_ballots.clone());
recent_ballots.append(&mut self.recent_ballots());
joined_community_fund_timestamp_seconds = self.joined_community_fund_timestamp_seconds;
}

Expand Down Expand Up @@ -1098,7 +1129,6 @@ impl Neuron {
impl From<Neuron> for NeuronProto {
fn from(neuron: Neuron) -> Self {
let visibility = neuron.visibility().map(|visibility| visibility as i32);

let Neuron {
id,
subaccount,
Expand All @@ -1122,6 +1152,7 @@ impl From<Neuron> for NeuronProto {
neuron_type,
visibility: _,
voting_power_refreshed_timestamp_seconds,
recent_ballots_next_entry_index,
} = neuron;

let id = Some(id);
Expand Down Expand Up @@ -1158,6 +1189,7 @@ impl From<Neuron> for NeuronProto {
neuron_type,
visibility,
voting_power_refreshed_timestamp_seconds,
recent_ballots_next_entry_index: recent_ballots_next_entry_index.map(|x| x as u32),
}
}
}
Expand Down Expand Up @@ -1190,6 +1222,7 @@ impl TryFrom<NeuronProto> for Neuron {
neuron_type,
visibility,
voting_power_refreshed_timestamp_seconds,
recent_ballots_next_entry_index,
} = proto;

let id = id.ok_or("Neuron ID is missing")?;
Expand Down Expand Up @@ -1235,6 +1268,7 @@ impl TryFrom<NeuronProto> for Neuron {
neuron_type,
visibility,
voting_power_refreshed_timestamp_seconds,
recent_ballots_next_entry_index: recent_ballots_next_entry_index.map(|x| x as usize),
})
}
}
Expand Down Expand Up @@ -1328,6 +1362,7 @@ impl TryFrom<Neuron> for DecomposedNeuron {
neuron_type,
visibility,
voting_power_refreshed_timestamp_seconds,
recent_ballots_next_entry_index,
} = source;

let account = subaccount.to_vec();
Expand Down Expand Up @@ -1359,6 +1394,7 @@ impl TryFrom<Neuron> for DecomposedNeuron {
dissolve_state,
visibility,
voting_power_refreshed_timestamp_seconds,
recent_ballots_next_entry_index: recent_ballots_next_entry_index.map(|x| x as u32),
max-dfinity marked this conversation as resolved.
Show resolved Hide resolved
};

Ok(Self {
Expand Down Expand Up @@ -1409,6 +1445,7 @@ impl From<DecomposedNeuron> for Neuron {
dissolve_state,
visibility,
voting_power_refreshed_timestamp_seconds,
recent_ballots_next_entry_index,
} = main;

let subaccount =
Expand Down Expand Up @@ -1447,6 +1484,7 @@ impl From<DecomposedNeuron> for Neuron {
neuron_type,
visibility,
voting_power_refreshed_timestamp_seconds,
recent_ballots_next_entry_index: recent_ballots_next_entry_index.map(|x| x as usize),
max-dfinity marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down Expand Up @@ -1693,6 +1731,9 @@ impl NeuronBuilder {
#[cfg(not(test))]
let known_neuron_data = None;

let recent_ballots_next_entry_index =
Some(recent_ballots.len() % MAX_NEURON_RECENT_BALLOTS);
max-dfinity marked this conversation as resolved.
Show resolved Hide resolved

Neuron {
id,
subaccount,
Expand All @@ -1716,6 +1757,7 @@ impl NeuronBuilder {
neuron_type,
visibility,
voting_power_refreshed_timestamp_seconds,
recent_ballots_next_entry_index,
}
}
}
Expand Down
Loading