diff --git a/Cargo.lock b/Cargo.lock index 7d43ca259fb59..0a14bf9f624cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12325,6 +12325,7 @@ dependencies = [ name = "pallet-election-provider-multi-block" version = "0.9.0" dependencies = [ + "cumulus-primitives-storage-weight-reclaim", "frame-benchmarking", "frame-election-provider-support", "frame-support", @@ -13632,6 +13633,7 @@ name = "pallet-staking-async" version = "0.1.0" dependencies = [ "anyhow", + "cumulus-primitives-storage-weight-reclaim", "env_logger 0.11.3", "frame-benchmarking", "frame-election-provider-support", diff --git a/substrate/frame/election-provider-multi-block/Cargo.toml b/substrate/frame/election-provider-multi-block/Cargo.toml index 270b51373bee7..fee4108526213 100644 --- a/substrate/frame/election-provider-multi-block/Cargo.toml +++ b/substrate/frame/election-provider-multi-block/Cargo.toml @@ -34,6 +34,8 @@ sp-npos-elections = { workspace = true } sp-runtime = { workspace = true } sp-std = { workspace = true } +cumulus-primitives-storage-weight-reclaim = { workspace = true } + # Optional imports for benchmarking frame-benchmarking = { optional = true, workspace = true } rand = { features = ["alloc", "small_rng"], optional = true, workspace = true } @@ -64,6 +66,7 @@ std = [ "sp-runtime/std", "sp-std/std", "sp-tracing/std", + "cumulus-primitives-storage-weight-reclaim/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", diff --git a/substrate/frame/election-provider-multi-block/src/lib.rs b/substrate/frame/election-provider-multi-block/src/lib.rs index 7869c806fcca3..b5167529116c3 100644 --- a/substrate/frame/election-provider-multi-block/src/lib.rs +++ b/substrate/frame/election-provider-multi-block/src/lib.rs @@ -751,7 +751,20 @@ pub mod pallet { weight_meter.remaining() ); if weight_meter.can_consume(combined_weight) { - combined_exec(weight_meter); + use cumulus_primitives_storage_weight_reclaim::StorageWeightReclaimer; + let mut reclaimer = StorageWeightReclaimer::new(weight_meter); + // pass a dummy weight meter here, as we no longer want it to be consumed. + combined_exec(&mut WeightMeter::new()); + // and consume it here. + weight_meter.consume(combined_weight); + // then try and reclaim what you can. + let _reclaimed = reclaimer.reclaim_with_meter(weight_meter).defensive(); + crate::log!( + debug, + " weight left post refund: {:?}, reclaimed: {:?}", + weight_meter.remaining().proof_size(), + _reclaimed + ); } else { Self::deposit_event(Event::UnexpectedPhaseTransitionOutOfWeight { from: current_phase, diff --git a/substrate/frame/staking-async/Cargo.toml b/substrate/frame/staking-async/Cargo.toml index a852d8e87571b..4043e0456fc55 100644 --- a/substrate/frame/staking-async/Cargo.toml +++ b/substrate/frame/staking-async/Cargo.toml @@ -32,6 +32,8 @@ sp-npos-elections = { workspace = true } sp-runtime = { features = ["serde"], workspace = true } sp-staking = { features = ["serde"], workspace = true } +cumulus-primitives-storage-weight-reclaim = { workspace = true } + # Optional imports for benchmarking frame-benchmarking = { optional = true, workspace = true } @@ -71,6 +73,7 @@ std = [ "sp-runtime/std", "sp-staking/std", "sp-tracing/std", + "cumulus-primitives-storage-weight-reclaim/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", diff --git a/substrate/frame/staking-async/runtimes/papi-tests/.gitignore b/substrate/frame/staking-async/runtimes/papi-tests/.gitignore index fd0c5bcc4e311..e1d424b75e334 100644 --- a/substrate/frame/staking-async/runtimes/papi-tests/.gitignore +++ b/substrate/frame/staking-async/runtimes/papi-tests/.gitignore @@ -37,3 +37,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json rc.json parachain.json miner.log + +./*.html diff --git a/substrate/frame/staking-async/runtimes/papi-tests/src/export-diagram.ts b/substrate/frame/staking-async/runtimes/papi-tests/src/export-diagram.ts new file mode 100644 index 0000000000000..c6164d5beb765 --- /dev/null +++ b/substrate/frame/staking-async/runtimes/papi-tests/src/export-diagram.ts @@ -0,0 +1,313 @@ +import { writeFileSync } from "fs"; +import type { WeightSummary } from "./test-case"; + +/** + * Exports weight data to an interactive HTML chart using Chart.js. + * + * Creates a chart with three lines: + * 1. Sum of authorship data (header + extrinsics + proof) in KB + * 2. Compressed authorship data in KB + * 3. Onchain mandatory proof_size in KB + * + * Also annotates blocks that have matched events. + * + * @param paraSummary - Map of block numbers to weight summary data + * @param outputPath - Path where the HTML file should be saved (default: './weight-diagram.html') + */ +export function exportWeightDiagram( + paraSummary: Map, + outputPath: string = "./weight-diagram.html" +): void { + const blocks = Array.from(paraSummary.keys()).sort((a, b) => a - b); + + // Prepare data arrays + const authorshipSumData: (number | null)[] = []; + const compressedData: (number | null)[] = []; + const onchainMandatoryProofData: number[] = []; + const eventAnnotations: string[] = []; + const blockEventMap: { [key: number]: string } = {}; + + for (const block of blocks) { + const summary = paraSummary.get(block)!; + + // Calculate authorship sum (header + extrinsics + proof) + if (summary.authorshipWeights) { + const sum = + summary.authorshipWeights.header + + summary.authorshipWeights.extrinsics + + summary.authorshipWeights.proof; + authorshipSumData.push(sum); + compressedData.push(summary.authorshipWeights.compressed); + } else { + authorshipSumData.push(null); + compressedData.push(null); + } + + // Onchain mandatory proof_size (convert from bytes to KB) + const mandatoryProofKb = Number(summary.onchainWeights.mandatory.proof_size) / 1024; + onchainMandatoryProofData.push(mandatoryProofKb); + + // Create event annotation if there are matched events + if (summary.matchedEvent.length > 0) { + const events = summary.matchedEvent + .map(e => `${e.module}::${e.event}`) + .join(", "); + eventAnnotations.push(`Block ${block}: ${events}`); + blockEventMap[block] = events; + } + } + + // Generate HTML with Chart.js + const html = ` + + + + + Weight Diagram + + + + + + +
+

Parachain Weight Diagram

+
+ +
+
+ +
+ ${eventAnnotations.length > 0 ? ` +
+

Matched Events

+
    + ${eventAnnotations.map(e => `
  • ${e}
  • `).join('\n\t\t\t\t')} +
+
+ ` : ''} +
+ + +`; + + // Write the HTML file + writeFileSync(outputPath, html, 'utf-8'); + console.log(`Weight diagram exported to ${outputPath}`); +} diff --git a/substrate/frame/staking-async/runtimes/papi-tests/src/test-case.ts b/substrate/frame/staking-async/runtimes/papi-tests/src/test-case.ts index 40466bbc276ca..80771b74fd99b 100644 --- a/substrate/frame/staking-async/runtimes/papi-tests/src/test-case.ts +++ b/substrate/frame/staking-async/runtimes/papi-tests/src/test-case.ts @@ -53,6 +53,12 @@ interface IAuthorshipData { time: number; } +export interface WeightSummary { + authorshipWeights: IAuthorshipData; + onchainWeights: IWeight; + matchedEvent: IEvent[]; +} + /// Print an event. function pe(e: IEvent): string { return `${e.module} ${e.event} ${e.data ? safeJsonStringify(e.data) : "no data"}`; @@ -68,7 +74,7 @@ interface IObservableEvent { export class Observe { e: IObservableEvent; - onPass: () => void = () => {}; + onPass: () => void = () => { }; constructor( chain: Chain, @@ -76,16 +82,15 @@ export class Observe { event: string, dataCheck: ((data: any) => boolean) | undefined = undefined, byBlock: number | undefined = undefined, - onPass: () => void = () => {} + onPass: () => void = () => { } ) { this.e = { chain, module, event, dataCheck, byBlock }; this.onPass = onPass; } toString(): string { - return `Observe(${this.e.chain}, ${this.e.module}, ${this.e.event}, ${ - this.e.dataCheck ? "dataCheck" : "no dataCheck" - }, ${this.e.byBlock ? this.e.byBlock : "no byBlock"})`; + return `Observe(${this.e.chain}, ${this.e.module}, ${this.e.event}, ${this.e.dataCheck ? "dataCheck" : "no dataCheck" + }, ${this.e.byBlock ? this.e.byBlock : "no byBlock"})`; } static on(chain: Chain, mod: string, event: string): ObserveBuilder { @@ -99,7 +104,7 @@ export class ObserveBuilder { private event: string; private dataCheck?: (data: any) => boolean; private byBlockVal?: number; - private onPassCallback: () => void = () => {}; + private onPassCallback: () => void = () => { }; constructor(chain: Chain, module: string, event: string) { this.chain = chain; @@ -147,10 +152,11 @@ export class TestCase { eventSequence: Observe[]; onKill: () => void; allowPerChainInterleavedEvents: boolean = false; - private resolveTestPromise: (outcome: EventOutcome) => void = () => {}; + summary: Map = new Map(); + private resolveTestPromise: (outcome: EventOutcome) => void = () => { }; /// See `example.test.ts` for more info. - constructor(e: Observe[], interleave: boolean = false, onKill: () => void = () => {}) { + constructor(e: Observe[], interleave: boolean = false, onKill: () => void = () => { }) { this.eventSequence = e; this.onKill = onKill; this.allowPerChainInterleavedEvents = interleave; @@ -251,15 +257,28 @@ export class TestCase { this.resolveTestPromise(EventOutcome.TimedOut); } + if (blockData.chain == Chain.Parachain) { + this.summary.set(blockData.number, { + authorshipWeights: blockData.authorship!, + onchainWeights: blockData.weights, + matchedEvent: [], + }); + } + for (const e of blockData.events) { - this.onEvent(e, blockData); + if (this.onEvent(e, blockData) && blockData.chain == Chain.Parachain) { + const summary = this.summary.get(blockData.number)!; + summary.matchedEvent.push(e); + this.summary.set(blockData.number, summary); + } } } - onEvent(e: IEvent, blockData: IBlock) { + /// Returns `true` if the `e` matched and was processed. + onEvent(e: IEvent, blockData: IBlock): boolean { if (!this.eventSequence.length) { logger.warn(`No events to process for ${blockData.chain}, event: ${pe(e)}`); - return; + return false; } logger.verbose(`${this.commonLog(blockData)} Processing event: ${pe(e)}`); const [primary, maybeSecondary] = this.nextEvent(blockData.chain); @@ -273,19 +292,21 @@ export class TestCase { this.resolveTestPromise(EventOutcome.Done); } else { logger.verbose( - `Next expected event: ${this.eventSequence[0]!.toString()}, remaining events: ${ - this.eventSequence.length + `Next expected event: ${this.eventSequence[0]!.toString()}, remaining events: ${this.eventSequence.length }` ); } + return true; } else if (maybeSecondary && this.match(maybeSecondary.e, e, blockData.chain)) { maybeSecondary.onPass(); this.removeEvent(maybeSecondary); logger.info(`Secondary event passed`); // when we check secondary events, we must have at least 2 items in the list, so no // need to check for the end of list. + return true; } else { logger.debug(`event not relevant`); + return false; } } } diff --git a/substrate/frame/staking-async/runtimes/papi-tests/tests/prune-full-era.test.ts b/substrate/frame/staking-async/runtimes/papi-tests/tests/prune-full-era.test.ts index 7a8f3ffba3967..6ad064f3c739a 100644 --- a/substrate/frame/staking-async/runtimes/papi-tests/tests/prune-full-era.test.ts +++ b/substrate/frame/staking-async/runtimes/papi-tests/tests/prune-full-era.test.ts @@ -7,7 +7,7 @@ import { commonSignedSteps } from "./common"; const PRESET: Presets = Presets.FakeDot; -test( +test.skip( `pruning era with signed (full solution) on ${PRESET}`, async () => { const { killZn, paraLog } = await runPresetUntilLaunched(PRESET); diff --git a/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-dev.test.ts b/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-dev.test.ts index 05e14640c94ca..2445c4d96b8d0 100644 --- a/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-dev.test.ts +++ b/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-dev.test.ts @@ -7,6 +7,7 @@ import { GlobalTimeout, } from "../src/utils"; import { commonSignedSteps } from "./common"; +import { exportWeightDiagram } from "../src/export-diagram"; const PRESET: Presets = Presets.FakeDev; test( @@ -26,6 +27,7 @@ test( ); const outcome = await runTest(testCase, apis, paraLog); + exportWeightDiagram(testCase.summary, `./signed-dev.html`); expect(outcome).toEqual(EventOutcome.Done); }, { timeout: GlobalTimeout } diff --git a/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-dot.test.ts b/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-dot.test.ts index efc62999eebe4..748d65eb7c3f5 100644 --- a/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-dot.test.ts +++ b/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-dot.test.ts @@ -4,6 +4,7 @@ import { runPresetUntilLaunched, spawnMiner } from "../src/cmd"; import { EventOutcome, runTest, TestCase } from "../src/test-case"; import { getApis, GlobalTimeout} from "../src/utils"; import { commonSignedSteps } from "./common"; +import { exportWeightDiagram } from "../src/export-diagram"; const PRESET: Presets = Presets.FakeDot; @@ -25,6 +26,7 @@ test( ); const outcome = await runTest(testCase, apis, paraLog); + exportWeightDiagram(testCase.summary, `./signed-dot.html`); expect(outcome).toEqual(EventOutcome.Done); }, { timeout: GlobalTimeout } diff --git a/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-ksm.test.ts b/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-ksm.test.ts index ad42d1683337d..7cf9bbdfa1329 100644 --- a/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-ksm.test.ts +++ b/substrate/frame/staking-async/runtimes/papi-tests/tests/signed-ksm.test.ts @@ -4,6 +4,7 @@ import { runPresetUntilLaunched, spawnMiner } from "../src/cmd"; import { EventOutcome, runTest, TestCase } from "../src/test-case"; import { getApis, GlobalTimeout } from "../src/utils"; import { commonSignedSteps } from "./common"; +import { exportWeightDiagram } from "../src/export-diagram"; const PRESET: Presets = Presets.FakeKsm; @@ -25,6 +26,7 @@ test( ); const outcome = await runTest(testCase, apis, paraLog); + exportWeightDiagram(testCase.summary, `./signed-ksm.html`); expect(outcome).toEqual(EventOutcome.Done); }, { timeout: GlobalTimeout } diff --git a/substrate/frame/staking-async/src/pallet/mod.rs b/substrate/frame/staking-async/src/pallet/mod.rs index 9600b992feb56..68610652520ff 100644 --- a/substrate/frame/staking-async/src/pallet/mod.rs +++ b/substrate/frame/staking-async/src/pallet/mod.rs @@ -1491,7 +1491,20 @@ pub mod pallet { ); if weight_meter.can_consume(weight) { - exec(weight_meter); + use cumulus_primitives_storage_weight_reclaim::StorageWeightReclaimer; + let mut reclaimer = StorageWeightReclaimer::new(weight_meter); + // pass a dummy weight meter here, as we no longer want it to be consumed. + exec(&mut WeightMeter::new()); + // and consume it here. + weight_meter.consume(weight); + // then try and reclaim what you can. + let _reclaimed = reclaimer.reclaim_with_meter(weight_meter).defensive(); + crate::log!( + debug, + " weight left post refund: {:?}, reclaimed: {:?}", + weight_meter.remaining().proof_size(), + _reclaimed + ); } else { Self::deposit_event(Event::::Unexpected( UnexpectedKind::PagedElectionOutOfWeight {