Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 26 additions & 23 deletions ledger/src/leader_schedule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub trait LeaderScheduleVariant:
std::fmt::Debug + Send + Sync + Index<u64, Output = Pubkey>
{
fn get_slot_leaders(&self) -> &[Pubkey];
fn get_leader_slots_map(&self) -> &HashMap<Pubkey, Arc<Vec<usize>>>;
fn get_leader_slots_map(&self) -> &HashMap<Pubkey, Vec<usize>>;

/// Get the vote account address for the given epoch slot index. This is
/// guaranteed to be Some if the leader schedule is keyed by vote account
Expand All @@ -38,29 +38,32 @@ pub trait LeaderScheduleVariant:
&self,
pubkey: &Pubkey,
offset: usize, // Starting index.
) -> Box<dyn Iterator<Item = usize>> {
let index = self
.get_leader_slots_map()
.get(pubkey)
.cloned()
.unwrap_or_default();
) -> Box<dyn Iterator<Item = usize> + '_> {
let index = self.get_leader_slots_map().get(pubkey);
let num_slots = self.num_slots();
let size = index.len();
#[allow(clippy::reversed_empty_ranges)]
let range = if index.is_empty() {
1..=0 // Intentionally empty range of type RangeInclusive.
} else {
let offset = index
.binary_search(&(offset % num_slots))
.unwrap_or_else(identity)
+ offset / num_slots * size;
offset..=usize::MAX
};
// The modular arithmetic here and above replicate Index implementation
// for LeaderSchedule, where the schedule keeps repeating endlessly.
// The '%' returns where in a cycle we are and the '/' returns how many
// times the schedule is repeated.
Box::new(range.map(move |k| index[k % size] + k / size * num_slots))

match index {
Some(index) if !index.is_empty() => {
let size = index.len();
let start_offset = index
.binary_search(&(offset % num_slots))
.unwrap_or_else(identity)
+ offset / num_slots * size;
// The modular arithmetic here and above replicate Index implementation
// for LeaderSchedule, where the schedule keeps repeating endlessly.
// The '%' returns where in a cycle we are and the '/' returns how many
// times the schedule is repeated.
Box::new(
(start_offset..=usize::MAX)
.map(move |k| index[k % size] + k / size * num_slots),
)
}
_ => {
// Empty iterator for pubkeys not in schedule
#[allow(clippy::reversed_empty_ranges)]
Box::new((1..=0).map(|_| 0))
}
}
}

fn num_slots(&self) -> usize {
Expand Down
11 changes: 4 additions & 7 deletions ledger/src/leader_schedule/identity_keyed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ use {
itertools::Itertools,
solana_clock::Epoch,
solana_pubkey::Pubkey,
std::{collections::HashMap, ops::Index, sync::Arc},
std::{collections::HashMap, ops::Index},
};

#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct LeaderSchedule {
slot_leaders: Vec<Pubkey>,
// Inverted index from pubkeys to indices where they are the leader.
leader_slots_map: HashMap<Pubkey, Arc<Vec<usize>>>,
leader_slots_map: HashMap<Pubkey, Vec<usize>>,
}

impl LeaderSchedule {
Expand All @@ -36,15 +36,12 @@ impl LeaderSchedule {
}
}

fn invert_slot_leaders(slot_leaders: &[Pubkey]) -> HashMap<Pubkey, Arc<Vec<usize>>> {
fn invert_slot_leaders(slot_leaders: &[Pubkey]) -> HashMap<Pubkey, Vec<usize>> {
slot_leaders
.iter()
.enumerate()
.map(|(i, pk)| (*pk, i))
.into_group_map()
.into_iter()
.map(|(k, v)| (k, Arc::new(v)))
.collect()
}

pub fn get_slot_leaders(&self) -> &[Pubkey] {
Expand All @@ -57,7 +54,7 @@ impl LeaderScheduleVariant for LeaderSchedule {
&self.slot_leaders
}

fn get_leader_slots_map(&self) -> &HashMap<Pubkey, Arc<Vec<usize>>> {
fn get_leader_slots_map(&self) -> &HashMap<Pubkey, Vec<usize>> {
&self.leader_slots_map
}
}
Expand Down
4 changes: 2 additions & 2 deletions ledger/src/leader_schedule/vote_keyed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use {
solana_clock::Epoch,
solana_pubkey::Pubkey,
solana_vote::vote_account::VoteAccountsHashMap,
std::{collections::HashMap, ops::Index, sync::Arc},
std::{collections::HashMap, ops::Index},
};

#[derive(Debug, PartialEq, Eq, Clone)]
Expand Down Expand Up @@ -80,7 +80,7 @@ impl LeaderScheduleVariant for LeaderSchedule {
self.identity_keyed_leader_schedule.get_slot_leaders()
}

fn get_leader_slots_map(&self) -> &HashMap<Pubkey, Arc<Vec<usize>>> {
fn get_leader_slots_map(&self) -> &HashMap<Pubkey, Vec<usize>> {
self.identity_keyed_leader_schedule.get_leader_slots_map()
}

Expand Down
15 changes: 10 additions & 5 deletions ledger/src/leader_schedule_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,20 @@ impl LeaderScheduleCache {
);
return None;
}
// Slots after current_slot where pubkey is the leader.
let mut schedule = (epoch..=max_epoch)
// Collect leader schedules first so they stay alive for the iterator chain
let schedules: Vec<_> = (epoch..=max_epoch)
.map(|epoch| self.get_epoch_schedule_else_compute(epoch, bank))
.while_some()
.zip(epoch..)
.collect();
Copy link
Member

@vadorovsky vadorovsky Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you have a chance to measure how much does this collect take? This change looks like a trade-off - slower next_leader_slot (the question is - how much) for faster epoch boundary (~5ms).

I can profile it, but I'm AFK now, so I could do it tomorrow.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be small. There is going to be at most two Arc of leader schedules.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it's like 0.1ms per each replay.

next_leader_slot


// Slots after current_slot where pubkey is the leader.
let mut schedule = schedules
.iter()
.flat_map(|(leader_schedule, k)| {
let offset = if k == epoch { start_index as usize } else { 0 };
let num_slots = bank.get_slots_in_epoch(k) as usize;
let first_slot = bank.epoch_schedule().get_first_slot_in_epoch(k);
let offset = if *k == epoch { start_index as usize } else { 0 };
let num_slots = bank.get_slots_in_epoch(*k) as usize;
let first_slot = bank.epoch_schedule().get_first_slot_in_epoch(*k);
leader_schedule
.get_leader_upcoming_slots(pubkey, offset)
.take_while(move |i| *i < num_slots)
Expand Down
Loading