From b72650974d66d45663a6e58dc8b4c78bf2c17e75 Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Wed, 9 Sep 2026 10:45:30 -0500 Subject: [PATCH] feat(bench): regenerar weights deja de ser una promesa y pasa a ser un comando MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Durante meses la cabecera de los cinco weights.rs prometia que regenerar "es mecanico". No lo era: no habia comando, ni plantilla, ni forma de correrlo. La promesa costo lo que cuestan las promesas sin codigo — al medir por primera vez, los 19 extrinsics estaban SUBDECLARADOS, los 19. Tres piezas: · scripts/bench/regen-weights.sh — construye el WASM con runtime-benchmarks y mide los 5 pallets. SKIP_BUILD=1 + WASM= mide un blob YA construido: es el modo del banco de referencia, donde el Pi 5 no compila Substrate sino que recibe el blob que se despliega, de modo que el binario medido y el desplegado son el MISMO. · scripts/bench/origin-weights-template.hbs — emite la forma de la casa (trait + SubstrateWeight + impl para ()) y, dentro, un guard GENERADO a partir de los benchmarks reales que reprueba cualquier proof_size = 0 o ref_time = 0. Generado, no escrito: no puede quedarse corto cuando se anada un extrinsic. · scripts/bench/HEADER.txt — la cabecera que va dentro de cada fichero generado, diciendo que un weight es una latencia y que sin su hardware y su perfil de potencia no se reproduce. El script documenta ademas dos cosas medidas hoy que cuestan horas si no estan escritas: (1) hay que usar frame-omni-bencher 0.15.0, porque la 0.23 trae sp-storage 25 contra el sp-storage 22 de este runtime y TrackedStorageKey no decodifica al cruzar el FFI — falla con "unexpected first byte decoding Option", que no se parece a "version equivocada"; (2) el governor ondemand arranca a 1600 MHz y sube a 2400, asi que las primeras muestras salen ~50% lentas y el weight sale de la rampa. Co-Authored-By: Claude Opus 5 --- scripts/bench/HEADER.txt | 9 ++ scripts/bench/origin-weights-template.hbs | 132 ++++++++++++++++++++++ scripts/bench/regen-weights.sh | 79 +++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 scripts/bench/HEADER.txt create mode 100644 scripts/bench/origin-weights-template.hbs create mode 100755 scripts/bench/regen-weights.sh diff --git a/scripts/bench/HEADER.txt b/scripts/bench/HEADER.txt new file mode 100644 index 0000000..b9a3318 --- /dev/null +++ b/scripts/bench/HEADER.txt @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// Origin Chain — Pilar 4 · Charon +// +// ATENCION AL LEER ESTOS NUMEROS: un weight es una LATENCIA, y una latencia sin +// el hardware y el perfil de potencia que la produjeron no se puede reproducir. +// La cabecera dice HOSTNAME y CPU: creele a eso y a nada mas. Un weight medido +// en un portatil y cobrado en un Pi 5 es mispricing estructural, solo que +// invisible. Los numeros CANONICOS de mainnet salen del banco de referencia +// (Pi 5, governor y aislamiento declarados), no de la maquina de quien compila. diff --git a/scripts/bench/origin-weights-template.hbs b/scripts/bench/origin-weights-template.hbs new file mode 100644 index 0000000..11addbe --- /dev/null +++ b/scripts/bench/origin-weights-template.hbs @@ -0,0 +1,132 @@ +{{header}} +//! Weights AUTOGENERADOS para `{{pallet}}` — NO editar a mano. +//! +//! Regenerar: `scripts/bench/regen-weights.sh` (ver ese script para el +//! procedimiento completo y el hardware al que hay que creerle). +//! +//! GENERADO POR SUBSTRATE BENCHMARK CLI VERSION {{version}} +//! FECHA: {{date}}, STEPS: `{{cmd.steps}}`, REPEAT: `{{cmd.repeat}}` +//! HOSTNAME: `{{hostname}}`, CPU: `{{cpuname}}` +//! WASM-EXECUTION: `{{cmd.wasm_execution}}`, DB CACHE: {{cmd.db_cache}} + +// Comando ejecutado: +{{#each args as |arg|}} +// {{arg}} +{{/each}} + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use core::marker::PhantomData; +use frame_support::{traits::Get, weights::constants::RocksDbWeight, weights::Weight}; + +/// Trait de weights para los extrinsics de `{{pallet}}`. +pub trait WeightInfo { + {{#each benchmarks as |benchmark|}} + /// Peso de `{{benchmark.name}}` — MEDIDO sobre el WASM, no estimado a mano. + fn {{benchmark.name~}} + ({{~#each benchmark.components as |c|}}{{c.name}}: u32, {{/each~}}) -> Weight; + {{/each}} +} + +/// Weights parametrizados por el `DbWeight` real del runtime. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + {{#each benchmarks as |benchmark|}} + {{#each benchmark.comments as |comment|}} + /// {{comment}} + {{/each}} + {{#each benchmark.component_ranges as |range|}} + /// El rango del componente `{{range.name}}` es `[{{range.min}}, {{range.max}}]`. + {{/each}} + fn {{benchmark.name~}} + ( + {{~#each benchmark.components as |c| ~}} + {{~#if (not c.is_used)}}_{{/if}}{{c.name}}: u32, {{/each~}} + ) -> Weight { + // Proof size en bytes: + // Medido: `{{benchmark.base_recorded_proof_size}}` + // Estimado: `{{benchmark.base_calculated_proof_size}}` + // Tiempo minimo de ejecucion: {{underscore benchmark.min_execution_time}}_000 picosegundos. + Weight::from_parts({{underscore benchmark.base_weight}}, 0) + .saturating_add(Weight::from_parts(0, {{benchmark.base_calculated_proof_size}})) + {{#each benchmark.component_weight as |cw|}} + // Error estandar: {{underscore cw.error}} + .saturating_add(Weight::from_parts({{underscore cw.slope}}, 0).saturating_mul({{cw.name}}.into())) + {{/each}} + {{#if (ne benchmark.base_reads "0")}} + .saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}}_u64)) + {{/if}} + {{#if (ne benchmark.base_writes "0")}} + .saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}}_u64)) + {{/if}} + } + {{/each}} +} + +/// Impl para mocks y tests — `RocksDbWeight` directo, sin necesitar `T`. +impl WeightInfo for () { + {{#each benchmarks as |benchmark|}} + fn {{benchmark.name~}} + ( + {{~#each benchmark.components as |c| ~}} + {{~#if (not c.is_used)}}_{{/if}}{{c.name}}: u32, {{/each~}} + ) -> Weight { + Weight::from_parts({{underscore benchmark.base_weight}}, 0) + .saturating_add(Weight::from_parts(0, {{benchmark.base_calculated_proof_size}})) + {{#each benchmark.component_weight as |cw|}} + .saturating_add(Weight::from_parts({{underscore cw.slope}}, 0).saturating_mul({{cw.name}}.into())) + {{/each}} + {{#if (ne benchmark.base_reads "0")}} + .saturating_add(RocksDbWeight::get().reads({{benchmark.base_reads}}_u64)) + {{/if}} + {{#if (ne benchmark.base_writes "0")}} + .saturating_add(RocksDbWeight::get().writes({{benchmark.base_writes}}_u64)) + {{/if}} + } + {{/each}} +} + +#[cfg(test)] +mod guard { + use super::*; + + /// NINGUN EXTRINSIC PUEDE DECLARAR `proof_size = 0`. + /// + /// El `proof_size` es lo que el block-builder usa para no pasarse del limite + /// de PoV. Un cero no dice «este extrinsic no toca storage»: dice «miento + /// sobre lo que va a ocupar la prueba», y con eso entra en un bloque mucho + /// mas de lo que cabe. + /// + /// El test nace de un error propio y de la peor clase: la cabecera de los + /// weights interinos afirmaba que `T::DbWeight` «puebla tanto ref_time como + /// proof_size … cerrando el hueco». Es falso y se comprueba en la fuente — + /// `sp_weights::RuntimeDbWeight` solo tiene los campos `read` y `write`, y + /// `reads(r)` devuelve `Weight::from_parts(read * r, 0)`. Los interinos + /// arreglaron reads/writes y dejaron el proof_size donde estaba: en 0. La + /// afirmacion de haberlo cerrado sobrevivio mas de un mes sin que ninguna + /// lectura la desmintiera; la desmintio una medicion. + /// + /// Esta lista la GENERA la plantilla a partir de los benchmarks reales, asi + /// que no puede quedarse corta cuando se anada un extrinsic nuevo. + #[test] + fn ningun_weight_declara_proof_size_cero() { + let casos: &[(&str, Weight)] = &[ + {{#each benchmarks as |benchmark|}} + ("{{benchmark.name}}", <() as WeightInfo>::{{benchmark.name}}({{#each benchmark.components as |c|}}1, {{/each}})), + {{/each}} + ]; + for (nombre, w) in casos { + assert!( + w.proof_size() > 0, + "`{nombre}` declara proof_size = 0: miente al block-builder sobre el peso PoV" + ); + assert!( + w.ref_time() > 0, + "`{nombre}` declara ref_time = 0: el extrinsic saldria gratis" + ); + } + } +} diff --git a/scripts/bench/regen-weights.sh b/scripts/bench/regen-weights.sh new file mode 100755 index 0000000..24fd6bd --- /dev/null +++ b/scripts/bench/regen-weights.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# regen-weights.sh — REGENERAR LOS WEIGHTS DE LOS 5 PALLETS CUSTOM +# +# POR QUE EXISTE +# ────────────── +# Durante meses los weights fueron "INTERIM hand-derived" y el fichero prometia +# que regenerarlos "es mecanico". No lo era: no habia comando. Cuando por fin se +# midio (2026-09-09) los 19 extrinsics estaban SUBDECLARADOS — los 19 — y el +# proof_size valia 0 en todos, que es el mismo agujero que los interinos decian +# haber cerrado. La promesa costo lo que cuestan las promesas sin codigo. +# +# QUE HACE +# ──────── +# 1. Construye el runtime WASM con `runtime-benchmarks`. +# 2. Corre `frame-omni-bencher` sobre ESE blob (el mismo que se despliega). +# 3. Reescribe los 5 `weights.rs` con la plantilla de la casa, que emite +# trait + SubstrateWeight + () + un guard generado que reprueba cualquier +# proof_size = 0. +# +# LA VERSION DEL BENCHER NO ES OPCIONAL +# ───────────────────────────────────── +# `frame-omni-bencher 0.15.0`. La ultima (0.23) trae `sp-storage 25` contra el +# `sp-storage 22` de este runtime, y `TrackedStorageKey` no decodifica al cruzar +# la frontera FFI. Falla con "unexpected first byte decoding Option", que no se +# parece en nada a "version equivocada". +# cargo install frame-omni-bencher --version 0.15.0 --locked +# +# DONDE CORRERLO +# ────────────── +# Para NUMEROS CANONICOS: en el banco de referencia (Pi 5), con el governor +# fijado a `performance` y el core aislado. `ondemand` arranca a 1600 MHz y sube +# a 2400: las primeras muestras salen ~50% lentas y el weight sale de la rampa, +# no del silicio. Sin `isolcpus` el aislamiento es una moneda al aire (medido). +# En cualquier otra maquina el resultado es un SMOKE del pipeline, util para ver +# la forma (reads/writes/proof_size, que NO dependen del hardware) e inservible +# como precio. +set -euo pipefail + +cd "$(dirname "$0")/../.." +ROOT="$PWD" +STEPS="${STEPS:-50}" +REPEAT="${REPEAT:-20}" +BENCHER="${BENCHER:-frame-omni-bencher}" +WASM="${WASM:-$ROOT/target/release/wbuild/origin-chain-runtime/origin_chain_runtime.compact.compressed.wasm}" +# SKIP_BUILD=1 mide un blob YA construido. Es el modo del banco de referencia: +# el Pi 5 no compila Substrate, se le lleva el blob que se va a desplegar — asi +# el binario medido y el desplegado son el MISMO, que es la condicion de Laurha. + +command -v "$BENCHER" >/dev/null || { echo "falta $BENCHER — ver cabecera"; exit 1; } +"$BENCHER" --version | grep -q "0\.15\.0" || echo "⚠ el bencher NO es 0.15.0; si falla al decodificar TrackedStorageKey, es esto" + +if [ "${SKIP_BUILD:-0}" = "1" ]; then + echo "═══ 1/2 SALTADO — midiendo blob ya construido ═══" +else + echo "═══ 1/2 construyendo el WASM con runtime-benchmarks ═══" + WASM_BUILD_STD=0 cargo build --release -p origin-chain-runtime --features wasm-build,runtime-benchmarks +fi +echo "blob: $( (shasum -a 256 "$WASM" 2>/dev/null || sha256sum "$WASM") | cut -d' ' -f1)" + +echo "═══ 2/2 midiendo (steps=$STEPS repeat=$REPEAT) ═══" +declare -A DEST=( + [origin_chain_provenance]=crates/origin-chain-provenance/src/weights.rs + [origin_chain_agent_identity]=crates/origin-chain-agent-identity/src/weights.rs + [origin_chain_fees]=crates/origin-chain-fees/src/weights.rs + [origin_chain_emission]=crates/origin-chain-emission/src/weights.rs + [origin_chain_governance]=crates/origin-chain-governance/src/weights.rs +) +for p in "${!DEST[@]}"; do + echo "── $p" + "$BENCHER" v1 benchmark pallet \ + --runtime "$WASM" --genesis-builder-preset development \ + --pallet "$p" --extrinsic "" \ + --steps "$STEPS" --repeat "$REPEAT" \ + --template "$ROOT/scripts/bench/origin-weights-template.hbs" \ + --header "$ROOT/scripts/bench/HEADER.txt" \ + --output "$ROOT/${DEST[$p]}" +done + +echo "═══ listo — ahora: cargo test --workspace ═══"