referrerpolicy=no-referrer-when-downgrade

pallet_scheduler/
benchmarking.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Scheduler pallet benchmarking.
19
20use alloc::vec;
21use frame_benchmarking::v2::*;
22use frame_support::{
23	ensure,
24	traits::{schedule::Priority, BoundedInline},
25	weights::WeightMeter,
26};
27use frame_system::{EventRecord, RawOrigin};
28
29use crate::*;
30
31type SystemCall<T> = frame_system::Call<T>;
32type SystemOrigin<T> = <T as frame_system::Config>::RuntimeOrigin;
33
34const SEED: u32 = 0;
35const BLOCK_NUMBER: u32 = 2;
36
37fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
38	let events = frame_system::Pallet::<T>::events();
39	let system_event: <T as frame_system::Config>::RuntimeEvent = generic_event.into();
40	// compare to the last event record
41	let EventRecord { event, .. } = &events[events.len() - 1];
42	assert_eq!(event, &system_event);
43}
44
45/// Add `n` items to the schedule.
46///
47/// For `resolved`:
48/// - `
49/// - `None`: aborted (hash without preimage)
50/// - `Some(true)`: hash resolves into call if possible, plain call otherwise
51/// - `Some(false)`: plain call
52fn fill_schedule<T: Config>(when: BlockNumberFor<T>, n: u32) -> Result<(), &'static str> {
53	let t = DispatchTime::At(when);
54	let origin: <T as Config>::PalletsOrigin = frame_system::RawOrigin::Root.into();
55	for i in 0..n {
56		let call = make_call::<T>(None);
57		let period = Some(((i + 100).into(), 100));
58		let name = u32_to_name(i);
59		Pallet::<T>::do_schedule_named(name, t, period, 0, origin.clone(), call)?;
60	}
61	ensure!(Agenda::<T>::get(when).len() == n as usize, "didn't fill schedule");
62	Ok(())
63}
64
65fn u32_to_name(i: u32) -> TaskName {
66	i.using_encoded(blake2_256)
67}
68
69fn make_task<T: Config>(
70	periodic: bool,
71	named: bool,
72	signed: bool,
73	maybe_lookup_len: Option<u32>,
74	priority: Priority,
75) -> ScheduledOf<T> {
76	let call = make_call::<T>(maybe_lookup_len);
77	let maybe_periodic = match periodic {
78		true => Some((100u32.into(), 100)),
79		false => None,
80	};
81	let maybe_id = match named {
82		true => Some(u32_to_name(0)),
83		false => None,
84	};
85	let origin = make_origin::<T>(signed);
86	Scheduled { maybe_id, priority, call, maybe_periodic, origin, _phantom: PhantomData }
87}
88
89fn bounded<T: Config>(len: u32) -> Option<BoundedCallOf<T>> {
90	let call =
91		<<T as Config>::RuntimeCall>::from(SystemCall::remark { remark: vec![0; len as usize] });
92	T::Preimages::bound(call).ok()
93}
94
95fn make_call<T: Config>(maybe_lookup_len: Option<u32>) -> BoundedCallOf<T> {
96	let bound = BoundedInline::bound() as u32;
97	let mut len = match maybe_lookup_len {
98		Some(len) => len.min(T::Preimages::MAX_LENGTH as u32 - 2).max(bound) - 3,
99		None => bound.saturating_sub(4),
100	};
101
102	loop {
103		let c = match bounded::<T>(len) {
104			Some(x) => x,
105			None => {
106				len -= 1;
107				continue
108			},
109		};
110		if c.lookup_needed() == maybe_lookup_len.is_some() {
111			break c
112		}
113		if maybe_lookup_len.is_some() {
114			len += 1;
115		} else {
116			if len > 0 {
117				len -= 1;
118			} else {
119				break c
120			}
121		}
122	}
123}
124
125fn make_origin<T: Config>(signed: bool) -> <T as Config>::PalletsOrigin {
126	match signed {
127		true => frame_system::RawOrigin::Signed(account("origin", 0, SEED)).into(),
128		false => frame_system::RawOrigin::Root.into(),
129	}
130}
131
132#[benchmarks]
133mod benchmarks {
134	use super::*;
135
136	// `service_agendas` when no work is done.
137	#[benchmark]
138	fn service_agendas_base() {
139		let now = BLOCK_NUMBER.into();
140		IncompleteSince::<T>::put(now - One::one());
141
142		#[block]
143		{
144			Pallet::<T>::service_agendas(&mut WeightMeter::new(), now, 0);
145		}
146
147		assert_eq!(IncompleteSince::<T>::get(), Some(now - One::one()));
148	}
149
150	// `service_agenda` when no work is done.
151	#[benchmark]
152	fn service_agenda_base(
153		s: Linear<0, { T::MaxScheduledPerBlock::get() }>,
154	) -> Result<(), BenchmarkError> {
155		let now = BLOCK_NUMBER.into();
156		fill_schedule::<T>(now, s)?;
157		assert_eq!(Agenda::<T>::get(now).len() as u32, s);
158
159		#[block]
160		{
161			Pallet::<T>::service_agenda(&mut WeightMeter::new(), true, now, now, 0);
162		}
163
164		assert_eq!(Agenda::<T>::get(now).len() as u32, s);
165
166		Ok(())
167	}
168
169	// `service_task` when the task is a non-periodic, non-named, non-fetched call which is not
170	// dispatched (e.g. due to being overweight).
171	#[benchmark]
172	fn service_task_base() {
173		let now = BLOCK_NUMBER.into();
174		let task = make_task::<T>(false, false, false, None, 0);
175		// prevent any tasks from actually being executed as we only want the surrounding weight.
176		let mut counter = WeightMeter::with_limit(Weight::zero());
177		let _result;
178
179		#[block]
180		{
181			_result = Pallet::<T>::service_task(&mut counter, now, now, 0, true, task);
182		}
183
184		// assert!(_result.is_ok());
185	}
186
187	// `service_task` when the task is a non-periodic, non-named, fetched call (with a known
188	// preimage length) and which is not dispatched (e.g. due to being overweight).
189	#[benchmark(pov_mode = MaxEncodedLen {
190		// Use measured PoV size for the Preimages since we pass in a length witness.
191		Preimage::PreimageFor: Measured
192	})]
193	fn service_task_fetched(
194		s: Linear<{ BoundedInline::bound() as u32 }, { T::Preimages::MAX_LENGTH as u32 }>,
195	) {
196		let now = BLOCK_NUMBER.into();
197		let task = make_task::<T>(false, false, false, Some(s), 0);
198		// prevent any tasks from actually being executed as we only want the surrounding weight.
199		let mut counter = WeightMeter::with_limit(Weight::zero());
200		let _result;
201
202		#[block]
203		{
204			_result = Pallet::<T>::service_task(&mut counter, now, now, 0, true, task);
205		}
206
207		// assert!(result.is_ok());
208	}
209
210	// `service_task` when the task is a non-periodic, named, non-fetched call which is not
211	// dispatched (e.g. due to being overweight).
212	#[benchmark]
213	fn service_task_named() {
214		let now = BLOCK_NUMBER.into();
215		let task = make_task::<T>(false, true, false, None, 0);
216		// prevent any tasks from actually being executed as we only want the surrounding weight.
217		let mut counter = WeightMeter::with_limit(Weight::zero());
218		let _result;
219
220		#[block]
221		{
222			_result = Pallet::<T>::service_task(&mut counter, now, now, 0, true, task);
223		}
224
225		// assert!(result.is_ok());
226	}
227
228	// `service_task` when the task is a periodic, non-named, non-fetched call which is not
229	// dispatched (e.g. due to being overweight).
230	#[benchmark]
231	fn service_task_periodic() {
232		let now = BLOCK_NUMBER.into();
233		let task = make_task::<T>(true, false, false, None, 0);
234		// prevent any tasks from actually being executed as we only want the surrounding weight.
235		let mut counter = WeightMeter::with_limit(Weight::zero());
236		let _result;
237
238		#[block]
239		{
240			_result = Pallet::<T>::service_task(&mut counter, now, now, 0, true, task);
241		}
242
243		// assert!(result.is_ok());
244	}
245
246	// `execute_dispatch` when the origin is `Signed`, not counting the dispatchable's weight.
247	#[benchmark]
248	fn execute_dispatch_signed() -> Result<(), BenchmarkError> {
249		let mut counter = WeightMeter::new();
250		let origin = make_origin::<T>(true);
251		let call = T::Preimages::realize(&make_call::<T>(None))?.0;
252		let result;
253
254		#[block]
255		{
256			result = Pallet::<T>::execute_dispatch(&mut counter, origin, call);
257		}
258
259		assert!(result.is_ok());
260
261		Ok(())
262	}
263
264	// `execute_dispatch` when the origin is not `Signed`, not counting the dispatchable's weight.
265	#[benchmark]
266	fn execute_dispatch_unsigned() -> Result<(), BenchmarkError> {
267		let mut counter = WeightMeter::new();
268		let origin = make_origin::<T>(false);
269		let call = T::Preimages::realize(&make_call::<T>(None))?.0;
270		let result;
271
272		#[block]
273		{
274			result = Pallet::<T>::execute_dispatch(&mut counter, origin, call);
275		}
276
277		assert!(result.is_ok());
278
279		Ok(())
280	}
281
282	#[benchmark]
283	fn schedule(
284		s: Linear<0, { T::MaxScheduledPerBlock::get() - 1 }>,
285	) -> Result<(), BenchmarkError> {
286		let when = BLOCK_NUMBER.into();
287		let periodic = Some((BlockNumberFor::<T>::one(), 100));
288		let priority = 0;
289		// Essentially a no-op call.
290		let call = Box::new(SystemCall::set_storage { items: vec![] }.into());
291
292		fill_schedule::<T>(when, s)?;
293
294		#[extrinsic_call]
295		_(RawOrigin::Root, when, periodic, priority, call);
296
297		ensure!(Agenda::<T>::get(when).len() == s as usize + 1, "didn't add to schedule");
298
299		Ok(())
300	}
301
302	#[benchmark]
303	fn cancel(s: Linear<1, { T::MaxScheduledPerBlock::get() }>) -> Result<(), BenchmarkError> {
304		let when = BLOCK_NUMBER.into();
305
306		fill_schedule::<T>(when, s)?;
307		assert_eq!(Agenda::<T>::get(when).len(), s as usize);
308		let schedule_origin =
309			T::ScheduleOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
310
311		#[extrinsic_call]
312		_(schedule_origin as SystemOrigin<T>, when, 0);
313
314		ensure!(
315			s == 1 || Lookup::<T>::get(u32_to_name(0)).is_none(),
316			"didn't remove from lookup if more than 1 task scheduled for `when`"
317		);
318		// Removed schedule is NONE
319		ensure!(
320			s == 1 || Agenda::<T>::get(when)[0].is_none(),
321			"didn't remove from schedule if more than 1 task scheduled for `when`"
322		);
323		ensure!(
324			s > 1 || Agenda::<T>::get(when).len() == 0,
325			"remove from schedule if only 1 task scheduled for `when`"
326		);
327
328		Ok(())
329	}
330
331	#[benchmark]
332	fn schedule_named(
333		s: Linear<0, { T::MaxScheduledPerBlock::get() - 1 }>,
334	) -> Result<(), BenchmarkError> {
335		let id = u32_to_name(s);
336		let when = BLOCK_NUMBER.into();
337		let periodic = Some((BlockNumberFor::<T>::one(), 100));
338		let priority = 0;
339		// Essentially a no-op call.
340		let call = Box::new(SystemCall::set_storage { items: vec![] }.into());
341
342		fill_schedule::<T>(when, s)?;
343
344		#[extrinsic_call]
345		_(RawOrigin::Root, id, when, periodic, priority, call);
346
347		ensure!(Agenda::<T>::get(when).len() == s as usize + 1, "didn't add to schedule");
348
349		Ok(())
350	}
351
352	#[benchmark]
353	fn cancel_named(
354		s: Linear<1, { T::MaxScheduledPerBlock::get() }>,
355	) -> Result<(), BenchmarkError> {
356		let when = BLOCK_NUMBER.into();
357
358		fill_schedule::<T>(when, s)?;
359
360		#[extrinsic_call]
361		_(RawOrigin::Root, u32_to_name(0));
362
363		ensure!(
364			s == 1 || Lookup::<T>::get(u32_to_name(0)).is_none(),
365			"didn't remove from lookup if more than 1 task scheduled for `when`"
366		);
367		// Removed schedule is NONE
368		ensure!(
369			s == 1 || Agenda::<T>::get(when)[0].is_none(),
370			"didn't remove from schedule if more than 1 task scheduled for `when`"
371		);
372		ensure!(
373			s > 1 || Agenda::<T>::get(when).len() == 0,
374			"remove from schedule if only 1 task scheduled for `when`"
375		);
376
377		Ok(())
378	}
379
380	#[benchmark]
381	fn schedule_retry(
382		s: Linear<1, { T::MaxScheduledPerBlock::get() }>,
383	) -> Result<(), BenchmarkError> {
384		let when = BLOCK_NUMBER.into();
385
386		fill_schedule::<T>(when, s)?;
387		let name = u32_to_name(s - 1);
388		let address = Lookup::<T>::get(name).unwrap();
389		let period: BlockNumberFor<T> = 1_u32.into();
390		let retry_config = RetryConfig { total_retries: 10, remaining: 10, period };
391		Retries::<T>::insert(address, retry_config);
392		let (mut when, index) = address;
393		let task = Agenda::<T>::get(when)[index as usize].clone().unwrap();
394		let mut weight_counter = WeightMeter::with_limit(T::MaximumWeight::get());
395
396		#[block]
397		{
398			Pallet::<T>::schedule_retry(
399				&mut weight_counter,
400				when,
401				when,
402				index,
403				&task,
404				retry_config,
405			);
406		}
407
408		when = when + BlockNumberFor::<T>::one();
409		assert_eq!(
410			Retries::<T>::get((when, 0)),
411			Some(RetryConfig { total_retries: 10, remaining: 9, period })
412		);
413
414		Ok(())
415	}
416
417	#[benchmark]
418	fn set_retry() -> Result<(), BenchmarkError> {
419		let s = T::MaxScheduledPerBlock::get();
420		let when = BLOCK_NUMBER.into();
421
422		fill_schedule::<T>(when, s)?;
423		let name = u32_to_name(s - 1);
424		let address = Lookup::<T>::get(name).unwrap();
425		let (when, index) = address;
426		let period = BlockNumberFor::<T>::one();
427
428		#[extrinsic_call]
429		_(RawOrigin::Root, (when, index), 10, period);
430
431		assert_eq!(
432			Retries::<T>::get((when, index)),
433			Some(RetryConfig { total_retries: 10, remaining: 10, period })
434		);
435		assert_last_event::<T>(
436			Event::RetrySet { task: address, id: None, period, retries: 10 }.into(),
437		);
438
439		Ok(())
440	}
441
442	#[benchmark]
443	fn set_retry_named() -> Result<(), BenchmarkError> {
444		let s = T::MaxScheduledPerBlock::get();
445		let when = BLOCK_NUMBER.into();
446
447		fill_schedule::<T>(when, s)?;
448		let name = u32_to_name(s - 1);
449		let address = Lookup::<T>::get(name).unwrap();
450		let (when, index) = address;
451		let period = BlockNumberFor::<T>::one();
452
453		#[extrinsic_call]
454		_(RawOrigin::Root, name, 10, period);
455
456		assert_eq!(
457			Retries::<T>::get((when, index)),
458			Some(RetryConfig { total_retries: 10, remaining: 10, period })
459		);
460		assert_last_event::<T>(
461			Event::RetrySet { task: address, id: Some(name), period, retries: 10 }.into(),
462		);
463
464		Ok(())
465	}
466
467	#[benchmark]
468	fn cancel_retry() -> Result<(), BenchmarkError> {
469		let s = T::MaxScheduledPerBlock::get();
470		let when = BLOCK_NUMBER.into();
471
472		fill_schedule::<T>(when, s)?;
473		let name = u32_to_name(s - 1);
474		let address = Lookup::<T>::get(name).unwrap();
475		let (when, index) = address;
476		let period = BlockNumberFor::<T>::one();
477		assert!(Pallet::<T>::set_retry(RawOrigin::Root.into(), (when, index), 10, period).is_ok());
478
479		#[extrinsic_call]
480		_(RawOrigin::Root, (when, index));
481
482		assert!(!Retries::<T>::contains_key((when, index)));
483		assert_last_event::<T>(Event::RetryCancelled { task: address, id: None }.into());
484
485		Ok(())
486	}
487
488	#[benchmark]
489	fn cancel_retry_named() -> Result<(), BenchmarkError> {
490		let s = T::MaxScheduledPerBlock::get();
491		let when = BLOCK_NUMBER.into();
492
493		fill_schedule::<T>(when, s)?;
494		let name = u32_to_name(s - 1);
495		let address = Lookup::<T>::get(name).unwrap();
496		let (when, index) = address;
497		let period = BlockNumberFor::<T>::one();
498		assert!(Pallet::<T>::set_retry_named(RawOrigin::Root.into(), name, 10, period).is_ok());
499
500		#[extrinsic_call]
501		_(RawOrigin::Root, name);
502
503		assert!(!Retries::<T>::contains_key((when, index)));
504		assert_last_event::<T>(Event::RetryCancelled { task: address, id: Some(name) }.into());
505
506		Ok(())
507	}
508
509	impl_benchmark_test_suite! {
510		Pallet,
511		mock::new_test_ext(),
512		mock::Test
513	}
514}