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

feat(state-viewer): simple gas counter extraction #7733

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

370 changes: 370 additions & 0 deletions core/primitives-core/src/parameter.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions tools/state-viewer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ nearcore = { path = "../../nearcore" }
node-runtime = { path = "../../runtime/runtime" }

[dev-dependencies]
insta.workspace = true
near-client = { path = "../../chain/client" }
testlib = { path = "../../test-utils/testlib" }

Expand Down
20 changes: 20 additions & 0 deletions tools/state-viewer/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use near_primitives::account::id::AccountId;
use near_primitives::hash::CryptoHash;
use near_primitives::sharding::ChunkHash;
use near_primitives::types::{BlockHeight, ShardId};
use near_primitives::version::ProtocolVersion;
use near_store::{Mode, Store};
use nearcore::{load_config, NearConfig};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -70,6 +71,9 @@ pub enum StateViewerSubCommand {
/// View trie structure.
#[clap(alias = "view_trie")]
ViewTrie(ViewTrieCmd),
/// Print the gas profile counters for a function call receipt.
#[clap(alias = "gas_profile")]
GasProfile(GasProfileCmd),
}

impl StateViewerSubCommand {
Expand Down Expand Up @@ -102,6 +106,7 @@ impl StateViewerSubCommand {
StateViewerSubCommand::ApplyTx(cmd) => cmd.run(home_dir, near_config, hot),
StateViewerSubCommand::ApplyReceipt(cmd) => cmd.run(home_dir, near_config, hot),
StateViewerSubCommand::ViewTrie(cmd) => cmd.run(hot),
StateViewerSubCommand::GasProfile(cmd) => cmd.run(near_config, hot),
}
}
}
Expand Down Expand Up @@ -469,3 +474,18 @@ impl ViewTrieCmd {
view_trie(store, hash, self.shard_id, self.shard_version, self.max_depth).unwrap();
}
}

#[derive(Parser)]
pub struct GasProfileCmd {
#[clap(long)]
receipt_id: String,
#[clap(long)]
protocol_version: ProtocolVersion,
}

impl GasProfileCmd {
pub fn run(self, near_config: NearConfig, store: Store) {
let hash = CryptoHash::from_str(&self.receipt_id).expect("invalid receipt hash");
gas_profile(store, near_config, hash, self.protocol_version).unwrap();
}
}
35 changes: 35 additions & 0 deletions tools/state-viewer/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ use near_network::iter_peers_from_store;
use near_primitives::account::id::AccountId;
use near_primitives::block::{Block, BlockHeader};
use near_primitives::hash::CryptoHash;
use near_primitives::runtime::config_store::RuntimeConfigStore;
use near_primitives::shard_layout::ShardUId;
use near_primitives::sharding::ChunkHash;
use near_primitives::state_record::StateRecord;
use near_primitives::trie_key::TrieKey;
use near_primitives::types::chunk_extra::ChunkExtra;
use near_primitives::types::{BlockHeight, ShardId, StateRoot};
use near_primitives::version::ProtocolVersion;
use near_primitives_core::types::Gas;
use near_store::test_utils::create_test_store;
use near_store::Trie;
Expand Down Expand Up @@ -818,3 +820,36 @@ pub(crate) fn view_trie(
trie.print_recursive(&mut std::io::stdout().lock(), &hash, max_depth);
Ok(())
}

pub(crate) fn gas_profile(
store: Store,
near_config: NearConfig,
receipt_id: CryptoHash,
protocol_version: ProtocolVersion,
) -> anyhow::Result<()> {
let chain_store = ChainStore::new(
store,
near_config.genesis.config.genesis_height,
!near_config.client_config.archive,
);
let outcomes = chain_store.get_outcomes_by_id(&receipt_id)?;
let config_store = RuntimeConfigStore::new(None);
let runtime_config = config_store.get_config(protocol_version);

for outcome in outcomes {
let profile = crate::gas_profile::extract_gas_counters(
&outcome.outcome_with_id.outcome,
runtime_config,
);
let mut out = std::io::stdout().lock();
match profile {
Some(profile) => {
writeln!(out, "{profile}")?;
}
None => {
writeln!(out, "No gas profile found")?;
}
}
}
Ok(())
}
146 changes: 146 additions & 0 deletions tools/state-viewer/src/gas_profile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//! State viewer functions to read gas profile information from execution
//! outcomes stored in RocksDB.

use near_primitives::profile::Cost;
use near_primitives::transaction::ExecutionOutcome;
use near_primitives_core::parameter::Parameter;
use node_runtime::config::RuntimeConfig;
use std::collections::BTreeMap;

pub(crate) struct GasFeeCounters {
counters: BTreeMap<Parameter, u64>,
}

pub(crate) fn extract_gas_counters(
outcome: &ExecutionOutcome,
runtime_config: &RuntimeConfig,
) -> Option<GasFeeCounters> {
match &outcome.metadata {
near_primitives::transaction::ExecutionMetadata::V1 => None,
near_primitives::transaction::ExecutionMetadata::V2(meta_data) => {
let mut counters = BTreeMap::new();

for param in Parameter::ext_costs() {
match param.cost().unwrap_or_else(|| panic!("ext cost {param} must have a cost")) {
Cost::ExtCost { ext_cost_kind } => {
let parameter_value =
ext_cost_kind.value(&runtime_config.wasm_config.ext_costs);
let gas = meta_data.get_ext_cost(ext_cost_kind);
if parameter_value != 0 && gas != 0 {
assert_eq!(
0,
gas % parameter_value,
"invalid gas profile for given config"
);
let counter = gas / parameter_value;
*counters.entry(*param).or_default() += counter;
}
}
_ => unreachable!("{param} must be ExtCost"),
};
}

let num_wasm_ops = meta_data[Cost::WasmInstruction]
/ runtime_config.wasm_config.regular_op_cost as u64;
if num_wasm_ops != 0 {
*counters.entry(Parameter::WasmRegularOpCost).or_default() += num_wasm_ops;
}

// TODO: Action costs should also be included.
// This is tricky, however. From just the gas numbers in the profile
// we cannot know the cost is split to parameters. Because base and byte
// costs are all merged. Same for different type of access keys.
// The only viable way right now is go through each action separately and
// recompute the gas cost from scratch. For promises in function
// calls that includes looping through outgoing promises and again
// recomputing the gas costs.
// And of course one has to consider that some actions will be SIR
// and some will not be.
//
// For now it is not clear if implementing this is even worth it.
// Alternatively, we could also make the profile data more detailed.

Some(GasFeeCounters { counters })
}
}
}

impl std::fmt::Display for GasFeeCounters {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (param, counter) in self.counters.iter() {
writeln!(f, "{param:<48} {counter:>16}")?;
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use near_primitives::profile::ProfileData;
use near_primitives::transaction::ExecutionMetadata;
use near_primitives::types::AccountId;
use node_runtime::config::RuntimeConfig;

#[test]
fn test_extract_gas_counters() {
let config = RuntimeConfig::test();
let costs = [
(Parameter::WasmStorageWriteBase, 137),
(Parameter::WasmStorageWriteKeyByte, 4629),
(Parameter::WasmStorageWriteValueByte, 2246),
// note: actions are not included in profile, yet
(Parameter::ActionDeployContractExecution, 2 * 184765750000),
(Parameter::ActionDeployContractSendSir, 2 * 184765750000),
(Parameter::ActionDeployContractPerByteSendSir, 1024 * 6812999),
(Parameter::ActionDeployContractPerByteExecution, 1024 * 64572944),
(Parameter::WasmRegularOpCost, 7000),
];

let outcome = create_execution_outcome(&costs, &config);
let profile = extract_gas_counters(&outcome, &config).expect("no counters returned");

insta::assert_display_snapshot!(profile);
}

fn create_execution_outcome(
costs: &[(Parameter, u64)],
config: &RuntimeConfig,
) -> ExecutionOutcome {
let mut gas_burnt = 0;
let mut profile_data = ProfileData::new();
for &(parameter, value) in costs {
match parameter.cost() {
Some(Cost::ExtCost { ext_cost_kind }) => {
let gas = value * ext_cost_kind.value(&config.wasm_config.ext_costs);
profile_data.add_ext_cost(ext_cost_kind, gas);
gas_burnt += gas;
}
Some(Cost::WasmInstruction) => {
let gas = value * config.wasm_config.regular_op_cost as u64;
profile_data[Cost::WasmInstruction] += gas;
gas_burnt += gas;
}
// Multiplying for actions isn't possible because costs can be
// split into multiple parameters. Caller has to specify exact
// values.
Some(Cost::ActionCost { action_cost_kind }) => {
profile_data.add_action_cost(action_cost_kind, value);
gas_burnt += value;
}
_ => unimplemented!(),
}
}
let metadata = ExecutionMetadata::V2(profile_data);
let account_id: AccountId = "alice.near".to_owned().try_into().unwrap();
ExecutionOutcome {
logs: vec![],
receipt_ids: vec![],
gas_burnt,
tokens_burnt: 0,
executor_id: account_id.clone(),
status: near_primitives::transaction::ExecutionStatus::SuccessValue(vec![]),
metadata,
}
}
}
1 change: 1 addition & 0 deletions tools/state-viewer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod apply_chunk;
pub mod cli;
mod commands;
mod epoch_info;
mod gas_profile;
mod rocksdb_stats;
mod state_dump;
mod tx_dump;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: tools/state-viewer/src/gas_profile.rs
expression: profile
---
wasm_regular_op_cost 7000
wasm_storage_write_base 137
wasm_storage_write_key_byte 4629
wasm_storage_write_value_byte 2246