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

Introduce event segment to store multiple events under one key #7836

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
23 changes: 23 additions & 0 deletions prdoc/pr_7836.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
title: '[frame-system] Introduce event segment to store multiple events under one key'

doc:
- audience: [Runtime User, Runtime Dev]
description: |-
Introduce a runtime config item `EventSegmentSize` and a runtime storage
`EventSegments` to replace `Events` to manage the runtime event in `frame-system`.

If `EventSegmentSize` is set to `()` or explicit `0`, the `EventSegments` is not used
and the event is still stored in `Events` as it is. If `EventSegmentSize` is set to `> 0`,
`EventSegments` is used instead of `Events`.

For runtime users, if you need to query the event through runtime storage directly
(e.g. generate storage proof for a event) be aware to use `EventSegments` if it is
activated.

For runtime developers, there is a storage migration `migrate_event_from_previous_block`
that properly clean up the event from the previous block, do include this migration to
the runtime whenever `EventSegmentSize` is changed.

crates:
- name: frame-system
bump: minor
95 changes: 86 additions & 9 deletions substrate/frame/system/src/lib.rs
Original file line number Diff line number Diff line change
@@ -356,6 +356,7 @@ pub mod pallet {
type PreInherents = ();
type PostInherents = ();
type PostTransactions = ();
type EventSegmentSize = ();
}

/// Default configurations of this pallet in a solochain environment.
@@ -458,6 +459,9 @@ pub mod pallet {
type PreInherents = ();
type PostInherents = ();
type PostTransactions = ();

/// The number of event stored together in a segment.
type EventSegmentSize = ();
}

/// Default configurations of this pallet in a relay-chain environment.
@@ -672,6 +676,18 @@ pub mod pallet {
///
/// See `frame_executive::block_flowchart` for a in-depth explanation when it runs.
type PostTransactions: PostTransactions;

/// The number of events stored together in a segment.
///
/// When set to zero all the events are stored in a single storage value `Events` under one key,
/// when set to a non-zero value `N`, `N` number of events will be stored together in a segment
/// under one key in the storage map `EventSegments`.
///
/// Given the same amount of event, the smaller this value the smaller the segment and thus a smaller
/// storage proof for a specific event, but as the number of segment increases and there will be more
/// key-value added to the Trie and may impact the performance.
#[pallet::constant]
type EventSegmentSize: Get<u32>;
}

#[pallet::pallet]
@@ -1014,6 +1030,26 @@ pub mod pallet {
#[pallet::getter(fn event_count)]
pub(super) type EventCount<T: Config> = StorageValue<_, EventIndex, ValueQuery>;

/// Similar to `Events`, storing events deposited for the current block but `EventSegmentSize`
/// number of events are stored together in an item (under the same key) in the map.
///
/// For an event with event index `i`, it is stored in the segment under key `i / EventSegmentSize`
/// in the map and index `i % EventSegmentSize` in the segment.
///
/// NOTE: The event segment from the previous blocks is not cleared but instead overwritten
/// by event deposited later, only the first `EventCount` number of events in the map are
/// deposited by the current block.
#[pallet::storage]
#[pallet::disable_try_decode_storage]
#[pallet::unbounded]
pub(super) type EventSegments<T: Config> =
StorageMap<_, Identity, u32, Vec<Box<EventRecord<T::RuntimeEvent, T::Hash>>>, OptionQuery>;

/// The total number of uncleared events from the previous blocks.
#[pallet::storage]
#[pallet::getter(fn uncleared_event_count)]
pub(super) type UnclearedEventCount<T: Config> = StorageValue<_, EventIndex, ValueQuery>;

/// Mapping between a topic (represented by T::Hash) and a vector of indexes
/// of events in the `<Events<T>>` list.
///
@@ -1784,7 +1820,20 @@ impl<T: Config> Pallet<T> {
old_event_count
};

Events::<T>::append(event);
match T::EventSegmentSize::get() {
0u32 => Events::<T>::append(event),
segment_size => {
let event_segment_idx = event_idx / segment_size;
// For the first event in the segment, use `StorageMap::set` to clear events from
// the previous block. For the following event in the segment, use `StorageMap::append`
// which is more efficient.
if event_idx % segment_size == 0 {
EventSegments::<T>::set(event_segment_idx, Some(vec![event.into()]));
} else {
EventSegments::<T>::append(event_segment_idx, event);
}
},
}

for topic in topics {
<EventTopics<T>>::append(topic, &(block_number, event_idx));
@@ -1969,8 +2018,16 @@ impl<T: Config> Pallet<T> {
/// Should only be called if you know what you are doing and outside of the runtime block
/// execution else it can have a large impact on the PoV size of a block.
pub fn read_events_no_consensus(
) -> impl Iterator<Item = Box<EventRecord<T::RuntimeEvent, T::Hash>>> {
Events::<T>::stream_iter()
) -> Box<dyn Iterator<Item = Box<EventRecord<T::RuntimeEvent, T::Hash>>>> {
if T::EventSegmentSize::get().is_zero() {
Box::new(Events::<T>::stream_iter())
} else {
Box::new(
EventSegments::<T>::iter_values()
.flatten()
.take(EventCount::<T>::get() as usize),
)
}
}

/// Read and return the events of a specific pallet, as denoted by `E`.
@@ -1981,11 +2038,20 @@ impl<T: Config> Pallet<T> {
where
T::RuntimeEvent: TryInto<E>,
{
Events::<T>::get()
.into_iter()
.map(|er| er.event)
.filter_map(|e| e.try_into().ok())
.collect::<_>()
if T::EventSegmentSize::get().is_zero() {
Events::<T>::get()
.into_iter()
.map(|er| er.event)
.filter_map(|e| e.try_into().ok())
.collect::<_>()
} else {
EventSegments::<T>::iter_values()
.flatten()
.take(EventCount::<T>::get() as usize)
.map(|er| er.event)
.filter_map(|e| e.try_into().ok())
.collect::<_>()
}
}

/// Simulate the execution of a block sequence up to a specified height, injecting the
@@ -2066,8 +2132,19 @@ impl<T: Config> Pallet<T> {
///
/// This needs to be used in prior calling [`initialize`](Self::initialize) for each new block
/// to clear events from previous block.
///
/// NOTE: when the event segment is used (i.e. `EventSegmentSize` > 0) the event is not really
/// cleared from the storage, it only resets the event counter and the event will be overwritten
/// by the new event from the next block.
pub fn reset_events() {
<Events<T>>::kill();
if T::EventSegmentSize::get().is_zero() {
<Events<T>>::kill();
} else {
let pre_uncleared_event_count = UnclearedEventCount::<T>::get();
let event_count = EventCount::<T>::get();
UnclearedEventCount::<T>::set(pre_uncleared_event_count.max(event_count));
}

EventCount::<T>::kill();
let _ = <EventTopics<T>>::clear(u32::max_value(), None);
}
15 changes: 14 additions & 1 deletion substrate/frame/system/src/migrations/mod.rs
Original file line number Diff line number Diff line change
@@ -18,11 +18,12 @@
//! Migrate the reference counting state.

use super::LOG_TARGET;
use crate::{Config, Pallet};
use crate::{Config, Events, EventSegments, Pallet, UnclearedEventCount};
use codec::{Decode, Encode, FullCodec};
use frame_support::{
pallet_prelude::ValueQuery, traits::PalletInfoAccess, weights::Weight, Blake2_128Concat,
};
use sp_core::Get;
use sp_runtime::RuntimeDebug;

/// Type used to encode the number of references an account has.
@@ -119,3 +120,15 @@ pub fn migrate_from_dual_to_triple_ref_count<V: V2ToV3, T: Config>() -> Weight {
<UpgradedToTripleRefCount<T>>::put(true);
Weight::MAX
}

/// Migration that clean up the event from the previous block
///
/// NOTE: This is needed because `reset_events` may not properly clean up
/// the event from the previous block when the `EventSegmentSize` config is
/// changed in the new runtime.
pub fn migrate_event_from_previous_block<T: Config>() -> Weight {
Events::<T>::kill();
let r = EventSegments::<T>::clear(u32::max_value(), None);
UnclearedEventCount::<T>::set(0);
T::DbWeight::get().writes(2u64 + r.unique as u64)
}
11 changes: 11 additions & 0 deletions substrate/frame/system/src/mock.rs
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
// limitations under the License.

use crate::{self as frame_system, *};
use core::sync::atomic::{AtomicU32, Ordering};
use frame_support::{derive_impl, parameter_types};
use sp_runtime::{type_with_default::TypeWithDefault, BuildStorage, Perbill};

@@ -96,12 +97,22 @@ impl Config for Test {
type OnKilledAccount = RecordKilled;
type MultiBlockMigrator = MockedMigrator;
type Nonce = TypeWithDefault<u64, DefaultNonceProvider>;
type EventSegmentSize = EventSegmentSize;
}

parameter_types! {
pub static Ongoing: bool = false;
}

pub static EVENT_SEGMENT_SIZE_VALUE: AtomicU32 = AtomicU32::new(0u32);

pub struct EventSegmentSize;
impl Get<u32> for EventSegmentSize {
fn get() -> u32 {
EVENT_SEGMENT_SIZE_VALUE.load(Ordering::SeqCst)
}
}

pub struct MockedMigrator;
impl frame_support::migrations::MultiStepMigrator for MockedMigrator {
fn ongoing() -> bool {
136 changes: 136 additions & 0 deletions substrate/frame/system/src/tests.rs
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
// limitations under the License.

use crate::*;
use core::sync::atomic::Ordering;
use frame_support::{
assert_noop, assert_ok,
dispatch::{Pays, PostDispatchInfo, WithPostDispatchInfo},
@@ -956,3 +957,138 @@ fn reclaim_works() {
assert_eq!(crate::ExtrinsicWeightReclaimed::<Test>::get(), Weight::zero());
});
}

#[test]
fn test_event_segment() {
new_test_ext().execute_with(|| {
// Set the `EventSegmentSize` to 0, the event will store in `Events` instead of `EventSegments`
System::reset_events();
EVENT_SEGMENT_SIZE_VALUE.store(0u32, Ordering::SeqCst);
System::initialize(&1, &[0u8; 32].into(), &Default::default());
System::note_finished_extrinsics();
System::deposit_event(SysEvent::CodeUpdated);
System::finalize();
assert_eq!(
System::events(),
vec![EventRecord {
phase: Phase::Finalization,
event: SysEvent::CodeUpdated.into(),
topics: vec![],
}]
);
assert_eq!(Events::<Test>::get().len(), 1);
assert_eq!(EventSegments::<Test>::iter().count(), 0);

// Set the `EventSegmentSize` > 0, the event will store in `EventSegments` instead of `Events`
System::reset_events();
assert!(System::events().is_empty());
EVENT_SEGMENT_SIZE_VALUE.store(1u32, Ordering::SeqCst);
System::initialize(&2, &[0u8; 32].into(), &Default::default());
for i in 0..10 {
System::deposit_event(SysEvent::NewAccount { account: i as u64 + 1 });
}
System::note_finished_initialize();
System::note_finished_extrinsics();
System::finalize();
for (i, event) in System::events().into_iter().enumerate() {
assert_eq!(
event,
EventRecord {
phase: Phase::Initialization,
event: SysEvent::NewAccount { account: i as u64 + 1 }.into(),
topics: vec![]
}
);
}
assert_eq!(Events::<Test>::get().len(), 0);
assert_eq!(EventSegments::<Test>::iter().count(), 10);

// Set the `EventSegmentSize` to 2, each event segment will at most contains 2 events
System::reset_events();
assert!(System::events().is_empty());
EVENT_SEGMENT_SIZE_VALUE.store(2u32, Ordering::SeqCst);
System::initialize(&3, &[0u8; 32].into(), &Default::default());
System::note_finished_initialize();
System::note_finished_extrinsics();
for i in 10..15 {
System::deposit_event(SysEvent::NewAccount { account: i as u64 + 1 });
}
System::finalize();
assert_eq!(Events::<Test>::get().len(), 0);

// 5 events from the current block stored in the first 3 segments
assert_eq!(
EventSegments::<Test>::get(0).unwrap(),
vec![
EventRecord {
phase: Phase::Finalization,
event: SysEvent::NewAccount { account: 11 }.into(),
topics: vec![]
}
.into(),
EventRecord {
phase: Phase::Finalization,
event: SysEvent::NewAccount { account: 12 }.into(),
topics: vec![]
}
.into()
]
);
assert_eq!(
EventSegments::<Test>::get(1).unwrap(),
vec![
EventRecord {
phase: Phase::Finalization,
event: SysEvent::NewAccount { account: 13 }.into(),
topics: vec![]
}
.into(),
EventRecord {
phase: Phase::Finalization,
event: SysEvent::NewAccount { account: 14 }.into(),
topics: vec![]
}
.into()
]
);
assert_eq!(
EventSegments::<Test>::get(2).unwrap(),
vec![EventRecord {
phase: Phase::Finalization,
event: SysEvent::NewAccount { account: 15 }.into(),
topics: vec![]
}
.into()]
);

// The events from the previous block is not cleared in `EventSegments`
assert_eq!(EventSegments::<Test>::iter().count(), 10);
assert_eq!(UnclearedEventCount::<Test>::get(), 10);
for i in 3..10 {
assert_eq!(
EventSegments::<Test>::get(i).unwrap(),
vec![EventRecord {
phase: Phase::Initialization,
event: SysEvent::NewAccount { account: i as u64 + 1 }.into(),
topics: vec![]
}
.into()]
);
}

// But also inaccessible by `System::events()`
for (i, event) in System::events().into_iter().enumerate() {
assert_eq!(
event,
EventRecord {
phase: Phase::Finalization,
event: SysEvent::NewAccount { account: i as u64 + 11 }.into(),
topics: vec![]
}
);
}

System::reset_events();
assert!(System::events().is_empty());
});
}