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
65/// Add `n` items to the schedule at `when`, the last of which is unnamed.
66///
67/// Returns the address of that unnamed task.
68fn fill_schedule_last_unnamed<T: Config>(
69	when: BlockNumberFor<T>,
70	n: u32,
71) -> Result<TaskAddress<BlockNumberFor<T>>, &'static str> {
72	ensure!(n > 0, "need at least one task");
73	fill_schedule::<T>(when, n - 1)?;
74	let origin: <T as Config>::PalletsOrigin = frame_system::RawOrigin::Root.into();
75	let call = make_call::<T>(None);
76	let period = Some(((n - 1 + 100).into(), 100));
77	let address = Pallet::<T>::do_schedule(DispatchTime::At(when), period, 0, origin, call)?;
78	ensure!(Agenda::<T>::get(when).len() == n as usize, "didn't fill schedule");
79	Ok(address)
80}
81
82fn u32_to_name(i: u32) -> TaskName {
83	i.using_encoded(blake2_256)
84}
85
86fn make_task<T: Config>(
87	periodic: bool,
88	named: bool,
89	signed: bool,
90	maybe_lookup_len: Option<u32>,
91	priority: Priority,
92) -> ScheduledOf<T> {
93	let call = make_call::<T>(maybe_lookup_len);
94	let maybe_periodic = match periodic {
95		true => Some((100u32.into(), 100)),
96		false => None,
97	};
98	let maybe_id = match named {
99		true => Some(u32_to_name(0)),
100		false => None,
101	};
102	let origin = make_origin::<T>(signed);
103	Scheduled { maybe_id, priority, call, maybe_periodic, origin, _phantom: PhantomData }
104}
105
106fn bounded<T: Config>(len: u32) -> Option<BoundedCallOf<T>> {
107	let call =
108		<<T as Config>::RuntimeCall>::from(SystemCall::remark { remark: vec![0; len as usize] });
109	T::Preimages::bound(call).ok()
110}
111
112fn make_call<T: Config>(maybe_lookup_len: Option<u32>) -> BoundedCallOf<T> {
113	let bound = BoundedInline::bound() as u32;
114	let mut len = match maybe_lookup_len {
115		Some(len) => len.min(T::Preimages::MAX_LENGTH as u32 - 2).max(bound) - 3,
116		None => bound.saturating_sub(4),
117	};
118
119	loop {
120		let c = match bounded::<T>(len) {
121			Some(x) => x,
122			None => {
123				len -= 1;
124				continue;
125			},
126		};
127		if c.lookup_needed() == maybe_lookup_len.is_some() {
128			break c;
129		}
130		if maybe_lookup_len.is_some() {
131			len += 1;
132		} else {
133			if len > 0 {
134				len -= 1;
135			} else {
136				break c;
137			}
138		}
139	}
140}
141
142fn make_origin<T: Config>(signed: bool) -> <T as Config>::PalletsOrigin {
143	match signed {
144		true => frame_system::RawOrigin::Signed(account("origin", 0, SEED)).into(),
145		false => frame_system::RawOrigin::Root.into(),
146	}
147}
148
149#[benchmarks]
150mod benchmarks {
151	use super::*;
152
153	// `service_agendas` when no work is done.
154	#[benchmark]
155	fn service_agendas_base() {
156		let now = BLOCK_NUMBER.into();
157		IncompleteSince::<T>::put(now - One::one());
158
159		#[block]
160		{
161			Pallet::<T>::service_agendas(&mut WeightMeter::new(), now, 0);
162		}
163
164		assert_eq!(IncompleteSince::<T>::get(), Some(now - One::one()));
165	}
166
167	// `service_agenda` when no work is done.
168	#[benchmark]
169	fn service_agenda_base(
170		s: Linear<0, { T::MaxScheduledPerBlock::get() }>,
171	) -> Result<(), BenchmarkError> {
172		let now = BLOCK_NUMBER.into();
173		fill_schedule::<T>(now, s)?;
174		assert_eq!(Agenda::<T>::get(now).len() as u32, s);
175
176		#[block]
177		{
178			Pallet::<T>::service_agenda(&mut WeightMeter::new(), true, now, now, 0);
179		}
180
181		assert_eq!(Agenda::<T>::get(now).len() as u32, s);
182
183		Ok(())
184	}
185
186	// `service_task` when the task is a non-periodic, non-named, non-fetched call which is not
187	// dispatched (e.g. due to being overweight).
188	#[benchmark]
189	fn service_task_base() {
190		let now = BLOCK_NUMBER.into();
191		let task = make_task::<T>(false, false, false, None, 0);
192		// prevent any tasks from actually being executed as we only want the surrounding weight.
193		let mut counter = WeightMeter::with_limit(Weight::zero());
194		let _result;
195
196		#[block]
197		{
198			_result = Pallet::<T>::service_task(&mut counter, now, now, 0, true, task);
199		}
200
201		// assert!(_result.is_ok());
202	}
203
204	// `service_task` when the task is a non-periodic, non-named, fetched call (with a known
205	// preimage length) and which is not dispatched (e.g. due to being overweight).
206	#[benchmark(pov_mode = MaxEncodedLen {
207		// Use measured PoV size for the Preimages since we pass in a length witness.
208		Preimage::PreimageFor: Measured
209	})]
210	fn service_task_fetched(
211		s: Linear<{ BoundedInline::bound() as u32 }, { T::Preimages::MAX_LENGTH as u32 }>,
212	) {
213		let now = BLOCK_NUMBER.into();
214		let task = make_task::<T>(false, false, false, Some(s), 0);
215		// prevent any tasks from actually being executed as we only want the surrounding weight.
216		let mut counter = WeightMeter::with_limit(Weight::zero());
217		let _result;
218
219		#[block]
220		{
221			_result = Pallet::<T>::service_task(&mut counter, now, now, 0, true, task);
222		}
223
224		// assert!(result.is_ok());
225	}
226
227	// `service_task` when the task is a non-periodic, named, non-fetched call which is not
228	// dispatched (e.g. due to being overweight).
229	#[benchmark]
230	fn service_task_named() {
231		let now = BLOCK_NUMBER.into();
232		let task = make_task::<T>(false, true, false, None, 0);
233		// prevent any tasks from actually being executed as we only want the surrounding weight.
234		let mut counter = WeightMeter::with_limit(Weight::zero());
235		let _result;
236
237		#[block]
238		{
239			_result = Pallet::<T>::service_task(&mut counter, now, now, 0, true, task);
240		}
241
242		// assert!(result.is_ok());
243	}
244
245	// `service_task` when the task is a periodic, non-named, non-fetched call which is not
246	// dispatched (e.g. due to being overweight).
247	#[benchmark]
248	fn service_task_periodic() {
249		let now = BLOCK_NUMBER.into();
250		let task = make_task::<T>(true, false, false, None, 0);
251		// prevent any tasks from actually being executed as we only want the surrounding weight.
252		let mut counter = WeightMeter::with_limit(Weight::zero());
253		let _result;
254
255		#[block]
256		{
257			_result = Pallet::<T>::service_task(&mut counter, now, now, 0, true, task);
258		}
259
260		// assert!(result.is_ok());
261	}
262
263	// `execute_dispatch` when the origin is `Signed`, not counting the dispatchable's weight.
264	#[benchmark]
265	fn execute_dispatch_signed() -> Result<(), BenchmarkError> {
266		let mut counter = WeightMeter::new();
267		let origin = make_origin::<T>(true);
268		let call = T::Preimages::realize(&make_call::<T>(None))?.0;
269		let result;
270
271		#[block]
272		{
273			result = Pallet::<T>::execute_dispatch(&mut counter, origin, call);
274		}
275
276		assert!(result.is_ok());
277
278		Ok(())
279	}
280
281	// `execute_dispatch` when the origin is not `Signed`, not counting the dispatchable's weight.
282	#[benchmark]
283	fn execute_dispatch_unsigned() -> Result<(), BenchmarkError> {
284		let mut counter = WeightMeter::new();
285		let origin = make_origin::<T>(false);
286		let call = T::Preimages::realize(&make_call::<T>(None))?.0;
287		let result;
288
289		#[block]
290		{
291			result = Pallet::<T>::execute_dispatch(&mut counter, origin, call);
292		}
293
294		assert!(result.is_ok());
295
296		Ok(())
297	}
298
299	#[benchmark]
300	fn schedule(
301		s: Linear<0, { T::MaxScheduledPerBlock::get() - 1 }>,
302	) -> Result<(), BenchmarkError> {
303		let when = BLOCK_NUMBER.into();
304		let periodic = Some((BlockNumberFor::<T>::one(), 100));
305		let priority = 0;
306		// Essentially a no-op call.
307		let call = Box::new(SystemCall::set_storage { items: vec![] }.into());
308
309		fill_schedule::<T>(when, s)?;
310
311		#[extrinsic_call]
312		_(RawOrigin::Root, when, periodic, priority, call);
313
314		ensure!(Agenda::<T>::get(when).len() == s as usize + 1, "didn't add to schedule");
315
316		Ok(())
317	}
318
319	#[benchmark]
320	fn cancel(s: Linear<1, { T::MaxScheduledPerBlock::get() }>) -> Result<(), BenchmarkError> {
321		let when = BLOCK_NUMBER.into();
322
323		fill_schedule::<T>(when, s)?;
324		assert_eq!(Agenda::<T>::get(when).len(), s as usize);
325		let schedule_origin =
326			T::ScheduleOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
327
328		#[extrinsic_call]
329		_(schedule_origin as SystemOrigin<T>, when, 0);
330
331		ensure!(
332			s == 1 || Lookup::<T>::get(u32_to_name(0)).is_none(),
333			"didn't remove from lookup if more than 1 task scheduled for `when`"
334		);
335		// Removed schedule is NONE
336		ensure!(
337			s == 1 || Agenda::<T>::get(when)[0].is_none(),
338			"didn't remove from schedule if more than 1 task scheduled for `when`"
339		);
340		ensure!(
341			s > 1 || Agenda::<T>::get(when).len() == 0,
342			"remove from schedule if only 1 task scheduled for `when`"
343		);
344
345		Ok(())
346	}
347
348	#[benchmark]
349	fn schedule_named(
350		s: Linear<0, { T::MaxScheduledPerBlock::get() - 1 }>,
351	) -> Result<(), BenchmarkError> {
352		let id = u32_to_name(s);
353		let when = BLOCK_NUMBER.into();
354		let periodic = Some((BlockNumberFor::<T>::one(), 100));
355		let priority = 0;
356		// Essentially a no-op call.
357		let call = Box::new(SystemCall::set_storage { items: vec![] }.into());
358
359		fill_schedule::<T>(when, s)?;
360
361		#[extrinsic_call]
362		_(RawOrigin::Root, id, when, periodic, priority, call);
363
364		ensure!(Agenda::<T>::get(when).len() == s as usize + 1, "didn't add to schedule");
365
366		Ok(())
367	}
368
369	#[benchmark]
370	fn cancel_named(
371		s: Linear<1, { T::MaxScheduledPerBlock::get() }>,
372	) -> Result<(), BenchmarkError> {
373		let when = BLOCK_NUMBER.into();
374
375		fill_schedule::<T>(when, s)?;
376
377		#[extrinsic_call]
378		_(RawOrigin::Root, u32_to_name(0));
379
380		ensure!(
381			s == 1 || Lookup::<T>::get(u32_to_name(0)).is_none(),
382			"didn't remove from lookup if more than 1 task scheduled for `when`"
383		);
384		// Removed schedule is NONE
385		ensure!(
386			s == 1 || Agenda::<T>::get(when)[0].is_none(),
387			"didn't remove from schedule if more than 1 task scheduled for `when`"
388		);
389		ensure!(
390			s > 1 || Agenda::<T>::get(when).len() == 0,
391			"remove from schedule if only 1 task scheduled for `when`"
392		);
393
394		Ok(())
395	}
396
397	#[benchmark]
398	fn schedule_retry(
399		s: Linear<1, { T::MaxScheduledPerBlock::get() }>,
400	) -> Result<(), BenchmarkError> {
401		let when = BLOCK_NUMBER.into();
402
403		fill_schedule::<T>(when, s)?;
404		let name = u32_to_name(s - 1);
405		let address = Lookup::<T>::get(name).unwrap();
406		let period: BlockNumberFor<T> = 1_u32.into();
407		let retry_config = RetryConfig { total_retries: 10, remaining: 10, period };
408		Retries::<T>::insert(address, retry_config);
409		let (mut when, index) = address;
410		let task = Agenda::<T>::get(when)[index as usize].clone().unwrap();
411		let mut weight_counter = WeightMeter::with_limit(T::MaximumWeight::get());
412
413		#[block]
414		{
415			Pallet::<T>::schedule_retry(
416				&mut weight_counter,
417				when,
418				when,
419				index,
420				&task,
421				retry_config,
422			);
423		}
424
425		when = when + BlockNumberFor::<T>::one();
426		assert_eq!(
427			Retries::<T>::get((when, 0)),
428			Some(RetryConfig { total_retries: 10, remaining: 9, period })
429		);
430
431		Ok(())
432	}
433
434	#[benchmark]
435	fn set_retry() -> Result<(), BenchmarkError> {
436		let s = T::MaxScheduledPerBlock::get();
437		let when = BLOCK_NUMBER.into();
438
439		// `set_retry` only accepts unnamed tasks.
440		let address = fill_schedule_last_unnamed::<T>(when, s)?;
441		let (when, index) = address;
442		let period = BlockNumberFor::<T>::one();
443
444		#[extrinsic_call]
445		_(RawOrigin::Root, (when, index), 10, period);
446
447		assert_eq!(
448			Retries::<T>::get((when, index)),
449			Some(RetryConfig { total_retries: 10, remaining: 10, period })
450		);
451		assert_last_event::<T>(
452			Event::RetrySet { task: address, id: None, period, retries: 10 }.into(),
453		);
454
455		Ok(())
456	}
457
458	#[benchmark]
459	fn set_retry_named() -> Result<(), BenchmarkError> {
460		let s = T::MaxScheduledPerBlock::get();
461		let when = BLOCK_NUMBER.into();
462
463		fill_schedule::<T>(when, s)?;
464		let name = u32_to_name(s - 1);
465		let address = Lookup::<T>::get(name).unwrap();
466		let (when, index) = address;
467		let period = BlockNumberFor::<T>::one();
468
469		#[extrinsic_call]
470		_(RawOrigin::Root, name, 10, period);
471
472		assert_eq!(
473			Retries::<T>::get((when, index)),
474			Some(RetryConfig { total_retries: 10, remaining: 10, period })
475		);
476		assert_last_event::<T>(
477			Event::RetrySet { task: address, id: Some(name), period, retries: 10 }.into(),
478		);
479
480		Ok(())
481	}
482
483	#[benchmark]
484	fn cancel_retry() -> Result<(), BenchmarkError> {
485		let s = T::MaxScheduledPerBlock::get();
486		let when = BLOCK_NUMBER.into();
487
488		fill_schedule::<T>(when, s)?;
489		let name = u32_to_name(s - 1);
490		let address = Lookup::<T>::get(name).unwrap();
491		let (when, index) = address;
492		let period = BlockNumberFor::<T>::one();
493		// `cancel_retry` still accepts named tasks, so keep the worst case: a fully named agenda.
494		assert!(Pallet::<T>::set_retry_named(RawOrigin::Root.into(), name, 10, period).is_ok());
495
496		#[extrinsic_call]
497		_(RawOrigin::Root, (when, index));
498
499		assert!(!Retries::<T>::contains_key((when, index)));
500		assert_last_event::<T>(Event::RetryCancelled { task: address, id: None }.into());
501
502		Ok(())
503	}
504
505	#[benchmark]
506	fn cancel_retry_named() -> Result<(), BenchmarkError> {
507		let s = T::MaxScheduledPerBlock::get();
508		let when = BLOCK_NUMBER.into();
509
510		fill_schedule::<T>(when, s)?;
511		let name = u32_to_name(s - 1);
512		let address = Lookup::<T>::get(name).unwrap();
513		let (when, index) = address;
514		let period = BlockNumberFor::<T>::one();
515		assert!(Pallet::<T>::set_retry_named(RawOrigin::Root.into(), name, 10, period).is_ok());
516
517		#[extrinsic_call]
518		_(RawOrigin::Root, name);
519
520		assert!(!Retries::<T>::contains_key((when, index)));
521		assert_last_event::<T>(Event::RetryCancelled { task: address, id: Some(name) }.into());
522
523		Ok(())
524	}
525
526	impl_benchmark_test_suite! {
527		Pallet,
528		mock::new_test_ext(),
529		mock::Test
530	}
531}