use super::{
bbr::{rounded_usize, BbrState},
config::*,
request::*,
work_queue::WorkQueue,
*,
};
use crate::zakura::{
chain_frontier_from_parts, Frontier, FrontierUpdate, ServicePeerDirection, ServicePeerSnapshot,
ZakuraBlockSyncCandidateState,
};
pub(super) const EFFECTIVE_BS_OUTBOUND_INFLIGHT_PER_PEER: usize = MAX_BS_INFLIGHT_REQUESTS as usize;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct BlockSyncFrontiers {
pub finalized_height: block::Height,
pub verified_block_tip: block::Height,
pub verified_block_hash: block::Hash,
}
#[derive(Clone, Debug)]
pub struct BlockSyncStartup {
pub frontiers: BlockSyncFrontiers,
pub best_header_tip: (block::Height, block::Hash),
pub header_tip: Option<watch::Receiver<(block::Height, block::Hash)>>,
pub frontier_updates: Option<watch::Receiver<FrontierUpdate>>,
pub config: ZakuraBlockSyncConfig,
pub shutdown: CancellationToken,
pub state_queries_enabled: bool,
pub trace: ZakuraTrace,
}
impl BlockSyncStartup {
pub fn new(
frontiers: BlockSyncFrontiers,
best_header_tip: (block::Height, block::Hash),
header_tip: watch::Receiver<(block::Height, block::Hash)>,
config: ZakuraBlockSyncConfig,
) -> Self {
Self {
frontiers,
best_header_tip,
header_tip: Some(header_tip),
frontier_updates: None,
config,
shutdown: CancellationToken::new(),
state_queries_enabled: true,
trace: ZakuraTrace::noop(),
}
}
pub fn new_with_exchange(
frontiers: BlockSyncFrontiers,
best_header_tip: (block::Height, block::Hash),
frontier_updates: watch::Receiver<FrontierUpdate>,
config: ZakuraBlockSyncConfig,
) -> Self {
Self {
frontiers,
best_header_tip,
header_tip: None,
frontier_updates: Some(frontier_updates),
config,
shutdown: CancellationToken::new(),
state_queries_enabled: true,
trace: ZakuraTrace::noop(),
}
}
pub fn frontier_update_from_parts(
frontiers: BlockSyncFrontiers,
best_header_tip: (block::Height, block::Hash),
) -> FrontierUpdate {
FrontierUpdate {
frontier: chain_frontier_from_parts(
frontiers.finalized_height,
Frontier::new(frontiers.verified_block_tip, frontiers.verified_block_hash),
Frontier::new(best_header_tip.0, best_header_tip.1),
),
change: crate::zakura::FrontierChange::Snapshot,
}
}
pub(super) fn inert(config: ZakuraBlockSyncConfig) -> Self {
Self {
frontiers: BlockSyncFrontiers {
finalized_height: block::Height::MIN,
verified_block_tip: block::Height::MIN,
verified_block_hash: block::Hash([0; 32]),
},
best_header_tip: (block::Height::MIN, block::Hash([0; 32])),
header_tip: None,
frontier_updates: None,
config,
shutdown: CancellationToken::new(),
state_queries_enabled: false,
trace: ZakuraTrace::noop(),
}
}
}
#[derive(Clone, Debug)]
pub struct BlockSyncHandle {
pub(super) events: mpsc::Sender<BlockSyncEvent>,
pub(super) lifecycle: mpsc::UnboundedSender<BlockSyncEvent>,
pub(super) peers: watch::Receiver<ServicePeerSnapshot>,
pub(super) status: watch::Receiver<BlockSyncStatus>,
pub(super) candidates: watch::Receiver<ZakuraBlockSyncCandidateState>,
pub(super) routine_wiring: Option<RoutineWiring>,
}
#[derive(Clone, Debug)]
pub(super) struct RoutineWiring {
pub(super) config: ZakuraBlockSyncConfig,
pub(super) budget: ByteBudget,
pub(super) work: Arc<WorkQueue>,
pub(super) registry: Arc<super::peer_registry::PeerRegistry>,
pub(super) received_throughput: Arc<std::sync::Mutex<ThroughputMeter>>,
pub(super) sequencer_input: mpsc::Sender<super::sequencer_task::SequencedBody>,
pub(super) sequencer_input_bytes: Arc<std::sync::atomic::AtomicU64>,
pub(super) sequencer_input_decoded_attributed_memory_bytes: Arc<std::sync::atomic::AtomicU64>,
pub(super) actions: mpsc::Sender<BlockSyncAction>,
pub(super) routine_to_reactor: mpsc::Sender<super::events::RoutineToReactor>,
pub(super) view: watch::Receiver<super::sequencer_task::SequencerView>,
pub(super) trace: ZakuraTrace,
}
impl BlockSyncHandle {
pub async fn send(
&self,
event: BlockSyncEvent,
) -> Result<(), mpsc::error::SendError<BlockSyncEvent>> {
self.events.send(event).await
}
pub fn try_send(
&self,
event: BlockSyncEvent,
) -> Result<(), mpsc::error::TrySendError<BlockSyncEvent>> {
self.events.try_send(event)
}
pub fn send_control(
&self,
event: BlockSyncEvent,
) -> Result<(), mpsc::error::SendError<BlockSyncEvent>> {
self.lifecycle
.send(event)
.map_err(|error| mpsc::error::SendError(error.0))
}
pub fn send_lifecycle(
&self,
event: BlockSyncEvent,
) -> Result<(), mpsc::error::SendError<BlockSyncEvent>> {
self.send_control(event)
}
pub fn peer_snapshot(&self) -> ServicePeerSnapshot {
*self.peers.borrow()
}
pub fn subscribe_status(&self) -> watch::Receiver<BlockSyncStatus> {
self.status.clone()
}
pub fn local_status(&self) -> BlockSyncStatus {
*self.status.borrow()
}
pub fn subscribe_candidate_state(&self) -> watch::Receiver<ZakuraBlockSyncCandidateState> {
self.candidates.clone()
}
pub fn candidate_state(&self) -> ZakuraBlockSyncCandidateState {
self.candidates.borrow().clone()
}
}
#[derive(Debug)]
pub(super) struct BlockSyncState {
pub(super) finalized_height: block::Height,
pub(super) verified_block_hash: block::Hash,
pub(super) servable_high: block::Height,
pub(super) servable_hash: block::Hash,
pub(super) best_header_tip: block::Height,
pub(super) best_header_hash: block::Hash,
pub(super) peers: HashMap<ZakuraPeerId, PeerBlockState>,
pub(super) parked_peers: HashSet<ZakuraPeerId>,
pub(super) work_queue: Arc<WorkQueue>,
pub(super) budget: ByteBudget,
pub(super) needed_heights: Vec<block::Height>,
pub(super) status_refresh: RateMeter,
pub(super) pending_status_refresh: bool,
pub(super) last_advertised_status: BlockSyncStatus,
pub(super) received_throughput: Arc<std::sync::Mutex<ThroughputMeter>>,
}
impl BlockSyncState {
pub(super) fn new(startup: &BlockSyncStartup) -> Self {
let last_advertised_status = BlockSyncStatus {
servable_low: block::Height::MIN,
servable_high: startup.frontiers.verified_block_tip,
tip_hash: startup.frontiers.verified_block_hash,
max_blocks_per_response: startup.config.advertised_max_blocks_per_response(),
max_inflight_requests: startup.config.advertised_max_inflight_requests(),
max_response_bytes: startup.config.advertised_max_response_bytes(),
};
Self {
finalized_height: startup.frontiers.finalized_height,
verified_block_hash: startup.frontiers.verified_block_hash,
servable_high: startup.frontiers.verified_block_tip,
servable_hash: startup.frontiers.verified_block_hash,
best_header_tip: startup.best_header_tip.0,
best_header_hash: startup.best_header_tip.1,
peers: HashMap::new(),
parked_peers: HashSet::new(),
work_queue: Arc::new(WorkQueue::new(startup.frontiers.verified_block_tip)),
budget: ByteBudget::new(startup.config.max_inflight_block_bytes),
needed_heights: Vec::new(),
status_refresh: RateMeter::new(startup.config.status_refresh_interval),
pending_status_refresh: false,
last_advertised_status,
received_throughput: Arc::new(std::sync::Mutex::new(ThroughputMeter::new(
Instant::now(),
))),
}
}
pub(super) fn peer_snapshot(&self, limits: ServicePeerLimits) -> ServicePeerSnapshot {
let inbound = self
.peers
.values()
.filter(|peer| peer.direction == ServicePeerDirection::Inbound)
.count();
let outbound = self
.peers
.values()
.filter(|peer| peer.direction == ServicePeerDirection::Outbound)
.count();
ServicePeerSnapshot::new(inbound, outbound, limits)
}
}
#[derive(Clone, Debug)]
pub(super) struct DownloadWindow {
pub(super) max_inflight_requests: u32,
pub(super) outstanding: Vec<OutstandingBlockRange>,
bbr: BbrState,
cwnd_unit: CwndUnit,
pub(super) startup_request_cap: usize,
pub(super) block_liveness_deadline: Option<Instant>,
pub(super) last_request_at: Option<Instant>,
pub(super) last_block_at: Option<Instant>,
pub(super) requests_without_block_progress: u32,
max_requests_without_block_progress: u32,
initial_block_probe_requests: u32,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum LivenessOutcome {
Ok,
Disarm,
Disconnect,
}
impl DownloadWindow {
pub(super) fn new(config: &ZakuraBlockSyncConfig) -> Self {
Self {
max_inflight_requests: config.advertised_max_inflight_requests(),
outstanding: Vec::new(),
bbr: BbrState::new(config),
cwnd_unit: config.bbr_cwnd_unit,
startup_request_cap: usize::try_from(config.initial_inflight_requests)
.unwrap_or(usize::MAX)
.max(1),
block_liveness_deadline: None,
last_request_at: None,
last_block_at: None,
requests_without_block_progress: 0,
max_requests_without_block_progress: config.max_requests_without_block_progress,
initial_block_probe_requests: config.initial_block_probe_requests,
}
}
pub(super) fn delivery_snapshot(&self, now: Instant) -> DeliverySnapshot {
self.bbr.delivery_snapshot(now)
}
pub(super) fn record_delivery(
&mut self,
now: Instant,
elapsed: Duration,
blocks: u32,
delivered_bytes: u64,
snapshot: DeliverySnapshot,
) {
let inflight = match self.cwnd_unit {
CwndUnit::Blocks => self.outstanding.len() as u64,
CwndUnit::Bytes => self.outstanding_reserved_bytes(),
};
self.bbr
.record_delivery(now, elapsed, blocks, delivered_bytes, inflight, snapshot);
}
pub(super) fn bbr_effective_cwnd(&self) -> usize {
match self.cwnd_unit {
CwndUnit::Blocks => self.bbr.effective_cwnd(),
CwndUnit::Bytes => {
let cwnd_bytes = self.bbr.effective_cwnd() as u64;
let rep = self.representative_body_bytes();
usize::try_from((cwnd_bytes / rep.max(1)).max(1)).unwrap_or(usize::MAX)
}
}
}
pub(super) fn bbr_effective_cwnd_bytes(&self) -> Option<u64> {
matches!(self.cwnd_unit, CwndUnit::Bytes).then(|| self.bbr.effective_cwnd() as u64)
}
fn representative_body_bytes(&self) -> u64 {
let outstanding = self.outstanding.len() as u64;
if outstanding == 0 {
return block::MAX_BLOCK_BYTES;
}
(self.outstanding_reserved_bytes() / outstanding).max(1)
}
pub(super) fn bbr_rtprop_ms(&self, now: Instant) -> Option<u64> {
self.bbr.rtprop_ms(now)
}
pub(super) fn bbr_btlbw_milliblocks(&self, now: Instant) -> Option<u64> {
matches!(self.cwnd_unit, CwndUnit::Blocks)
.then(|| self.bbr.btlbw_milliblocks_per_sec(now))
.flatten()
}
pub(super) fn bbr_btlbw_bytes_per_sec(&self, now: Instant) -> Option<u64> {
if !matches!(self.cwnd_unit, CwndUnit::Bytes) {
return None;
}
self.bbr
.btlbw_units_per_sec(now)
.map(|rate| rate.round() as u64)
}
pub(super) fn bbr_inflight_bytes(&self) -> u64 {
self.outstanding_reserved_bytes()
}
pub(super) fn bbr_delivered(&self) -> u64 {
self.bbr.delivered()
}
pub(super) fn bbr_phase_code(&self) -> u64 {
self.bbr.phase_code()
}
pub(super) fn bbr_smoothed_elapsed_ms(&self) -> Option<u64> {
self.bbr.smoothed_elapsed_ms()
}
pub(super) fn bbr_delay_cap(&self) -> Option<u64> {
self.bbr
.delay_cap()
.map(|cap| u64::try_from(cap).unwrap_or(u64::MAX))
}
pub(super) fn bbr_reliability_permille(&self) -> u64 {
self.bbr.reliability_permille()
}
pub(super) fn available_slots(&self) -> usize {
self.available_slots_at(Instant::now())
}
pub(super) fn available_slots_at(&self, now: Instant) -> usize {
self.available_slots_with_bonus_at(0, now)
}
pub(super) fn available_slots_with_bonus_at(&self, bonus: usize, now: Instant) -> usize {
let hard_cap = self.hard_outbound_capacity();
match self.cwnd_unit {
CwndUnit::Blocks => {
let cwnd_slots = self
.bbr
.effective_cwnd()
.saturating_add(bonus)
.min(hard_cap);
cwnd_slots.saturating_sub(self.outstanding.len())
}
CwndUnit::Bytes => {
let outstanding = self.outstanding.len();
if outstanding >= hard_cap {
return 0;
}
if !self.bbr.has_fresh_bdp(now) {
let startup_cap = self.startup_request_cap.saturating_add(bonus).min(hard_cap);
if outstanding >= startup_cap {
return 0;
}
}
let reserved = self.outstanding_reserved_bytes();
let representative = self.representative_body_bytes();
let bonus_bytes = (bonus as u64).saturating_mul(representative);
let cwnd_bytes = (self.bbr.effective_cwnd() as u64).saturating_add(bonus_bytes);
usize::try_from(cwnd_bytes.saturating_sub(reserved)).unwrap_or(usize::MAX)
}
}
}
pub(super) fn cwnd_byte_headroom_at(&self, bonus: usize, now: Instant) -> Option<u64> {
match self.cwnd_unit {
CwndUnit::Blocks => None,
CwndUnit::Bytes => Some(self.available_slots_with_bonus_at(bonus, now) as u64),
}
}
#[cfg(test)]
pub(super) fn available_slots_with_bonus(&self, bonus: usize) -> usize {
self.available_slots_with_bonus_at(bonus, Instant::now())
}
#[cfg(test)]
pub(super) fn cwnd_byte_headroom(&self, bonus: usize) -> Option<u64> {
self.cwnd_byte_headroom_at(bonus, Instant::now())
}
pub(super) fn scaled_floor_bonus(&self, base: usize) -> usize {
rounded_usize(base as f64 * self.bbr.reliability_factor(), 0)
}
#[cfg(test)]
pub(super) fn reliability_factor(&self) -> f64 {
self.bbr.reliability_factor()
}
fn outstanding_reserved_bytes(&self) -> u64 {
self.outstanding.iter().fold(0u64, |acc, range| {
acc.saturating_add(range.reserved_bytes())
})
}
pub(super) fn record_timeout(&mut self, timed_out: usize) {
self.bbr.dip_on_timeout();
self.bbr.penalize_reliability(timed_out);
}
pub(super) fn penalize_short_response(&mut self, missing: usize) {
if missing > 0 {
self.bbr.penalize_reliability(1);
}
}
pub(super) fn credit_late_delivery(&mut self) {
self.bbr.credit_late_success();
}
pub(super) fn has_block_progress(&self) -> bool {
self.last_block_at.is_some()
}
pub(super) fn no_progress_request_cap(&self) -> u32 {
if self.has_block_progress() {
self.max_requests_without_block_progress
} else {
self.initial_block_probe_requests
}
}
pub(super) fn arm_liveness(&mut self, now: Instant, timeout: Duration) {
self.last_request_at = Some(now);
self.requests_without_block_progress =
self.requests_without_block_progress.saturating_add(1);
if self.block_liveness_deadline.is_none() {
self.block_liveness_deadline = Some(now + timeout);
}
}
pub(super) fn note_block_progress(&mut self, now: Instant, timeout: Duration) {
self.last_block_at = Some(now);
self.requests_without_block_progress = 0;
self.block_liveness_deadline = if self.outstanding.is_empty() {
None
} else {
Some(now + timeout)
};
}
pub(super) fn disarm_liveness_after_progress_if_idle(&mut self) {
if self.outstanding.is_empty()
&& matches!(
(self.last_request_at, self.last_block_at),
(Some(request_at), Some(block_at)) if block_at >= request_at
)
{
self.block_liveness_deadline = None;
}
}
pub(super) fn clear_liveness_if_idle(&mut self) {
if self.outstanding.is_empty() {
self.block_liveness_deadline = None;
}
}
pub(super) fn note_view_reset(&mut self) {
self.requests_without_block_progress = 0;
self.clear_liveness_if_idle();
}
pub(super) fn extend_liveness_deadline(&mut self, now: Instant, timeout: Duration) {
self.block_liveness_deadline = Some(now + timeout);
}
pub(super) fn check_liveness(&self, now: Instant) -> LivenessOutcome {
match self.block_liveness_deadline {
None => LivenessOutcome::Ok,
Some(deadline) if self.last_request_at.is_none() && now >= deadline => {
LivenessOutcome::Disarm
}
Some(deadline) if now < deadline => LivenessOutcome::Ok,
Some(_) => LivenessOutcome::Disconnect,
}
}
pub(super) fn hard_outbound_capacity(&self) -> usize {
usize::try_from(self.max_inflight_requests)
.expect("u32 max inflight requests fits in usize on supported targets")
.min(EFFECTIVE_BS_OUTBOUND_INFLIGHT_PER_PEER)
}
pub(super) fn outstanding_index_for_height(&self, height: block::Height) -> Option<usize> {
self.outstanding
.iter()
.position(|outstanding| outstanding.request.contains(height))
}
pub(super) fn outstanding_index_for_start(&self, start_height: block::Height) -> Option<usize> {
self.outstanding
.iter()
.position(|outstanding| outstanding.request.start_height == start_height)
}
}
#[derive(Debug)]
pub(super) struct PeerBlockState {
pub(super) session: BlockSyncPeerSession,
pub(super) direction: ServicePeerDirection,
pub(super) refresh_meter: RateMeter,
pub(super) served_blocks_inflight: u32,
pub(super) served_block_requests: VecDeque<(block::Height, Instant)>,
}
impl PeerBlockState {
pub(super) fn new(session: BlockSyncPeerSession, config: &ZakuraBlockSyncConfig) -> Self {
Self {
direction: session.direction(),
session,
refresh_meter: RateMeter::new(config.status_refresh_interval),
served_blocks_inflight: 0,
served_block_requests: VecDeque::new(),
}
}
pub(super) fn try_start_serving_blocks(
&mut self,
local_inflight_cap: u32,
start_height: block::Height,
) -> bool {
if self.served_blocks_inflight >= local_inflight_cap {
return false;
}
self.served_blocks_inflight = self.served_blocks_inflight.saturating_add(1);
self.served_block_requests
.push_back((start_height, Instant::now()));
true
}
pub(super) fn serving_blocks_elapsed(&self, start_height: block::Height) -> Option<Duration> {
self.served_block_requests
.iter()
.find_map(|(start, started)| (*start == start_height).then(|| started.elapsed()))
}
pub(super) fn finish_serving_blocks(
&mut self,
start_height: block::Height,
) -> Option<Duration> {
self.served_blocks_inflight = self.served_blocks_inflight.saturating_sub(1);
self.served_block_requests
.iter()
.position(|(start, _)| *start == start_height)
.and_then(|index| self.served_block_requests.remove(index))
.map(|(_, started)| started.elapsed())
}
}
#[derive(Clone, Debug)]
pub(super) struct OutstandingBlockRange {
pub(super) request: BlockRangeRequest,
pub(super) queued_at: Instant,
pub(super) deadline: Instant,
pub(super) delivery_snapshot: DeliverySnapshot,
pub(super) delivered_bytes: u64,
pub(super) received: ReceivedBlockTracker,
}
#[derive(Copy, Clone, Debug)]
pub(super) struct DeliverySnapshot {
pub(super) delivered: u64,
pub(super) delivered_at: Instant,
}
impl OutstandingBlockRange {
pub(super) fn reserved_bytes(&self) -> u64 {
self.request
.expected_blocks
.iter()
.filter(|expected| !self.has_received(expected.height))
.fold(0u64, |acc, expected| {
acc.saturating_add(expected.estimated_bytes)
})
}
pub(super) fn estimated_bytes_for_height(&self, height: block::Height) -> Option<u64> {
self.request.estimated_bytes_for_height(height)
}
pub(super) fn has_received(&self, height: block::Height) -> bool {
self.request
.offset_for_height(height)
.is_some_and(|offset| self.received.contains_offset(offset))
}
pub(super) fn mark_received(&mut self, height: block::Height) {
if let Some(offset) = self.request.offset_for_height(height) {
self.received.insert_offset(offset);
}
}
pub(super) fn record_body_bytes(&mut self, bytes: u64) {
self.delivered_bytes = self.delivered_bytes.saturating_add(bytes);
}
pub(super) fn mark_received_through(&mut self, tip: block::Height) -> u64 {
self.request
.expected_blocks
.iter()
.filter(|expected| {
expected.height <= tip
&& self
.request
.offset_for_height(expected.height)
.is_some_and(|offset| self.received.insert_offset(offset))
})
.fold(0u64, |acc, expected| {
acc.saturating_add(expected.estimated_bytes)
})
}
pub(super) fn is_complete(&self) -> bool {
self.received.len() == self.request.expected_blocks.len()
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum BlockBudgetLedger {
Reserved(u64),
Released,
}
impl BlockBudgetLedger {
pub(super) fn reserved(estimate: u64) -> Self {
Self::Reserved(estimate)
}
pub(super) fn reserved_charge(self) -> u64 {
match self {
Self::Reserved(bytes) => bytes,
Self::Released => 0,
}
}
pub(super) fn is_reserved(self) -> bool {
matches!(self, Self::Reserved(_))
}
pub(super) fn release_reserved(&mut self) -> u64 {
let released = self.reserved_charge();
*self = Self::Released;
released
}
}
const RECEIVED_TRACKER_OFFSET_CAPACITY: u32 = u128::BITS;
const _: () = assert!(MAX_BS_BLOCKS_PER_REQUEST <= RECEIVED_TRACKER_OFFSET_CAPACITY);
#[derive(Clone, Debug, Default)]
pub(super) struct ReceivedBlockTracker {
bits: u128,
count: usize,
}
impl ReceivedBlockTracker {
pub(super) fn len(&self) -> usize {
self.count
}
fn contains_offset(&self, offset: u32) -> bool {
Self::bit_for_offset(offset).is_some_and(|bit| self.bits & bit != 0)
}
fn insert_offset(&mut self, offset: u32) -> bool {
let Some(bit) = Self::bit_for_offset(offset) else {
return false;
};
if self.bits & bit != 0 {
return false;
}
self.bits |= bit;
self.count = self.count.saturating_add(1);
true
}
fn bit_for_offset(offset: u32) -> Option<u128> {
1u128.checked_shl(offset)
}
}
#[derive(Clone, Debug)]
pub(super) struct RateMeter {
pub(super) next_allowed: Instant,
pub(super) interval: Duration,
}
impl RateMeter {
pub(super) fn new(interval: Duration) -> Self {
Self {
next_allowed: Instant::now(),
interval,
}
}
pub(super) fn try_take(&mut self, now: Instant) -> bool {
if now < self.next_allowed {
return false;
}
self.next_allowed = now + self.interval;
true
}
pub(super) fn is_ready(&self, now: Instant) -> bool {
now >= self.next_allowed
}
pub(super) fn mark_taken(&mut self, now: Instant) {
self.next_allowed = now + self.interval;
}
}
#[derive(Clone, Debug)]
pub(super) struct ThroughputMeter {
bytes: u64,
blocks: u64,
window_start: Instant,
last_bytes_per_sec: u64,
last_blocks_per_sec: u64,
}
impl ThroughputMeter {
pub(super) fn new(now: Instant) -> Self {
Self {
bytes: 0,
blocks: 0,
window_start: now,
last_bytes_per_sec: 0,
last_blocks_per_sec: 0,
}
}
pub(super) fn record(&mut self, bytes: u64) {
self.bytes = self.bytes.saturating_add(bytes);
self.blocks = self.blocks.saturating_add(1);
}
pub(super) fn sample(&mut self, now: Instant) {
let elapsed = now
.saturating_duration_since(self.window_start)
.as_secs_f64();
if elapsed <= 0.0 {
return;
}
self.last_bytes_per_sec = (self.bytes as f64 / elapsed) as u64;
self.last_blocks_per_sec = (self.blocks as f64 / elapsed) as u64;
self.bytes = 0;
self.blocks = 0;
self.window_start = now;
}
pub(super) fn bytes_per_sec(&self) -> u64 {
self.last_bytes_per_sec
}
pub(super) fn blocks_per_sec(&self) -> u64 {
self.last_blocks_per_sec
}
}
pub(crate) use crate::zakura::transport::ByteBudget;
pub(super) fn next_height(height: block::Height) -> Option<block::Height> {
height.0.checked_add(1).map(block::Height)
}
pub(super) fn previous_height(height: block::Height) -> Option<block::Height> {
height.0.checked_sub(1).map(block::Height)
}
pub(super) fn height_after_count(start: block::Height, count: u32) -> Option<block::Height> {
start.0.checked_add(count).map(block::Height)
}