Skip to content

Commit 8e8d4ce

Browse files
authored
Avoid loading empty outboxes when creating network actions. (#4119)
## Motivation In #3989 I had to remove the code that deletes empty outboxes. However, we can actually still do that in some cases, and we can keep a separate list of nonempty outboxes to avoid reading empty ones. ## Proposal Delete empty outboxes if that outbox is not ahead of chain execution. Keep a list of nonempty outboxes in the chain state to avoid loading empty ones. ## Test Plan This improved the benchmark for me again: ``` same_chain_native_token_transfers time: [152.54 ms 155.17 ms 158.41 ms] change: [-27.026% -24.383% -21.712%] (p = 0.00 < 0.05) Performance has improved. ``` CI should catch regressions. ## Release Plan - Nothing to do / These changes follow the usual release cycle. ## Links - Closes #4064. - [reviewer checklist](https://github.com/linera-io/linera-protocol/blob/main/CONTRIBUTING.md#reviewer-checklist)
1 parent 5e90da3 commit 8e8d4ce

File tree

3 files changed

+39
-11
lines changed

3 files changed

+39
-11
lines changed

linera-chain/src/chain.rs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use linera_views::{
2828
context::Context,
2929
log_view::LogView,
3030
map_view::MapView,
31-
reentrant_collection_view::ReentrantCollectionView,
31+
reentrant_collection_view::{ReadGuardedView, ReentrantCollectionView},
3232
register_view::RegisterView,
3333
set_view::SetView,
3434
store::ReadableKeyValueStore as _,
@@ -278,6 +278,8 @@ where
278278
/// Number of outgoing messages in flight for each block height.
279279
/// We use a `RegisterView` to prioritize speed for small maps.
280280
pub outbox_counters: RegisterView<C, BTreeMap<BlockHeight, u32>>,
281+
/// Outboxes with at least one pending message. This allows us to avoid loading all outboxes.
282+
pub nonempty_outboxes: RegisterView<C, BTreeSet<ChainId>>,
281283

282284
/// Blocks that have been verified but not executed yet, and that may not be contiguous.
283285
pub preprocessed_blocks: MapView<C, BlockHeight, CryptoHash>,
@@ -414,15 +416,22 @@ where
414416
.outbox_counters
415417
.get_mut()
416418
.get_mut(&update)
417-
.expect("message counter should be present");
418-
*counter = counter
419-
.checked_sub(1)
420-
.expect("message counter should not underflow");
419+
.ok_or_else(|| {
420+
ChainError::InternalError("message counter should be present".into())
421+
})?;
422+
*counter = counter.checked_sub(1).ok_or(ArithmeticError::Underflow)?;
421423
if *counter == 0 {
422424
// Important for the test in `all_messages_delivered_up_to`.
423425
self.outbox_counters.get_mut().remove(&update);
424426
}
425427
}
428+
if outbox.queue.count() == 0 {
429+
self.nonempty_outboxes.get_mut().remove(target);
430+
// If the outbox is empty and not ahead of the executed blocks, remove it.
431+
if *outbox.next_height_to_schedule.get() <= self.tip_state.get().next_block_height {
432+
self.outboxes.remove_entry(target)?;
433+
}
434+
}
426435
#[cfg(with_metrics)]
427436
metrics::NUM_OUTBOXES
428437
.with_label_values(&[])
@@ -696,6 +705,21 @@ where
696705
Ok(())
697706
}
698707

708+
/// Returns the chain IDs of all recipients for which a message is waiting in the outbox.
709+
pub fn nonempty_outbox_chain_ids(&self) -> Vec<ChainId> {
710+
self.nonempty_outboxes.get().iter().copied().collect()
711+
}
712+
713+
/// Returns the outboxes for the given targets, or an error if any of them are missing.
714+
pub async fn load_outboxes(
715+
&self,
716+
targets: &[ChainId],
717+
) -> Result<Vec<ReadGuardedView<OutboxStateView<C>>>, ChainError> {
718+
let vec_of_options = self.outboxes.try_load_entries(targets).await?;
719+
let optional_vec = vec_of_options.into_iter().collect::<Option<Vec<_>>>();
720+
optional_vec.ok_or_else(|| ChainError::InternalError("Missing outboxes".into()))
721+
}
722+
699723
/// Executes a block: first the incoming messages, then the main operation.
700724
/// Does not update chain state other than the execution state.
701725
#[expect(clippy::too_many_arguments)]
@@ -1146,6 +1170,7 @@ where
11461170

11471171
// Update the outboxes.
11481172
let outbox_counters = self.outbox_counters.get_mut();
1173+
let nonempty_outboxes = self.nonempty_outboxes.get_mut();
11491174
let targets = recipients.into_iter().collect::<Vec<_>>();
11501175
let outboxes = self.outboxes.try_load_entries_mut(&targets).await?;
11511176
for (mut outbox, target) in outboxes.into_iter().zip(&targets) {
@@ -1176,6 +1201,7 @@ where
11761201
}
11771202
if outbox.schedule_message(block_height)? {
11781203
*outbox_counters.entry(block_height).or_default() += 1;
1204+
nonempty_outboxes.insert(*target);
11791205
}
11801206
}
11811207

linera-core/src/chain_worker/state/mod.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -448,16 +448,15 @@ where
448448
/// Loads pending cross-chain requests.
449449
async fn create_network_actions(&self) -> Result<NetworkActions, WorkerError> {
450450
let mut heights_by_recipient = BTreeMap::<_, Vec<_>>::new();
451-
let mut targets = self.chain.outboxes.indices().await?;
451+
let mut targets = self.chain.nonempty_outbox_chain_ids();
452452
if let Some(tracked_chains) = self.tracked_chains.as_ref() {
453453
let tracked_chains = tracked_chains
454454
.read()
455455
.expect("Panics should not happen while holding a lock to `tracked_chains`");
456456
targets.retain(|target| tracked_chains.contains(target));
457457
}
458-
let outboxes = self.chain.outboxes.try_load_entries(&targets).await?;
458+
let outboxes = self.chain.load_outboxes(&targets).await?;
459459
for (target, outbox) in targets.into_iter().zip(outboxes) {
460-
let outbox = outbox.expect("Only existing outboxes should be referenced by `indices`");
461460
let heights = outbox.queue.elements().await?;
462461
heights_by_recipient.insert(target, heights);
463462
}
@@ -539,14 +538,13 @@ where
539538
let Some(tracked_chains) = self.tracked_chains.as_ref() else {
540539
return Ok(false);
541540
};
542-
let mut targets = self.chain.outboxes.indices().await?;
541+
let mut targets = self.chain.nonempty_outbox_chain_ids();
543542
{
544543
let tracked_chains = tracked_chains.read().unwrap();
545544
targets.retain(|target| tracked_chains.contains(target));
546545
}
547-
let outboxes = self.chain.outboxes.try_load_entries(&targets).await?;
546+
let outboxes = self.chain.load_outboxes(&targets).await?;
548547
for outbox in outboxes {
549-
let outbox = outbox.expect("Only existing outboxes should be referenced by `indices`");
550548
let front = outbox.queue.front();
551549
if front.is_some_and(|key| *key <= height) {
552550
return Ok(false);

linera-service-graphql-client/gql/service_schema.graphql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,10 @@ type ChainStateExtendedView {
361361
"""
362362
outboxCounters: JSONObject!
363363
"""
364+
Outboxes with at least one pending message. This allows us to avoid loading all outboxes.
365+
"""
366+
nonemptyOutboxes: [ChainId!]!
367+
"""
364368
Blocks that have been verified but not executed yet, and that may not be contiguous.
365369
"""
366370
preprocessedBlocks: MapView_BlockHeight_CryptoHash_1bae6d76!

0 commit comments

Comments
 (0)