Skip to content

Commit bcaf401

Browse files
committed
Use the forwards iterator more often (#2376)
## Issue Addressed NA ## Primary Change When investigating memory usage, I noticed that retrieving a block from an early slot (e.g., slot 900) would cause a sharp increase in the memory footprint (from 400mb to 800mb+) which seemed to be ever-lasting. After some investigation, I found that the reverse iteration from the head back to that slot was the likely culprit. To counter this, I've switched the `BeaconChain::block_root_at_slot` to use the forwards iterator, instead of the reverse one. I also noticed that the networking stack is using `BeaconChain::root_at_slot` to check if a peer is relevant (`check_peer_relevance`). Perhaps the steep, seemingly-random-but-consistent increases in memory usage are caused by the use of this function. Using the forwards iterator with the HTTP API alleviated the sharp increases in memory usage. It also made the response much faster (before it felt like to took 1-2s, now it feels instant). ## Additional Changes In the process I also noticed that we have two functions for getting block roots: - `BeaconChain::block_root_at_slot`: returns `None` for a skip slot. - `BeaconChain::root_at_slot`: returns the previous root for a skip slot. I unified these two functions into `block_root_at_slot` and added the `WhenSlotSkipped` enum. Now, the caller must be explicit about the skip-slot behaviour when requesting a root. Additionally, I replaced `vec![]` with `Vec::with_capacity` in `store::chunked_vector::range_query`. I stumbled across this whilst debugging and made this modification to see what effect it would have (not much). It seems like a decent change to keep around, but I'm not concerned either way. Also, `BeaconChain::get_ancestor_block_root` is unused, so I got rid of it :wastebasket:. ## Additional Info I haven't also done the same for state roots here. Whilst it's possible and a good idea, it's more work since the fwds iterators are presently block-roots-specific. Whilst there's a few places a reverse iteration of state roots could be triggered (e.g., attestation production, HTTP API), they're no where near as common as the `check_peer_relevance` call. As such, I think we should get this PR merged first, then come back for the state root iters. I made an issue here #2377.
1 parent 263f46b commit bcaf401

File tree

12 files changed

+339
-65
lines changed

12 files changed

+339
-65
lines changed

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 142 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use eth2::types::{EventKind, SseBlock, SseFinalizedCheckpoint, SseHead};
3636
use fork_choice::ForkChoice;
3737
use futures::channel::mpsc::Sender;
3838
use itertools::process_results;
39+
use itertools::Itertools;
3940
use operation_pool::{OperationPool, PersistedOperationPool};
4041
use parking_lot::{Mutex, RwLock};
4142
use slasher::Slasher;
@@ -85,6 +86,18 @@ pub const OP_POOL_DB_KEY: Hash256 = Hash256::zero();
8586
pub const ETH1_CACHE_DB_KEY: Hash256 = Hash256::zero();
8687
pub const FORK_CHOICE_DB_KEY: Hash256 = Hash256::zero();
8788

89+
/// Defines the behaviour when a block/block-root for a skipped slot is requested.
90+
pub enum WhenSlotSkipped {
91+
/// If the slot is a skip slot, return `None`.
92+
///
93+
/// This is how the HTTP API behaves.
94+
None,
95+
/// If the slot it a skip slot, return the previous non-skipped block.
96+
///
97+
/// This is generally how the specification behaves.
98+
Prev,
99+
}
100+
88101
/// The result of a chain segment processing.
89102
pub enum ChainSegmentResult<T: EthSpec> {
90103
/// Processing this chain segment finished successfully.
@@ -442,18 +455,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
442455
.map(|result| result.map_err(|e| e.into())))
443456
}
444457

445-
/// Traverse backwards from `block_root` to find the root of the ancestor block at `slot`.
446-
pub fn get_ancestor_block_root(
447-
&self,
448-
block_root: Hash256,
449-
slot: Slot,
450-
) -> Result<Option<Hash256>, Error> {
451-
process_results(self.rev_iter_block_roots_from(block_root)?, |mut iter| {
452-
iter.find(|(_, ancestor_slot)| *ancestor_slot == slot)
453-
.map(|(ancestor_block_root, _)| ancestor_block_root)
454-
})
455-
}
456-
457458
/// Iterates across all `(state_root, slot)` pairs from the head of the chain (inclusive) to
458459
/// the earliest reachable ancestor (may or may not be genesis).
459460
///
@@ -489,17 +490,17 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
489490

490491
/// Returns the block at the given slot, if any. Only returns blocks in the canonical chain.
491492
///
493+
/// Use the `skips` parameter to define the behaviour when `request_slot` is a skipped slot.
494+
///
492495
/// ## Errors
493496
///
494497
/// May return a database error.
495498
pub fn block_at_slot(
496499
&self,
497-
slot: Slot,
500+
request_slot: Slot,
501+
skips: WhenSlotSkipped,
498502
) -> Result<Option<SignedBeaconBlock<T::EthSpec>>, Error> {
499-
let root = process_results(self.rev_iter_block_roots()?, |mut iter| {
500-
iter.find(|(_, this_slot)| *this_slot == slot)
501-
.map(|(root, _)| root)
502-
})?;
503+
let root = self.block_root_at_slot(request_slot, skips)?;
503504

504505
if let Some(block_root) = root {
505506
Ok(self.store.get_item(&block_root)?)
@@ -521,21 +522,132 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
521522
}
522523

523524
/// Returns the block root at the given slot, if any. Only returns roots in the canonical chain.
524-
/// Returns `Ok(None)` if the given `Slot` was skipped.
525+
///
526+
/// ## Notes
527+
///
528+
/// - Use the `skips` parameter to define the behaviour when `request_slot` is a skipped slot.
529+
/// - Returns `Ok(None)` for any slot higher than the current wall-clock slot.
530+
pub fn block_root_at_slot(
531+
&self,
532+
request_slot: Slot,
533+
skips: WhenSlotSkipped,
534+
) -> Result<Option<Hash256>, Error> {
535+
match skips {
536+
WhenSlotSkipped::None => self.block_root_at_slot_skips_none(request_slot),
537+
WhenSlotSkipped::Prev => self.block_root_at_slot_skips_prev(request_slot),
538+
}
539+
}
540+
541+
/// Returns the block root at the given slot, if any. Only returns roots in the canonical chain.
542+
///
543+
/// ## Notes
544+
///
545+
/// - Returns `Ok(None)` if the given `Slot` was skipped.
546+
/// - Returns `Ok(None)` for any slot higher than the current wall-clock slot.
525547
///
526548
/// ## Errors
527549
///
528550
/// May return a database error.
529-
pub fn block_root_at_slot(&self, slot: Slot) -> Result<Option<Hash256>, Error> {
530-
process_results(self.rev_iter_block_roots()?, |mut iter| {
531-
let root_opt = iter
532-
.find(|(_, this_slot)| *this_slot == slot)
533-
.map(|(root, _)| root);
534-
if let (Some(root), Some((prev_root, _))) = (root_opt, iter.next()) {
535-
return (prev_root != root).then(|| root);
551+
fn block_root_at_slot_skips_none(&self, request_slot: Slot) -> Result<Option<Hash256>, Error> {
552+
if request_slot > self.slot()? {
553+
return Ok(None);
554+
} else if request_slot == self.spec.genesis_slot {
555+
return Ok(Some(self.genesis_block_root));
556+
}
557+
558+
let prev_slot = request_slot.saturating_sub(1_u64);
559+
560+
// Try an optimized path of reading the root directly from the head state.
561+
let fast_lookup: Option<Option<Hash256>> = self.with_head(|head| {
562+
let state = &head.beacon_state;
563+
564+
// Try find the root for the `request_slot`.
565+
let request_root_opt = match state.slot.cmp(&request_slot) {
566+
// It's always a skip slot if the head is less than the request slot, return early.
567+
Ordering::Less => return Ok(Some(None)),
568+
// The request slot is the head slot.
569+
Ordering::Equal => Some(head.beacon_block_root),
570+
// Try find the request slot in the state.
571+
Ordering::Greater => state.get_block_root(request_slot).ok().copied(),
572+
};
573+
574+
if let Some(request_root) = request_root_opt {
575+
if let Ok(prev_root) = state.get_block_root(prev_slot) {
576+
return Ok(Some((*prev_root != request_root).then(|| request_root)));
577+
}
536578
}
537-
root_opt
538-
})
579+
580+
// Fast lookup is not possible.
581+
Ok::<_, Error>(None)
582+
})?;
583+
if let Some(root_opt) = fast_lookup {
584+
return Ok(root_opt);
585+
}
586+
587+
if let Some(((prev_root, _), (curr_root, curr_slot))) =
588+
process_results(self.forwards_iter_block_roots(prev_slot)?, |iter| {
589+
iter.tuple_windows().next()
590+
})?
591+
{
592+
// Sanity check.
593+
if curr_slot != request_slot {
594+
return Err(Error::InconsistentForwardsIter {
595+
request_slot,
596+
slot: curr_slot,
597+
});
598+
}
599+
return Ok((curr_root != prev_root).then(|| curr_root));
600+
} else {
601+
return Ok(None);
602+
}
603+
}
604+
605+
/// Returns the block root at the given slot, if any. Only returns roots in the canonical chain.
606+
///
607+
/// ## Notes
608+
///
609+
/// - Returns the root at the previous non-skipped slot if the given `Slot` was skipped.
610+
/// - Returns `Ok(None)` for any slot higher than the current wall-clock slot.
611+
///
612+
/// ## Errors
613+
///
614+
/// May return a database error.
615+
fn block_root_at_slot_skips_prev(&self, request_slot: Slot) -> Result<Option<Hash256>, Error> {
616+
if request_slot > self.slot()? {
617+
return Ok(None);
618+
} else if request_slot == self.spec.genesis_slot {
619+
return Ok(Some(self.genesis_block_root));
620+
}
621+
622+
// Try an optimized path of reading the root directly from the head state.
623+
let fast_lookup: Option<Hash256> = self.with_head(|head| {
624+
if head.beacon_block.slot() <= request_slot {
625+
// Return the head root if all slots between the request and the head are skipped.
626+
Ok(Some(head.beacon_block_root))
627+
} else if let Ok(root) = head.beacon_state.get_block_root(request_slot) {
628+
// Return the root if it's easily accessible from the head state.
629+
Ok(Some(*root))
630+
} else {
631+
// Fast lookup is not possible.
632+
Ok::<_, Error>(None)
633+
}
634+
})?;
635+
if let Some(root) = fast_lookup {
636+
return Ok(Some(root));
637+
}
638+
639+
process_results(self.forwards_iter_block_roots(request_slot)?, |mut iter| {
640+
if let Some((root, slot)) = iter.next() {
641+
if slot == request_slot {
642+
Ok(Some(root))
643+
} else {
644+
// Sanity check.
645+
Err(Error::InconsistentForwardsIter { request_slot, slot })
646+
}
647+
} else {
648+
Ok(None)
649+
}
650+
})?
539651
}
540652

541653
/// Returns the block at the given root, if any.
@@ -825,16 +937,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
825937
Ok(map)
826938
}
827939

828-
/// Returns the block canonical root of the current canonical chain at a given slot.
829-
///
830-
/// Returns `None` if the given slot doesn't exist in the chain.
831-
pub fn root_at_slot(&self, target_slot: Slot) -> Result<Option<Hash256>, Error> {
832-
process_results(self.rev_iter_block_roots()?, |mut iter| {
833-
iter.find(|(_, slot)| *slot == target_slot)
834-
.map(|(root, _)| root)
835-
})
836-
}
837-
838940
/// Returns the block canonical root of the current canonical chain at a given slot, starting from the given state.
839941
///
840942
/// Returns `None` if the given slot doesn't exist in the chain.
@@ -2324,10 +2426,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
23242426
if let Some(event_handler) = self.event_handler.as_ref() {
23252427
if event_handler.has_head_subscribers() {
23262428
if let Ok(Some(current_duty_dependent_root)) =
2327-
self.root_at_slot(target_epoch_start_slot - 1)
2429+
self.block_root_at_slot(target_epoch_start_slot - 1, WhenSlotSkipped::Prev)
23282430
{
2329-
if let Ok(Some(previous_duty_dependent_root)) =
2330-
self.root_at_slot(prev_target_epoch_start_slot - 1)
2431+
if let Ok(Some(previous_duty_dependent_root)) = self
2432+
.block_root_at_slot(prev_target_epoch_start_slot - 1, WhenSlotSkipped::Prev)
23312433
{
23322434
event_handler.register(EventKind::Head(SseHead {
23332435
slot: head_slot,

beacon_node/beacon_chain/src/errors.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ pub enum BeaconChainError {
113113
state_epoch: Epoch,
114114
shuffling_epoch: Epoch,
115115
},
116+
InconsistentForwardsIter {
117+
request_slot: Slot,
118+
slot: Slot,
119+
},
116120
}
117121

118122
easy_from_to!(SlotProcessingError, BeaconChainError);

beacon_node/beacon_chain/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ mod validator_pubkey_cache;
3131

3232
pub use self::beacon_chain::{
3333
AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, BeaconStore, ChainSegmentResult,
34-
ForkChoiceError, StateSkipConfig, MAXIMUM_GOSSIP_CLOCK_DISPARITY,
34+
ForkChoiceError, StateSkipConfig, WhenSlotSkipped, MAXIMUM_GOSSIP_CLOCK_DISPARITY,
3535
};
3636
pub use self::beacon_snapshot::BeaconSnapshot;
3737
pub use self::chain_config::ChainConfig;

beacon_node/beacon_chain/tests/attestation_production.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ extern crate lazy_static;
55

66
use beacon_chain::{
77
test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy},
8-
StateSkipConfig,
8+
StateSkipConfig, WhenSlotSkipped,
99
};
1010
use store::config::StoreConfig;
1111
use tree_hash::TreeHash;
@@ -60,7 +60,7 @@ fn produces_attestations() {
6060
};
6161

6262
let block = chain
63-
.block_at_slot(block_slot)
63+
.block_at_slot(block_slot, WhenSlotSkipped::Prev)
6464
.expect("should get block")
6565
.expect("block should not be skipped");
6666
let block_root = block.message.tree_hash_root();

beacon_node/beacon_chain/tests/attestation_verification.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ extern crate lazy_static;
66
use beacon_chain::{
77
attestation_verification::Error as AttnError,
88
test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType},
9-
BeaconChain, BeaconChainTypes,
9+
BeaconChain, BeaconChainTypes, WhenSlotSkipped,
1010
};
1111
use int_to_bytes::int_to_bytes32;
1212
use state_processing::{
@@ -912,7 +912,7 @@ fn attestation_that_skips_epochs() {
912912
let earlier_slot = (current_epoch - 2).start_slot(MainnetEthSpec::slots_per_epoch());
913913
let earlier_block = harness
914914
.chain
915-
.block_at_slot(earlier_slot)
915+
.block_at_slot(earlier_slot, WhenSlotSkipped::Prev)
916916
.expect("should not error getting block at slot")
917917
.expect("should find block at slot");
918918

0 commit comments

Comments
 (0)