From 1dd7886ec8980c89258b68b41997b81b8ae0988c Mon Sep 17 00:00:00 2001 From: steelgeek091 Date: Wed, 11 Sep 2024 20:40:16 +0800 Subject: [PATCH 1/6] Rooch Gas Profiling Implementation enable the gas event recording finish the gas profiling convert linear gas event list to stacked implement flamegraph for execution cargo clippy render html template add the content of the execution trace Put the gas profiling in moveos repo implement ClassifiedGasMeter and SwitchableGasMeter for GasProfiler cargo fmt add function execute_tx_locally add an command line argument for getStates command integrate gas profiler into the tx execution flow --- Cargo.lock | 54 +- Cargo.toml | 6 + crates/rooch-executor/Cargo.toml | 1 + crates/rooch-executor/src/actor/messages.rs | 2 + .../src/actor/reader_executor.rs | 10 +- crates/rooch-executor/src/proxy/mod.rs | 51 +- .../rooch-open-rpc-spec/schemas/openrpc.json | 10 + crates/rooch-rpc-api/src/api/rooch_api.rs | 13 +- .../src/jsonrpc_types/str_view.rs | 17 + crates/rooch-rpc-client/src/lib.rs | 130 ++++- crates/rooch-rpc-client/src/rooch_client.rs | 19 +- .../src/server/rooch_server.rs | 19 +- .../src/service/aggregate_service.rs | 5 +- .../src/service/rpc_service.rs | 30 +- crates/rooch/Cargo.toml | 1 + .../move_cli/commands/run_function.rs | 35 +- crates/rooch/src/commands/resource.rs | 6 +- crates/rooch/src/commands/state.rs | 3 +- crates/rooch/src/lib.rs | 2 + crates/rooch/src/tx_runner.rs | 199 +++++++ crates/testsuite/features/cmd.feature | 12 +- .../moveos-commons/moveos-common/Cargo.toml | 4 + .../moveos-commons/moveos-common/src/lib.rs | 1 + .../moveos-commons/moveos-common/src/types.rs | 59 ++ moveos/moveos-gas-profiling/Cargo.toml | 26 + moveos/moveos-gas-profiling/src/aggregate.rs | 96 ++++ moveos/moveos-gas-profiling/src/erased.rs | 131 +++++ moveos/moveos-gas-profiling/src/flamegraph.rs | 143 +++++ moveos/moveos-gas-profiling/src/lib.rs | 11 + moveos/moveos-gas-profiling/src/log.rs | 129 +++++ moveos/moveos-gas-profiling/src/misc.rs | 15 + moveos/moveos-gas-profiling/src/profiler.rs | 528 ++++++++++++++++++ moveos/moveos-gas-profiling/src/render.rs | 67 +++ moveos/moveos-gas-profiling/src/report.rs | 155 +++++ .../moveos-gas-profiling/templates/index.html | 101 ++++ moveos/moveos-types/src/lib.rs | 1 + moveos/moveos-types/src/state_root_hash.rs | 28 + moveos/moveos-types/src/transaction.rs | 7 - moveos/moveos/Cargo.toml | 2 + moveos/moveos/src/gas/mod.rs | 18 - moveos/moveos/src/gas/table.rs | 36 +- moveos/moveos/src/moveos.rs | 3 +- moveos/moveos/src/vm/moveos_vm.rs | 11 +- moveos/moveos/src/vm/tx_argument_resolver.rs | 2 +- 44 files changed, 2072 insertions(+), 127 deletions(-) create mode 100644 crates/rooch/src/tx_runner.rs create mode 100644 moveos/moveos-commons/moveos-common/src/types.rs create mode 100644 moveos/moveos-gas-profiling/Cargo.toml create mode 100644 moveos/moveos-gas-profiling/src/aggregate.rs create mode 100644 moveos/moveos-gas-profiling/src/erased.rs create mode 100644 moveos/moveos-gas-profiling/src/flamegraph.rs create mode 100644 moveos/moveos-gas-profiling/src/lib.rs create mode 100644 moveos/moveos-gas-profiling/src/log.rs create mode 100644 moveos/moveos-gas-profiling/src/misc.rs create mode 100644 moveos/moveos-gas-profiling/src/profiler.rs create mode 100644 moveos/moveos-gas-profiling/src/render.rs create mode 100644 moveos/moveos-gas-profiling/src/report.rs create mode 100644 moveos/moveos-gas-profiling/templates/index.html create mode 100644 moveos/moveos-types/src/state_root_hash.rs diff --git a/Cargo.lock b/Cargo.lock index fce60843f7..f582097871 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3425,6 +3425,15 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", +] + [[package]] name = "env_logger" version = "0.11.5" @@ -4640,6 +4649,20 @@ dependencies = [ "crunchy", ] +[[package]] +name = "handlebars" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde 1.0.210", + "serde_json", + "thiserror", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -5194,6 +5217,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "321f0f839cd44a4686e9504b0a62b4d69a50b62072144c71c68f5873c167b8d9" dependencies = [ "ahash 0.8.11", + "clap 4.5.17", + "crossbeam-channel", + "crossbeam-utils", + "dashmap 5.5.3", + "env_logger 0.10.2", "indexmap 2.2.6", "is-terminal", "itoa", @@ -6934,7 +6962,9 @@ dependencies = [ "move-transactional-test-runner", "move-vm-runtime", "move-vm-types", + "moveos-common", "moveos-eventbus", + "moveos-gas-profiling", "moveos-object-runtime", "moveos-stdlib", "moveos-store", @@ -6959,6 +6989,9 @@ dependencies = [ "itertools 0.13.0", "libc", "log", + "move-binary-format", + "move-core-types", + "move-vm-types", "serde 1.0.210", ] @@ -6994,6 +7027,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "moveos-gas-profiling" +version = "0.7.2" +dependencies = [ + "anyhow", + "handlebars", + "inferno", + "move-binary-format", + "move-core-types", + "move-vm-types", + "moveos-common", + "moveos-types", + "regex", + "serde_json", + "smallvec", +] + [[package]] name = "moveos-object-runtime" version = "0.7.2" @@ -9377,6 +9427,7 @@ dependencies = [ "moveos-common", "moveos-compiler", "moveos-config", + "moveos-gas-profiling", "moveos-object-runtime", "moveos-stdlib", "moveos-store", @@ -9558,6 +9609,7 @@ dependencies = [ "async-trait", "coerce", "function_name", + "hex", "log", "metrics", "move-core-types", @@ -11897,7 +11949,7 @@ dependencies = [ "backtrace", "clap 4.5.17", "cucumber", - "env_logger", + "env_logger 0.11.5", "futures", "hex", "hmac", diff --git a/Cargo.toml b/Cargo.toml index c192d7a19f..5586256d5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "moveos/moveos-commons/bcs_ext", "moveos/moveos-commons/moveos-common", "moveos/moveos-commons/timeout-join-handler", + "moveos/moveos-commons/moveos-common", "moveos/moveos-compiler", "moveos/moveos-config", "moveos/moveos-object-runtime", @@ -18,6 +19,7 @@ members = [ "moveos/raw-store", "moveos/smt", "moveos/moveos-eventbus", + "moveos/moveos-gas-profiling", "crates/data_verify", "crates/rooch", "crates/rooch-benchmarks", @@ -103,6 +105,7 @@ moveos-object-runtime = { path = "moveos/moveos-object-runtime" } moveos-compiler = { path = "moveos/moveos-compiler" } moveos-eventbus = { path = "moveos/moveos-eventbus" } accumulator = { path = "moveos/moveos-commons/accumulator" } +moveos-gas-profiling = { path = "moveos/moveos-gas-profiling" } # crates for Rooch rooch = { path = "crates/rooch" } @@ -347,6 +350,9 @@ vergen-git2 = { version = "1.0.0", features = ["build", "cargo", "rustc"] } vergen-pretty = "0.3.5" crossbeam-channel = "0.5.13" +inferno = "0.11.14" +handlebars = "4.2.2" + # Note: the BEGIN and END comments below are required for external tooling. Do not remove. # BEGIN MOVE DEPENDENCIES move-abigen = { git = "https://github.com/rooch-network/move", rev = "c05b5f71a24beb498eb743d73be4f69d8d325e62" } diff --git a/crates/rooch-executor/Cargo.toml b/crates/rooch-executor/Cargo.toml index 33d1a84186..e41dfdd3be 100644 --- a/crates/rooch-executor/Cargo.toml +++ b/crates/rooch-executor/Cargo.toml @@ -35,3 +35,4 @@ rooch-types = { workspace = true } rooch-genesis = { workspace = true } rooch-event = { workspace = true } rooch-store = { workspace = true } +hex = "0.4.3" diff --git a/crates/rooch-executor/src/actor/messages.rs b/crates/rooch-executor/src/actor/messages.rs index 2e85ac7ec9..3a844b0e79 100644 --- a/crates/rooch-executor/src/actor/messages.rs +++ b/crates/rooch-executor/src/actor/messages.rs @@ -12,6 +12,7 @@ use moveos_types::moveos_std::event::{AnnotatedEvent, Event, EventID}; use moveos_types::moveos_std::object::ObjectMeta; use moveos_types::state::{AnnotatedState, FieldKey, ObjectState, StateChangeSetExt}; use moveos_types::state_resolver::{AnnotatedStateKV, StateKV}; +use moveos_types::state_root_hash::StateRootHash; use moveos_types::transaction::TransactionExecutionInfo; use moveos_types::transaction::TransactionOutput; use moveos_types::transaction::VerifiedMoveOSTransaction; @@ -99,6 +100,7 @@ impl Message for ExecuteViewFunctionMessage { #[derive(Debug, Serialize, Deserialize)] pub struct StatesMessage { + pub state_root: StateRootHash, pub access_path: AccessPath, } diff --git a/crates/rooch-executor/src/actor/reader_executor.rs b/crates/rooch-executor/src/actor/reader_executor.rs index fc67b2bc5c..e4f6e3821a 100644 --- a/crates/rooch-executor/src/actor/reader_executor.rs +++ b/crates/rooch-executor/src/actor/reader_executor.rs @@ -21,6 +21,7 @@ use moveos_store::transaction_store::TransactionStore; use moveos_store::MoveOSStore; use moveos_types::function_return_value::AnnotatedFunctionResult; use moveos_types::function_return_value::AnnotatedFunctionReturnValue; +use moveos_types::h256::H256; use moveos_types::moveos_std::event::EventHandle; use moveos_types::moveos_std::event::{AnnotatedEvent, Event}; use moveos_types::moveos_std::object::ObjectMeta; @@ -146,7 +147,14 @@ impl Handler for ReaderExecutorActor { msg: StatesMessage, _ctx: &mut ActorContext, ) -> Result>, anyhow::Error> { - let resolver = RootObjectResolver::new(self.root.clone(), &self.moveos_store); + let resolver = if !msg.state_root.is_empty() { + let hex_bytes = hex::decode(msg.state_root.0).expect("decode root state failed"); + let state_root = H256::from_slice(hex_bytes.as_slice()); + let root_object_meta = ObjectMeta::root_metadata(state_root, 55); + RootObjectResolver::new(root_object_meta, &self.moveos_store) + } else { + RootObjectResolver::new(self.root.clone(), &self.moveos_store) + }; resolver.get_states(msg.access_path) } } diff --git a/crates/rooch-executor/src/proxy/mod.rs b/crates/rooch-executor/src/proxy/mod.rs index e88cd073b4..a23116b968 100644 --- a/crates/rooch-executor/src/proxy/mod.rs +++ b/crates/rooch-executor/src/proxy/mod.rs @@ -28,6 +28,7 @@ use moveos_types::moveos_std::object::ObjectMeta; use moveos_types::moveos_std::tx_context::TxContext; use moveos_types::state::{FieldKey, StateChangeSetExt}; use moveos_types::state_resolver::{AnnotatedStateKV, StateKV}; +use moveos_types::state_root_hash::StateRootHash; use moveos_types::transaction::FunctionCall; use moveos_types::transaction::TransactionExecutionInfo; use moveos_types::transaction::TransactionOutput; @@ -116,9 +117,16 @@ impl ExecutorProxy { .await? } - pub async fn get_states(&self, access_path: AccessPath) -> Result>> { + pub async fn get_states( + &self, + access_path: AccessPath, + state_root: StateRootHash, + ) -> Result>> { self.reader_actor - .send(StatesMessage { access_path }) + .send(StatesMessage { + state_root, + access_path, + }) .await? } @@ -261,29 +269,38 @@ impl ExecutorProxy { } pub async fn chain_id(&self) -> Result { - self.get_states(AccessPath::object(ChainID::chain_id_object_id())) - .await? - .into_iter() - .next() - .ok_or_else(|| anyhow::anyhow!("chain id not found")) - .and_then(|state| state.ok_or_else(|| anyhow::anyhow!("chain id not found"))) - .and_then(|state| Ok(state.into_object::()?.value)) + self.get_states( + AccessPath::object(ChainID::chain_id_object_id()), + StateRootHash::empty(), + ) + .await? + .into_iter() + .next() + .ok_or_else(|| anyhow::anyhow!("chain id not found")) + .and_then(|state| state.ok_or_else(|| anyhow::anyhow!("chain id not found"))) + .and_then(|state| Ok(state.into_object::()?.value)) } pub async fn bitcoin_network(&self) -> Result { - self.get_states(AccessPath::object(BitcoinNetwork::object_id())) - .await? - .into_iter() - .next() - .ok_or_else(|| anyhow::anyhow!("bitcoin network not found")) - .and_then(|state| state.ok_or_else(|| anyhow::anyhow!("bitcoin network not found"))) - .and_then(|state| Ok(state.into_object::()?.value)) + self.get_states( + AccessPath::object(BitcoinNetwork::object_id()), + StateRootHash::empty(), + ) + .await? + .into_iter() + .next() + .ok_or_else(|| anyhow::anyhow!("bitcoin network not found")) + .and_then(|state| state.ok_or_else(|| anyhow::anyhow!("bitcoin network not found"))) + .and_then(|state| Ok(state.into_object::()?.value)) } //TODO provide a trait to abstract the async state reader, elemiate the duplicated code bwteen RpcService and Client pub async fn get_sequence_number(&self, address: AccountAddress) -> Result { Ok(self - .get_states(AccessPath::object(Account::account_object_id(address))) + .get_states( + AccessPath::object(Account::account_object_id(address)), + StateRootHash::empty(), + ) .await? .pop() .flatten() diff --git a/crates/rooch-open-rpc-spec/schemas/openrpc.json b/crates/rooch-open-rpc-spec/schemas/openrpc.json index 79ce1698b3..52911d01bb 100644 --- a/crates/rooch-open-rpc-spec/schemas/openrpc.json +++ b/crates/rooch-open-rpc-spec/schemas/openrpc.json @@ -423,6 +423,13 @@ "$ref": "#/components/schemas/moveos_types::access_path::AccessPath" } }, + { + "name": "state_root", + "required": true, + "schema": { + "$ref": "#/components/schemas/moveos_types::state_root_hash::StateRootHash" + } + }, { "name": "state_option", "schema": { @@ -3402,6 +3409,9 @@ "moveos_types::state::FieldKey": { "type": "string" }, + "moveos_types::state_root_hash::StateRootHash": { + "type": "string" + }, "primitive_types::H256": { "type": "string" }, diff --git a/crates/rooch-rpc-api/src/api/rooch_api.rs b/crates/rooch-rpc-api/src/api/rooch_api.rs index d2c24ab904..1c8f93056f 100644 --- a/crates/rooch-rpc-api/src/api/rooch_api.rs +++ b/crates/rooch-rpc-api/src/api/rooch_api.rs @@ -12,11 +12,12 @@ use crate::jsonrpc_types::{ FieldKeyView, FunctionCallView, H256View, IndexerEventPageView, IndexerObjectStatePageView, IndexerStateIDView, ModuleABIView, ObjectIDVecView, ObjectIDView, ObjectStateFilterView, ObjectStateView, QueryOptions, RoochAddressView, StateChangeSetPageView, StateOptions, - StatePageView, StrView, StructTagView, SyncStateFilterView, TransactionWithInfoPageView, - TxOptions, + StatePageView, StateRootHashView, StrView, StructTagView, SyncStateFilterView, + TransactionWithInfoPageView, TxOptions, }; use crate::RpcResult; use jsonrpsee::proc_macros::rpc; +use moveos_types::state_root_hash::StateRootHash; use moveos_types::{access_path::AccessPath, state::FieldKey}; use rooch_open_rpc_macros::open_rpc; @@ -58,6 +59,7 @@ pub trait RoochAPI { async fn get_states( &self, access_path: AccessPathView, + state_root: StateRootHashView, state_option: Option, ) -> RpcResult>>; @@ -91,7 +93,12 @@ pub trait RoochAPI { let key_states = field_key.into_iter().map(FieldKey::from).collect(); let access_path_view = AccessPathView::from(AccessPath::fields(object_id.into(), key_states)); - self.get_states(access_path_view, state_option).await + self.get_states( + access_path_view, + StrView::from(StateRootHash::empty()), + state_option, + ) + .await } /// List Object Fields via ObjectID. diff --git a/crates/rooch-rpc-api/src/jsonrpc_types/str_view.rs b/crates/rooch-rpc-api/src/jsonrpc_types/str_view.rs index d55b6b0b97..a87d2e8d01 100644 --- a/crates/rooch-rpc-api/src/jsonrpc_types/str_view.rs +++ b/crates/rooch-rpc-api/src/jsonrpc_types/str_view.rs @@ -6,6 +6,7 @@ use move_core_types::account_address::AccountAddress; use moveos_types::move_std::string::MoveString; +use moveos_types::state_root_hash::StateRootHash; use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; @@ -325,3 +326,19 @@ where self.0.to_human_readable_string(verbose, indent) } } + +pub type StateRootHashView = StrView; + +impl FromStr for StateRootHashView { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + Ok(Self(StateRootHash(s.to_string()))) + } +} + +impl std::fmt::Display for StateRootHashView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} diff --git a/crates/rooch-rpc-client/src/lib.rs b/crates/rooch-rpc-client/src/lib.rs index 1771a16d78..99cb2ad406 100644 --- a/crates/rooch-rpc-client/src/lib.rs +++ b/crates/rooch-rpc-client/src/lib.rs @@ -1,16 +1,22 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 -use anyhow::Result; +use anyhow::{ensure, Error, Result}; use jsonrpsee::core::client::ClientT; use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; -use move_core_types::language_storage::ModuleId; +use move_core_types::account_address::AccountAddress; +use move_core_types::language_storage::{ModuleId, StructTag}; use move_core_types::metadata::Metadata; -use move_core_types::resolver::ModuleResolver; +use move_core_types::resolver::{ModuleResolver, ResourceResolver}; use moveos_types::access_path::AccessPath; +use moveos_types::h256::H256; use moveos_types::move_std::string::MoveString; +use moveos_types::moveos_std::account::Account; use moveos_types::moveos_std::move_module::MoveModule; -use moveos_types::state::ObjectState; +use moveos_types::moveos_std::object::{ObjectID, ObjectMeta, RawField}; +use moveos_types::state::{FieldKey, MoveType, ObjectState}; +use moveos_types::state_resolver::{StateKV, StateResolver, StatelessResolver}; +use moveos_types::state_root_hash::StateRootHash; use moveos_types::{ function_return_value::FunctionResult, module_binding::MoveFunctionCaller, moveos_std::tx_context::TxContext, transaction::FunctionCall, @@ -109,7 +115,10 @@ impl ModuleResolver for &Client { fn get_module(&self, id: &ModuleId) -> Result>> { tokio::task::block_in_place(|| { Handle::current().block_on(async { - let mut states = self.rooch.get_states(AccessPath::module(id)).await?; + let mut states = self + .rooch + .get_states(AccessPath::module(id), StateRootHash::empty()) + .await?; states .pop() .flatten() @@ -123,3 +132,114 @@ impl ModuleResolver for &Client { }) } } + +#[derive(Clone)] +pub struct ClientResolver { + root: ObjectMeta, + client: Client, +} + +impl ClientResolver { + pub fn new(client: Client, root: ObjectMeta) -> Self { + Self { root, client } + } +} + +impl ResourceResolver for ClientResolver { + fn get_resource_with_metadata( + &self, + address: &AccountAddress, + resource_tag: &StructTag, + _metadata: &[Metadata], + ) -> std::result::Result<(Option>, usize), Error> { + let account_object_id = Account::account_object_id(*address); + + let key = FieldKey::derive_resource_key(resource_tag); + let result = self + .get_field(&account_object_id, &key)? + .map(|s| { + ensure!( + s.match_dynamic_field_type(MoveString::type_tag(), resource_tag.clone().into()), + "Resource type mismatch, expected field value type: {:?}, actual: {:?}", + resource_tag, + s.object_type() + ); + let field = RawField::parse_resource_field(&s.value, resource_tag.clone().into())?; + Ok(field.value) + }) + .transpose(); + + match result { + Ok(opt) => { + if let Some(data) = opt { + Ok((Some(data), 0)) + } else { + Ok((None, 0)) + } + } + Err(err) => Err(err), + } + } +} + +impl ModuleResolver for ClientResolver { + fn get_module_metadata(&self, _module_id: &ModuleId) -> Vec { + vec![] + } + + fn get_module(&self, id: &ModuleId) -> std::result::Result>, Error> { + (&self.client).get_module(id) + } +} + +impl StatelessResolver for ClientResolver { + fn get_field_at(&self, state_root: H256, key: &FieldKey) -> Result, Error> { + tokio::task::block_in_place(|| { + Handle::current().block_on(async { + let access_path = AccessPath::object(ObjectID::new(key.0)); + let mut object_state_view_list = self + .client + .rooch + .get_states( + access_path, + StateRootHash::new(hex::encode(state_root.0.as_slice()).as_str()), + ) + .await?; + Ok(object_state_view_list.pop().flatten().map(|state_view| { + let v: ObjectState = state_view.into(); + v + })) + }) + }) + } + + fn list_fields_at( + &self, + state_root: H256, + cursor: Option, + limit: usize, + ) -> Result> { + tokio::task::block_in_place(|| { + Handle::current().block_on(async { + let object_id = ObjectID::new(state_root.0); + let field_cursor = cursor.map(|field_key| field_key.to_hex_literal()); + let fields_states = self + .client + .rooch + .list_field_states(object_id.into(), field_cursor, Some(limit as u64), None) + .await?; + Ok(fields_states + .data + .iter() + .map(|item| StateKV::from((item.field_key.into(), item.state.clone().into()))) + .collect()) + }) + }) + } +} + +impl StateResolver for ClientResolver { + fn root(&self) -> &ObjectMeta { + &self.root + } +} diff --git a/crates/rooch-rpc-client/src/rooch_client.rs b/crates/rooch-rpc-client/src/rooch_client.rs index cdd57c3d46..5190741afe 100644 --- a/crates/rooch-rpc-client/src/rooch_client.rs +++ b/crates/rooch-rpc-client/src/rooch_client.rs @@ -10,6 +10,7 @@ use moveos_types::move_std::string::MoveString; use moveos_types::moveos_std::account::Account; use moveos_types::moveos_std::object::ObjectID; use moveos_types::state::{FieldKey, MoveStructState}; +use moveos_types::state_root_hash::StateRootHash; use moveos_types::{access_path::AccessPath, state::ObjectState, transaction::FunctionCall}; use rooch_rpc_api::api::btc_api::BtcAPIClient; use rooch_rpc_api::api::rooch_api::RoochAPIClient; @@ -92,18 +93,24 @@ impl RoochRpcClient { pub async fn get_states( &self, access_path: AccessPath, + state_root: StateRootHash, ) -> Result>> { - Ok(self.http.get_states(access_path.into(), None).await?) + Ok(self + .http + .get_states(access_path.into(), state_root.into(), None) + .await?) } pub async fn get_decoded_states( &self, access_path: AccessPath, + state_root: StateRootHash, ) -> Result>> { Ok(self .http .get_states( access_path.into(), + state_root.into(), Some(StateOptions::default().decode(true)), ) .await?) @@ -117,6 +124,7 @@ impl RoochRpcClient { .http .get_states( access_path.into(), + StateRootHash::empty().into(), Some(StateOptions::default().decode(true).show_display(true)), ) .await?) @@ -168,9 +176,10 @@ impl RoochRpcClient { pub async fn get_sequence_number(&self, sender: RoochAddress) -> Result { Ok(self - .get_states(AccessPath::object(Account::account_object_id( - sender.into(), - ))) + .get_states( + AccessPath::object(Account::account_object_id(sender.into())), + StateRootHash::empty(), + ) .await? .pop() .flatten() @@ -375,7 +384,7 @@ impl RoochRpcClient { account: RoochAddress, ) -> Result> { let access_path = AccessPath::resource(account.into(), T::struct_tag()); - let mut states = self.get_states(access_path).await?; + let mut states = self.get_states(access_path, StateRootHash::empty()).await?; let state = states.pop().flatten(); if let Some(state) = state { let state = ObjectState::from(state); diff --git a/crates/rooch-rpc-server/src/server/rooch_server.rs b/crates/rooch-rpc-server/src/server/rooch_server.rs index db24f57402..a6b9a22b73 100644 --- a/crates/rooch-rpc-server/src/server/rooch_server.rs +++ b/crates/rooch-rpc-server/src/server/rooch_server.rs @@ -8,6 +8,7 @@ use jsonrpsee::{core::async_trait, RpcModule}; use move_core_types::{ account_address::AccountAddress, identifier::Identifier, language_storage::ModuleId, }; +use moveos_types::state_root_hash::StateRootHash; use moveos_types::{ access_path::AccessPath, h256::H256, @@ -25,8 +26,9 @@ use rooch_rpc_api::jsonrpc_types::{ IndexerEventPageView, IndexerObjectStatePageView, IndexerStateIDView, ModuleABIView, ObjectIDVecView, ObjectStateFilterView, ObjectStateView, QueryOptions, RawTransactionOutputView, RoochAddressView, StateChangeSetPageView, - StateChangeSetWithTxOrderView, StateKVView, StateOptions, StatePageView, StrView, - StructTagView, SyncStateFilterView, TransactionWithInfoPageView, TxOptions, UnitedAddressView, + StateChangeSetWithTxOrderView, StateKVView, StateOptions, StatePageView, StateRootHashView, + StrView, StructTagView, SyncStateFilterView, TransactionWithInfoPageView, TxOptions, + UnitedAddressView, }; use rooch_rpc_api::{ api::rooch_api::RoochAPIServer, @@ -196,6 +198,7 @@ impl RoochAPIServer for RoochServer { async fn get_states( &self, access_path: AccessPathView, + state_root: StateRootHashView, state_option: Option, ) -> RpcResult>> { let state_option = state_option.unwrap_or_default(); @@ -212,7 +215,7 @@ impl RoochAPIServer for RoochServer { let valid_states = states.iter().filter_map(|s| s.as_ref()).collect::>(); let mut valid_display_field_views = self .rpc_service - .get_display_fields_and_render(valid_states.as_slice()) + .get_display_fields_and_render(valid_states.as_slice(), state_root.0) .await?; valid_display_field_views.reverse(); states @@ -236,7 +239,7 @@ impl RoochAPIServer for RoochServer { } } else { self.rpc_service - .get_states(access_path.into()) + .get_states(access_path.into(), state_root.0) .await? .into_iter() .map(|s| s.map(ObjectStateView::from)) @@ -275,7 +278,7 @@ impl RoochAPIServer for RoochServer { if show_display { let display_field_views = self .rpc_service - .get_display_fields_and_render(state_refs.as_slice()) + .get_display_fields_and_render(state_refs.as_slice(), StateRootHash::empty()) .await?; key_states .into_iter() @@ -335,7 +338,7 @@ impl RoochAPIServer for RoochServer { let mut valid_display_field_views = if show_display { let valid_states = states.iter().filter_map(|s| s.as_ref()).collect::>(); self.rpc_service - .get_display_fields_and_render(valid_states.as_slice()) + .get_display_fields_and_render(valid_states.as_slice(), StateRootHash::empty()) .await? } else { vec![] @@ -368,7 +371,7 @@ impl RoochAPIServer for RoochServer { } } else { self.rpc_service - .get_states(access_path) + .get_states(access_path, StateRootHash::empty()) .await? .into_iter() .map(|s| s.map(Into::into)) @@ -621,7 +624,7 @@ impl RoochAPIServer for RoochServer { let access_path = AccessPath::module(&module_id); let module = self .rpc_service - .get_states(access_path) + .get_states(access_path, StateRootHash::empty()) .await? .pop() .flatten(); diff --git a/crates/rooch-rpc-server/src/service/aggregate_service.rs b/crates/rooch-rpc-server/src/service/aggregate_service.rs index 7315bfcd5a..831c7f7b26 100644 --- a/crates/rooch-rpc-server/src/service/aggregate_service.rs +++ b/crates/rooch-rpc-server/src/service/aggregate_service.rs @@ -9,6 +9,7 @@ use moveos_types::access_path::AccessPath; use moveos_types::h256::H256; use moveos_types::moveos_std::object::ObjectID; use moveos_types::state::PlaceholderStruct; +use moveos_types::state_root_hash::StateRootHash; use rooch_rpc_api::jsonrpc_types::account_view::BalanceInfoView; use rooch_rpc_api::jsonrpc_types::CoinInfoView; use rooch_types::address::RoochAddress; @@ -45,7 +46,7 @@ impl AggregateService { .collect(), ); self.rpc_service - .get_states(access_path) + .get_states(access_path, StateRootHash::empty()) .await? .into_iter() .zip(coin_types) @@ -72,7 +73,7 @@ impl AggregateService { ) -> Result>> { let access_path = AccessPath::objects(coin_store_ids); self.rpc_service - .get_states(access_path) + .get_states(access_path, StateRootHash::empty()) .await? .into_iter() .map(|state_opt| state_opt.map(CoinStoreInfo::try_from).transpose()) diff --git a/crates/rooch-rpc-server/src/service/rpc_service.rs b/crates/rooch-rpc-server/src/service/rpc_service.rs index 57f3f9276e..2d5aed97c1 100644 --- a/crates/rooch-rpc-server/src/service/rpc_service.rs +++ b/crates/rooch-rpc-server/src/service/rpc_service.rs @@ -14,6 +14,7 @@ use moveos_types::moveos_std::event::{AnnotatedEvent, Event, EventID}; use moveos_types::moveos_std::object::ObjectID; use moveos_types::state::{AnnotatedState, FieldKey, ObjectState, StateChangeSet}; use moveos_types::state_resolver::{AnnotatedStateKV, StateKV}; +use moveos_types::state_root_hash::StateRootHash; use moveos_types::transaction::{FunctionCall, TransactionExecutionInfo}; use rooch_executor::actor::messages::DryRunTransactionResult; use rooch_executor::proxy::ExecutorProxy; @@ -112,12 +113,18 @@ impl RpcService { Ok(resp) } - pub async fn get_states(&self, access_path: AccessPath) -> Result>> { - self.executor.get_states(access_path).await + pub async fn get_states( + &self, + access_path: AccessPath, + state_root: StateRootHash, + ) -> Result>> { + self.executor.get_states(access_path, state_root).await } pub async fn exists_module(&self, module_id: ModuleId) -> Result { - let mut resp = self.get_states(AccessPath::module(&module_id)).await?; + let mut resp = self + .get_states(AccessPath::module(&module_id), StateRootHash::empty()) + .await?; Ok(resp.pop().flatten().is_some()) } @@ -367,8 +374,9 @@ impl RpcService { .iter() .filter_map(|s| s.as_ref()) .collect::>(); - let valid_display_field_views = - self.get_display_fields_and_render(&valid_states).await?; + let valid_display_field_views = self + .get_display_fields_and_render(&valid_states, StateRootHash::empty()) + .await?; valid_states .iter() .zip(valid_display_field_views) @@ -405,7 +413,7 @@ impl RpcService { } object_states } else { - let states = self.get_states(access_path).await?; + let states = self.get_states(access_path, StateRootHash::empty()).await?; states .into_iter() .zip(indexer_ids) @@ -466,7 +474,7 @@ impl RpcService { let access_path = AccessPath::fields(mapping_object_id, owner_keys); let address_mapping = self - .get_states(access_path) + .get_states(access_path, StateRootHash::empty()) .await? .into_iter() .zip(user_addresses) @@ -489,6 +497,7 @@ impl RpcService { pub async fn get_display_fields_and_render( &self, states: &[&AnnotatedState], + state_root: StateRootHash, ) -> Result>> { let mut display_ids = vec![]; let mut displayable_states = vec![]; @@ -504,7 +513,7 @@ impl RpcService { // get display fields let path = AccessPath::objects(display_ids); let mut display_fields = self - .get_states(path) + .get_states(path, state_root) .await? .into_iter() .map(|option_s| { @@ -596,7 +605,10 @@ impl RpcService { ) -> Result<()> { { let states = self - .get_states(AccessPath::objects(object_ids.clone())) + .get_states( + AccessPath::objects(object_ids.clone()), + StateRootHash::empty(), + ) .await?; let mut remove_object_ids = vec![]; diff --git a/crates/rooch/Cargo.toml b/crates/rooch/Cargo.toml index 7e26fa3170..452a9061eb 100644 --- a/crates/rooch/Cargo.toml +++ b/crates/rooch/Cargo.toml @@ -86,6 +86,7 @@ moveos-compiler = { workspace = true } moveos-config = { workspace = true } accumulator = { workspace = true } metrics = { workspace = true } +moveos-gas-profiling = { workspace = true } framework-builder = { workspace = true } framework-types = { workspace = true } diff --git a/crates/rooch/src/commands/move_cli/commands/run_function.rs b/crates/rooch/src/commands/move_cli/commands/run_function.rs index b677680f00..ac1b2bd69e 100644 --- a/crates/rooch/src/commands/move_cli/commands/run_function.rs +++ b/crates/rooch/src/commands/move_cli/commands/run_function.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::cli_types::{CommandAction, FunctionArg, TransactionOptions, WalletContextOptions}; +use crate::tx_runner::execute_tx_locally_with_gas_profile; use anyhow::Result; use async_trait::async_trait; use clap::Parser; @@ -58,6 +59,10 @@ pub struct RunFunction { /// Return command outputs in json format #[clap(long, default_value = "false")] json: bool, + + /// Run the gas profiler and output html report + #[clap(long, default_value = "false")] + gas_profile: bool, } #[async_trait] @@ -160,9 +165,33 @@ impl CommandAction for RunFunction { )); } - context - .sign_and_execute(sender, action, Some(password), max_gas_amount) - .await? + let tx_execution_result = context + .sign_and_execute( + sender, + action.clone(), + Some(password.clone()), + max_gas_amount, + ) + .await?; + + if self.gas_profile { + let state_root = tx_execution_result + .execution_info + .state_root + .0 + .as_bytes() + .to_vec(); + let tx = context + .sign(sender, action, Some(password), max_gas_amount) + .await?; + execute_tx_locally_with_gas_profile( + state_root, + context.get_client().await?, + tx.data, + ); + } + + tx_execution_result } } }; diff --git a/crates/rooch/src/commands/resource.rs b/crates/rooch/src/commands/resource.rs index 8d913b9c5a..4a7c0e85ef 100644 --- a/crates/rooch/src/commands/resource.rs +++ b/crates/rooch/src/commands/resource.rs @@ -6,6 +6,7 @@ use async_trait::async_trait; use clap::Parser; use move_command_line_common::types::ParsedStructType; use moveos_types::access_path::AccessPath; +use moveos_types::state_root_hash::StateRootHash; use rooch_rpc_api::jsonrpc_types::ObjectStateView; use rooch_types::{address::ParsedAddress, error::RoochResult}; @@ -49,7 +50,10 @@ impl CommandAction> for ResourceCommand { } else { client .rooch - .get_decoded_states(AccessPath::resource(address, resource)) + .get_decoded_states( + AccessPath::resource(address, resource), + StateRootHash::empty(), + ) .await? .pop() .flatten() diff --git a/crates/rooch/src/commands/state.rs b/crates/rooch/src/commands/state.rs index 74e04a0b98..760b577673 100644 --- a/crates/rooch/src/commands/state.rs +++ b/crates/rooch/src/commands/state.rs @@ -5,6 +5,7 @@ use crate::cli_types::{CommandAction, WalletContextOptions}; use async_trait::async_trait; use clap::Parser; use moveos_types::access_path::AccessPath; +use moveos_types::state_root_hash::StateRootHash; use rooch_rpc_api::jsonrpc_types::ObjectStateView; use rooch_types::error::{RoochError, RoochResult}; @@ -42,7 +43,7 @@ impl CommandAction>> for StateCommand { } else { client .rooch - .get_decoded_states(self.access_path) + .get_decoded_states(self.access_path, StateRootHash::empty()) .await .map_err(RoochError::from)? }; diff --git a/crates/rooch/src/lib.rs b/crates/rooch/src/lib.rs index 5f8694c287..bbeaa0f7b2 100644 --- a/crates/rooch/src/lib.rs +++ b/crates/rooch/src/lib.rs @@ -23,6 +23,8 @@ pub mod cli_types; pub mod commands; pub mod utils; +pub mod tx_runner; + #[derive(clap::Parser)] #[clap(author, long_version = LONG_VERSION.as_str(), about, long_about = None, styles = Styles::styled() diff --git a/crates/rooch/src/tx_runner.rs b/crates/rooch/src/tx_runner.rs new file mode 100644 index 0000000000..f77fdc748e --- /dev/null +++ b/crates/rooch/src/tx_runner.rs @@ -0,0 +1,199 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use move_core_types::vm_status::KeptVMStatus::Executed; +use moveos::gas::table::{ + get_gas_schedule_entries, initial_cost_schedule, CostTable, MoveOSGasMeter, +}; +use moveos::moveos::MoveOSConfig; +use moveos::vm::moveos_vm::{MoveOSSession, MoveOSVM}; +use moveos_common::types::ClassifiedGasMeter; +use moveos_gas_profiling::profiler::{new_gas_profiler, ProfileGasMeter}; +use moveos_object_runtime::runtime::ObjectRuntime; +use moveos_types::h256::H256; +use moveos_types::move_std::option::MoveOption; +use moveos_types::moveos_std::gas_schedule::GasScheduleConfig; +use moveos_types::moveos_std::object::ObjectMeta; +use moveos_types::moveos_std::tx_context::TxContext; +use moveos_types::transaction::{MoveAction, VerifiedMoveAction, VerifiedMoveOSTransaction}; +use parking_lot::RwLock; +use rooch_genesis::FrameworksGasParameters; +use rooch_rpc_client::{Client, ClientResolver}; +use rooch_types::address::{BitcoinAddress, MultiChainAddress}; +use rooch_types::framework::auth_validator::{BuiltinAuthValidator, TxValidateResult}; +use rooch_types::framework::system_pre_execute_functions; +use rooch_types::transaction::RoochTransactionData; +use std::rc::Rc; +use std::str::FromStr; + +pub fn execute_tx_locally(state_root_bytes: Vec, client: Client, tx: RoochTransactionData) { + let state_root = H256::from_slice(state_root_bytes.as_slice()); + let root_object_meta = ObjectMeta::root_metadata(state_root, 55); + let client_resolver = ClientResolver::new(client, root_object_meta.clone()); + + let (move_mv, object_runtime, client_resolver, action, cost_table) = + prepare_execute_env(root_object_meta, &client_resolver, tx.clone()); + + let mut gas_meter = MoveOSGasMeter::new(cost_table, GasScheduleConfig::READONLY_MAX_GAS_AMOUNT); + gas_meter.charge_io_write(tx.tx_size()).unwrap(); + + let mut moveos_session = MoveOSSession::new( + move_mv.inner(), + client_resolver, + object_runtime, + gas_meter, + false, + ); + + let system_pre_execute_functions = system_pre_execute_functions(); + + moveos_session + .execute_function_call(system_pre_execute_functions, false) + .expect("system_pre_execute_functions execution failed"); + + moveos_session + .execute_move_action(action) + .expect("execute_move_action failed"); + + let (_tx_context, _raw_output) = moveos_session + .finish_with_extensions(Executed) + .expect("finish_with_extensions failed"); +} + +pub fn execute_tx_locally_with_gas_profile( + state_root_bytes: Vec, + client: Client, + tx: RoochTransactionData, +) { + let state_root = H256::from_slice(state_root_bytes.as_slice()); + let root_object_meta = ObjectMeta::root_metadata(state_root, 55); + let client_resolver = ClientResolver::new(client, root_object_meta.clone()); + + let (move_mv, object_runtime, client_resolver, action, cost_table) = + prepare_execute_env(root_object_meta, &client_resolver, tx.clone()); + + let mut gas_meter = MoveOSGasMeter::new(cost_table, GasScheduleConfig::READONLY_MAX_GAS_AMOUNT); + gas_meter.charge_io_write(tx.tx_size()).unwrap(); + + let mut gas_profiler = new_gas_profiler(tx.clone().action, gas_meter); + + let mut moveos_session = MoveOSSession::new( + move_mv.inner(), + client_resolver, + object_runtime, + gas_profiler.clone(), + false, + ); + + let system_pre_execute_functions = system_pre_execute_functions(); + + moveos_session + .execute_function_call(system_pre_execute_functions, false) + .expect("system_pre_execute_functions execution failed"); + + moveos_session + .execute_move_action(action) + .expect("execute_move_action failed"); + + let (_tx_context, _raw_output) = moveos_session + .finish_with_extensions(Executed) + .expect("finish_with_extensions failed"); + + let gas_profiling_info = gas_profiler.finish(); + + gas_profiling_info + .generate_html_report( + format!("./gas_profiling_{:?}", tx.tx_hash()), + "Rooch Gas Profiling".to_string(), + ) + .unwrap(); +} + +pub fn prepare_execute_env( + state_root: ObjectMeta, + client_resolver: &ClientResolver, + tx: RoochTransactionData, +) -> ( + MoveOSVM, + Rc>, + &ClientResolver, + VerifiedMoveAction, + CostTable, +) { + let gas_entries = + get_gas_schedule_entries(client_resolver).expect("get_gas_schedule_entries failed"); + let cost_table = initial_cost_schedule(gas_entries); + + let verified_tx = + convert_to_verified_tx(state_root.clone(), tx).expect("convert_to_verified_tx failed"); + + let VerifiedMoveOSTransaction { + root: _, + ctx, + action, + } = verified_tx; + + let gas_parameters = + FrameworksGasParameters::load_from_chain(client_resolver).expect("load_from_chain failed"); + + let object_runtime = Rc::new(RwLock::new(ObjectRuntime::new( + ctx, + state_root, + client_resolver, + ))); + + let vm = MoveOSVM::new( + gas_parameters.all_natives(), + MoveOSConfig::default().vm_config, + ) + .expect("create MoveVM failed"); + + (vm, object_runtime, client_resolver, action, cost_table) +} + +fn convert_to_verified_tx( + root: ObjectMeta, + tx_data: RoochTransactionData, +) -> anyhow::Result { + let mut tx_ctx = TxContext::new( + tx_data.sender.into(), + tx_data.sequence_number, + tx_data.max_gas_amount, + tx_data.tx_hash(), + tx_data.tx_size(), + ); + + let mut bitcoin_address = BitcoinAddress::from_str("18cBEMRxXHqzWWCxZNtU91F5sbUNKhL5PX")?; + + let user_multi_chain_address: MultiChainAddress = tx_data.sender.into(); + if user_multi_chain_address.is_bitcoin_address() { + bitcoin_address = user_multi_chain_address.try_into()?; + } + + let dummy_result = TxValidateResult { + auth_validator_id: BuiltinAuthValidator::Bitcoin.flag().into(), + auth_validator: MoveOption::none(), + session_key: MoveOption::none(), + bitcoin_address, + }; + + tx_ctx.add(dummy_result)?; + + let verified_action = match tx_data.action { + MoveAction::Script(script_call) => VerifiedMoveAction::Script { call: script_call }, + MoveAction::Function(function_call) => VerifiedMoveAction::Function { + call: function_call, + bypass_visibility: false, + }, + MoveAction::ModuleBundle(module_bundle) => VerifiedMoveAction::ModuleBundle { + module_bundle, + init_function_modules: vec![], + }, + }; + + Ok(VerifiedMoveOSTransaction::new( + root, + tx_ctx, + verified_action, + )) +} diff --git a/crates/testsuite/features/cmd.feature b/crates/testsuite/features/cmd.feature index dd0fd3002b..af044b9f78 100644 --- a/crates/testsuite/features/cmd.feature +++ b/crates/testsuite/features/cmd.feature @@ -3,18 +3,18 @@ Feature: Rooch CLI integration tests @serial Scenario: rooch rpc test Given a server for rooch_rpc_test - Then cmd: "rpc request --method rooch_getStates --params '["/resource/0x3/0x3::account_coin_store::AutoAcceptCoins",{"decode":true}]' --json" + Then cmd: "rpc request --method rooch_getStates --params '["/resource/0x3/0x3::account_coin_store::AutoAcceptCoins", "", {"decode":true}]' --json" #The object_type contians blank space, so, we should quote it Then assert: "'{{$.rpc[-1][0].object_type}}' == '0x2::object::DynamicField<0x1::string::String, 0x3::account_coin_store::AutoAcceptCoins>'" - Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3",{"decode":true}]' --json" + Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3", "", {"decode":true}]' --json" Then assert: "{{$.rpc[-1][0].object_type}} == '0x2::account::Account'" Then cmd: "rpc request --method rooch_listStates --params '["/resource/0x3", null, null, {"decode":true}]' --json" Then assert: "'{{$.rpc[-1]}}' contains '0x3::account_coin_store::AutoAcceptCoins'" # named_object_id(0x2::timestamp::Timestamp) == 0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca - Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca",{"decode":true}]' --json" + Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca", "", {"decode":true}]' --json" Then assert: "{{$.rpc[-1][0].object_type}} == '0x2::timestamp::Timestamp'" Then assert: "{{$.rpc[-1][0].decoded_value.value.milliseconds}} == 0" - Then cmd: "rpc request --method rooch_getStates --params '["/object/0x2::timestamp::Timestamp",{"decode":true}]' --json" + Then cmd: "rpc request --method rooch_getStates --params '["/object/0x2::timestamp::Timestamp","",{"decode":true}]' --json" Then assert: "{{$.rpc[-1][0].object_type}} == '0x2::timestamp::Timestamp'" Then cmd: "rpc request --method rooch_getObjectStates --params '["0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca", {"decode":false}]' --json" Then cmd: "rpc request --method rooch_getObjectStates --params '["0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca", {"decode":true}]' --json" @@ -58,7 +58,7 @@ Feature: Rooch CLI integration tests Then cmd: "move run --function rooch_framework::gas_coin::faucet_entry --args u256:10000000000 --json" Then assert: "{{$.move[-1].execution_info.status.type}} == executed" - Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca",{"decode":true}]' --json" + Then cmd: "rpc request --method rooch_getStates --params '["/object/0x3a7dfe7a9a5cd608810b5ebd60c7adf7316667b17ad5ae703af301b74310bcca", "", {"decode":true}]' --json" Then assert: "{{$.rpc[-1][0].object_type}} == '0x2::timestamp::Timestamp'" # ensure the tx_timestamp update the global timestamp Then assert: "{{$.rpc[-1][0].decoded_value.value.milliseconds}} != 0" @@ -388,7 +388,7 @@ Feature: Rooch CLI integration tests Then cmd: "state --access-path /object/{{$.event[-1].data[0].decoded_event_data.value.id}}" Then assert: "{{$.state[-1][0].object_type}} == '{{$.address_mapping.default}}::display::ObjectType'" - Then cmd: "rpc request --method rooch_getStates --params '["/object/{{$.event[-1].data[0].decoded_event_data.value.id}}", {"decode": false, "showDisplay": true}]' --json" + Then cmd: "rpc request --method rooch_getStates --params '["/object/{{$.event[-1].data[0].decoded_event_data.value.id}}", "", {"decode": false, "showDisplay": true}]' --json" Then assert: "{{$.rpc[-1][0].display_fields.fields.name}} == test_object" # because the indexer is async update, so sleep 2 seconds to wait indexer update. diff --git a/moveos/moveos-commons/moveos-common/Cargo.toml b/moveos/moveos-commons/moveos-common/Cargo.toml index acabec2767..3f5cdccdc2 100644 --- a/moveos/moveos-commons/moveos-common/Cargo.toml +++ b/moveos/moveos-commons/moveos-common/Cargo.toml @@ -20,3 +20,7 @@ bcs = { workspace = true } log = { workspace = true } itertools = { workspace = true } libc = { workspace = true } + +move-core-types = { workspace = true } +move-binary-format = { workspace = true } +move-vm-types = { workspace = true } \ No newline at end of file diff --git a/moveos/moveos-commons/moveos-common/src/lib.rs b/moveos/moveos-commons/moveos-common/src/lib.rs index ae386d55b4..c51b47321a 100644 --- a/moveos/moveos-commons/moveos-common/src/lib.rs +++ b/moveos/moveos-commons/moveos-common/src/lib.rs @@ -1,4 +1,5 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 +pub mod types; pub mod utils; diff --git a/moveos/moveos-commons/moveos-common/src/types.rs b/moveos/moveos-commons/moveos-common/src/types.rs new file mode 100644 index 0000000000..4aa23c94ac --- /dev/null +++ b/moveos/moveos-commons/moveos-common/src/types.rs @@ -0,0 +1,59 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::errors::PartialVMResult; +use move_core_types::gas_algebra::InternalGas; +use move_vm_types::gas::{GasMeter, UnmeteredGasMeter}; + +#[derive(Debug, Clone)] +pub struct GasStatement { + pub execution_gas_used: InternalGas, + pub storage_gas_used: InternalGas, +} + +pub trait ClassifiedGasMeter { + fn charge_execution(&mut self, gas_cost: u64) -> PartialVMResult<()>; + // fn charge_io_read(&mut self); + fn charge_io_write(&mut self, data_size: u64) -> PartialVMResult<()>; + //fn charge_event(&mut self, events: &[TransactionEvent]) -> PartialVMResult<()>; + //fn charge_change_set(&mut self, change_set: &StateChangeSet) -> PartialVMResult<()>; + fn check_constrains(&self, max_gas_amount: u64) -> PartialVMResult<()>; + fn gas_statement(&self) -> GasStatement; +} + +impl ClassifiedGasMeter for UnmeteredGasMeter { + fn charge_execution(&mut self, _gas_cost: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_io_write(&mut self, _data_size: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn check_constrains(&self, _max_gas_amount: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn gas_statement(&self) -> GasStatement { + GasStatement { + execution_gas_used: InternalGas::from(0), + storage_gas_used: InternalGas::from(0), + } + } +} + +pub trait SwitchableGasMeter: GasMeter { + fn stop_metering(&mut self); + fn start_metering(&mut self); + fn is_metering(&self) -> bool; +} + +impl SwitchableGasMeter for UnmeteredGasMeter { + fn stop_metering(&mut self) {} + + fn start_metering(&mut self) {} + + fn is_metering(&self) -> bool { + false + } +} diff --git a/moveos/moveos-gas-profiling/Cargo.toml b/moveos/moveos-gas-profiling/Cargo.toml new file mode 100644 index 0000000000..3b8b137e49 --- /dev/null +++ b/moveos/moveos-gas-profiling/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "moveos-gas-profiling" + +# Workspace inherited keys +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +publish = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } + +[dependencies] +inferno = { workspace = true } +move-core-types = { workspace = true } +move-vm-types = { workspace = true } +move-binary-format = { workspace = true } +anyhow = { workspace = true } +regex = { workspace = true } +serde_json = { workspace = true } +smallvec = { workspace = true } +handlebars = { workspace = true } + +moveos-types = { workspace = true } +moveos-common = { workspace = true } \ No newline at end of file diff --git a/moveos/moveos-gas-profiling/src/aggregate.rs b/moveos/moveos-gas-profiling/src/aggregate.rs new file mode 100644 index 0000000000..79ad7a2e50 --- /dev/null +++ b/moveos/moveos-gas-profiling/src/aggregate.rs @@ -0,0 +1,96 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::log::ExecutionAndIOCosts; +use crate::log::ExecutionGasEvent; +use crate::render::Render; +use move_core_types::gas_algebra::{GasQuantity, InternalGas}; +use std::collections::{btree_map, BTreeMap}; + +/// Represents an aggregation of execution gas events, including the count and total gas costs for each type of event. +/// +/// The events are sorted by the amount of gas used, from high to low. +#[derive(Debug)] +pub struct AggregatedExecutionGasEvents { + pub ops: Vec<(String, usize, InternalGas)>, +} + +fn insert_or_add( + map: &mut BTreeMap)>, + key: K, + amount: GasQuantity, +) where + K: Ord, +{ + if amount.is_zero() { + return; + } + match map.entry(key) { + btree_map::Entry::Occupied(entry) => { + let r = entry.into_mut(); + r.0 += 1; + r.1 += amount; + } + btree_map::Entry::Vacant(entry) => { + entry.insert((1, amount)); + } + } +} + +fn into_sorted_vec(collection: I) -> Vec<(K, usize, N)> +where + N: Ord, + I: IntoIterator, +{ + let mut v = collection + .into_iter() + .map(|(key, (count, amount))| (key, count, amount)) + .collect::>(); + // Sort in descending order. + v.sort_by(|(_key1, _count1, amount1), (_key2, _count2, amount2)| amount2.cmp(amount1)); + v +} + +impl ExecutionAndIOCosts { + /// Counts the number of hits and aggregates the gas costs for each type of event. + pub fn aggregate_gas_events(&self) -> AggregatedExecutionGasEvents { + use ExecutionGasEvent::*; + + let mut ops = BTreeMap::new(); + let mut storage_reads = BTreeMap::new(); + + for event in self.gas_events() { + match event { + Loc(..) | Call(..) => (), + Bytecode { op, cost } => insert_or_add( + &mut ops, + format!("{:?}", op).to_ascii_lowercase().to_string(), + *cost, + ), + CallNative { + module_id, + fn_name, + ty_args, + cost, + } => insert_or_add( + &mut ops, + format!( + "{}", + Render(&(module_id, fn_name.as_ident_str(), ty_args.as_slice())), + ), + *cost, + ), + LoadResource { + addr: _addr, + ty, + cost, + } => insert_or_add(&mut storage_reads, format!("{}", ty), *cost), + CreateTy { cost } => insert_or_add(&mut ops, "create_ty".to_string(), *cost), + } + } + + AggregatedExecutionGasEvents { + ops: into_sorted_vec(ops), + } + } +} diff --git a/moveos/moveos-gas-profiling/src/erased.rs b/moveos/moveos-gas-profiling/src/erased.rs new file mode 100644 index 0000000000..ba5d42ba45 --- /dev/null +++ b/moveos/moveos-gas-profiling/src/erased.rs @@ -0,0 +1,131 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::log::{CallFrame, ExecutionAndIOCosts, ExecutionGasEvent, FrameName}; +use crate::render::Render; +use move_core_types::gas_algebra::InternalGas; +use std::ops::AddAssign; + +/// Represents a node in a general tree structure where each node is tagged with +/// some text & a numerical value. +#[derive(Clone)] +pub struct Node { + pub text: String, + pub val: N, + pub children: Vec>, +} + +#[derive(Clone)] +pub struct TypeErasedExecutionAndIoCosts { + pub total: InternalGas, + pub tree: Node, +} + +impl Node { + pub fn new(name: impl Into, data: impl Into) -> Self { + Self { + text: name.into(), + val: data.into(), + children: vec![], + } + } + + pub fn new_with_children( + name: impl Into, + data: impl Into, + children: impl IntoIterator, + ) -> Self { + Self { + text: name.into(), + val: data.into(), + children: children.into_iter().collect(), + } + } + + pub fn preorder_traversel(&self, mut f: impl FnMut(usize, &str, &N)) { + let mut stack = vec![(self, 0)]; + + while let Some((node, depth)) = stack.pop() { + f(depth, &node.text, &node.val); + stack.extend(node.children.iter().map(|child| (child, depth + 1)).rev()); + } + } +} + +impl CallFrame { + fn to_erased(&self) -> Node { + let name = match &self.name { + FrameName::Script => "script".to_string(), + FrameName::Function { + module_id, + name, + ty_args, + } => { + format!( + "{}", + Render(&(module_id, name.as_ident_str(), ty_args.as_slice())) + ) + } + }; + + let children = self + .events + .iter() + .map(|event| event.to_erased()) + .collect::>(); + + Node::new_with_children(name, 0, children) + } +} + +impl ExecutionAndIOCosts { + /// Convert the gas log into a type-erased representation. + pub fn to_erased(&self) -> TypeErasedExecutionAndIoCosts { + let nodes = vec![self.call_graph.to_erased()]; + + TypeErasedExecutionAndIoCosts { + total: self.total, + tree: Node::new_with_children("execution & IO (gas unit, full trace)", 0, nodes), + } + } +} + +impl Node +where + N: AddAssign + Copy, +{ + pub fn include_child_costs(&mut self) { + for child in &mut self.children { + child.include_child_costs(); + self.val += child.val; + } + } +} + +impl ExecutionGasEvent { + fn to_erased(&self) -> Node { + use ExecutionGasEvent::*; + + match self { + Loc(offset) => Node::new(format!("@{}", offset), 0), + Bytecode { op, cost } => Node::new(format!("{:?}", op).to_ascii_lowercase(), *cost), + Call(frame) => frame.to_erased(), + CallNative { + module_id, + fn_name, + ty_args, + cost, + } => Node::new( + format!( + "{}", + Render(&(module_id, fn_name.as_ident_str(), ty_args.as_slice())) + ), + *cost, + ), + LoadResource { addr, ty, cost } => { + Node::new(format!("load<{}::{}>", Render(addr), ty), *cost) + } + CreateTy { cost } => Node::new("create_ty", *cost), + } + } +} diff --git a/moveos/moveos-gas-profiling/src/flamegraph.rs b/moveos/moveos-gas-profiling/src/flamegraph.rs new file mode 100644 index 0000000000..a0e57409de --- /dev/null +++ b/moveos/moveos-gas-profiling/src/flamegraph.rs @@ -0,0 +1,143 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::log::{CallFrame, ExecutionAndIOCosts}; +use crate::render::Render; +use inferno::flamegraph::TextTruncateDirection; +use move_core_types::gas_algebra::InternalGas; +use regex::Captures; + +#[allow(dead_code)] +#[derive(Debug)] +struct LineBuffer(Vec); + +#[allow(dead_code)] +impl LineBuffer { + fn new() -> Self { + Self(vec![]) + } + + fn push(&mut self, item: impl AsRef, count: impl Into) { + let count: u64 = count.into(); + + if count > 0 { + self.0.push(format!("{} {}", item.as_ref(), count)); + } + } + + fn into_inner(self) -> Vec { + self.0 + } +} + +#[allow(dead_code)] +impl ExecutionAndIOCosts { + /// Convert the execution gas log into folded stack lines, which can + /// then be used to generate a flamegraph. + fn to_folded_stack_lines(&self) -> Vec { + let mut lines = LineBuffer::new(); + let mut path = vec![]; + + struct Rec<'a> { + lines: &'a mut LineBuffer, + path: &'a mut Vec, + } + + impl<'a> Rec<'a> { + fn visit(&mut self, frame: &CallFrame) { + self.path.push(format!("{}", frame.name)); + + let mut frame_cost = InternalGas::new(0); + + for event in &frame.events { + use crate::log::ExecutionGasEvent; + use ExecutionGasEvent::*; + + match event { + Loc(_) => (), + Bytecode { cost, .. } | CreateTy { cost } => frame_cost += *cost, + Call(inner_frame) => self.visit(inner_frame), + CallNative { + module_id: module, + fn_name, + ty_args, + cost, + } => self.lines.push( + format!( + "{};{}", + self.path(), + Render(&(module, fn_name.as_ident_str(), ty_args.as_slice())), + ), + *cost, + ), + LoadResource { addr, ty, cost } => self.lines.push( + format!("{};load<{}::{}>", self.path(), Render(addr), ty), + *cost, + ), + } + } + + self.lines.push(&self.path(), frame_cost); + self.path.pop(); + } + + fn path(&self) -> String { + self.path.join(";") + } + } + + Rec { + lines: &mut lines, + path: &mut path, + } + .visit(&self.call_graph); + + lines.into_inner() + } + + pub fn to_flamegraph(&self, title: String) -> anyhow::Result>> { + let lines = self.to_folded_stack_lines(); + + if lines.is_empty() { + return Ok(None); + } + + let mut options = inferno::flamegraph::Options::default(); + options.flame_chart = true; + options.text_truncate_direction = TextTruncateDirection::Right; + options.color_diffusion = true; + options.title = title; + + let mut graph_content = vec![]; + inferno::flamegraph::from_lines( + &mut options, + lines.iter().rev().map(|s| s.as_str()), + &mut graph_content, + )?; + let graph_content = String::from_utf8_lossy(&graph_content); + + // Inferno does not allow us to customize some of the text in the resulting graph, + // so we have to do it through regex replacement. + let re = regex::Regex::new("([1-9][0-9]*(,[0-9]+)*) samples") + .expect("should be able to build regex successfully"); + + let graph_content = re.replace_all(&graph_content, |caps: &Captures| { + let count: u64 = caps[1] + .replace(',', "") + .parse() + .expect("should be able parse count as u64"); + + let count_scaled = count as f64 / 1f64; + + format!( + "{} gas units", + crate::misc::strip_trailing_zeros_and_decimal_point(&format!( + "{:.8}", + count_scaled + )) + ) + }); + + Ok(Some(graph_content.as_bytes().to_vec())) + } +} diff --git a/moveos/moveos-gas-profiling/src/lib.rs b/moveos/moveos-gas-profiling/src/lib.rs new file mode 100644 index 0000000000..a64fbefd33 --- /dev/null +++ b/moveos/moveos-gas-profiling/src/lib.rs @@ -0,0 +1,11 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +pub mod aggregate; +pub mod erased; +pub mod flamegraph; +pub mod log; +pub mod misc; +pub mod profiler; +pub mod render; +pub mod report; diff --git a/moveos/moveos-gas-profiling/src/log.rs b/moveos/moveos-gas-profiling/src/log.rs new file mode 100644 index 0000000000..b4391989fe --- /dev/null +++ b/moveos/moveos-gas-profiling/src/log.rs @@ -0,0 +1,129 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use move_binary_format::file_format::CodeOffset; +use move_binary_format::file_format_common::Opcodes; +use move_core_types::account_address::AccountAddress; +use move_core_types::gas_algebra::InternalGas; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::{ModuleId, TypeTag}; +use smallvec::{smallvec, SmallVec}; + +/// An event occurred during the execution of a function, along with the +/// gas cost associated with it, if any. +#[derive(Debug, Clone)] +pub enum ExecutionGasEvent { + /// A special event indicating that the program counter has moved to + /// a specific offset. This is emitted by the branch instructions + /// and is crucial for reconstructing the control flow. + Loc(CodeOffset), + Bytecode { + op: Opcodes, + cost: InternalGas, + }, + Call(CallFrame), + CallNative { + module_id: ModuleId, + fn_name: Identifier, + ty_args: Vec, + cost: InternalGas, + }, + LoadResource { + addr: AccountAddress, + ty: TypeTag, + cost: InternalGas, + }, + CreateTy { + cost: InternalGas, + }, +} + +/// An enum representing the name of a call frame. +/// Could be either a script or a function. +#[derive(Debug, Clone)] +pub enum FrameName { + Script, + Function { + module_id: ModuleId, + name: Identifier, + ty_args: Vec, + }, +} + +/// A struct containing information about a function call, including the name of the +/// function and all gas events that happened during the call. +#[derive(Debug, Clone)] +pub struct CallFrame { + pub name: FrameName, + pub events: Vec, +} + +impl CallFrame { + pub fn new_function(module_id: ModuleId, name: Identifier, ty_args: Vec) -> Self { + Self { + name: FrameName::Function { + module_id, + name, + ty_args, + }, + events: vec![], + } + } + + pub fn new_script() -> Self { + Self { + name: FrameName::Script, + events: vec![], + } + } +} + +#[derive(Debug, Clone)] +pub struct ExecutionAndIOCosts { + pub total: InternalGas, + pub call_graph: CallFrame, +} + +#[derive(Debug, Clone)] +pub struct TransactionGasLog { + pub exec_io: ExecutionAndIOCosts, + pub storage: InternalGas, +} + +pub struct GasEventIter<'a> { + stack: SmallVec<[(&'a CallFrame, usize); 16]>, +} + +impl<'a> Iterator for GasEventIter<'a> { + type Item = &'a ExecutionGasEvent; + + fn next(&mut self) -> Option { + loop { + match self.stack.last_mut() { + None => return None, + Some((frame, pc)) => { + if *pc >= frame.events.len() { + self.stack.pop(); + continue; + } + + let event = &frame.events[*pc]; + *pc += 1; + if let ExecutionGasEvent::Call(child_frame) = event { + self.stack.push((child_frame, 0)) + } + return Some(event); + } + } + } + } +} + +impl ExecutionAndIOCosts { + #[allow(clippy::needless_lifetimes)] + pub fn gas_events<'a>(&'a self) -> GasEventIter<'a> { + GasEventIter { + stack: smallvec![(&self.call_graph, 0)], + } + } +} diff --git a/moveos/moveos-gas-profiling/src/misc.rs b/moveos/moveos-gas-profiling/src/misc.rs new file mode 100644 index 0000000000..585269113b --- /dev/null +++ b/moveos/moveos-gas-profiling/src/misc.rs @@ -0,0 +1,15 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +pub fn strip_trailing_zeros_and_decimal_point(mut s: &str) -> &str { + loop { + match s { + "0" | ".0" => return s, + _ => match s.strip_suffix('0') { + Some(stripped) => s = stripped, + None => break, + }, + } + } + s.strip_suffix('.').unwrap_or(s) +} diff --git a/moveos/moveos-gas-profiling/src/profiler.rs b/moveos/moveos-gas-profiling/src/profiler.rs new file mode 100644 index 0000000000..c402964faf --- /dev/null +++ b/moveos/moveos-gas-profiling/src/profiler.rs @@ -0,0 +1,528 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::log::{CallFrame, ExecutionAndIOCosts, ExecutionGasEvent, FrameName, TransactionGasLog}; +use move_binary_format::file_format::CodeOffset; +use move_binary_format::file_format_common::Opcodes; +use move_core_types::account_address::AccountAddress; +use move_core_types::gas_algebra::{InternalGas, NumArgs, NumBytes}; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::{ModuleId, TypeTag}; +use move_vm_types::gas::{GasMeter, SimpleInstruction}; +use move_vm_types::natives::function::PartialVMResult; +use move_vm_types::views::{TypeView, ValueView}; +use moveos_common::types::{ClassifiedGasMeter, GasStatement, SwitchableGasMeter}; +use moveos_types::transaction::MoveAction; +use std::sync::{Arc, RwLock}; + +#[derive(Debug, Clone)] +pub struct GasProfiler { + base: G, + frames: Arc>>, + metering: bool, +} + +macro_rules! delegate_mut { + ($( + fn $fn: ident $(<$($lt: lifetime),*>)? (&mut self $(, $arg: ident : $ty: ty)* $(,)?) -> $ret_ty: ty; + )*) => { + $(fn $fn $(<$($lt)*>)? (&mut self, $($arg: $ty),*) -> $ret_ty { + self.base.$fn($($arg),*) + })* + }; +} + +macro_rules! record_bytecode { + ($( + $([$op: expr])? + fn $fn: ident $(<$($lt: lifetime),*>)? (&mut self $(, $arg: ident : $ty: ty)* $(,)?) -> PartialVMResult<()>; + )*) => { + $(fn $fn $(<$($lt)*>)? (&mut self, $($arg: $ty),*) -> PartialVMResult<()> { + #[allow(unused)] + use Opcodes::*; + + #[allow(unused)] + let (cost, res) = self.delegate_charge(|base| base.$fn($($arg),*)); + + $( + self.record_bytecode($op, cost); + )? + + res + })* + }; +} + +impl GasProfiler { + pub fn new_function( + base: G, + module_id: ModuleId, + func_name: Identifier, + ty_args: Vec, + ) -> Self { + Self { + base, + frames: Arc::new(RwLock::new(vec![CallFrame::new_function( + module_id, func_name, ty_args, + )])), + metering: true, + } + } +} + +impl GasProfiler { + fn record_gas_event(&mut self, event: ExecutionGasEvent) { + if self.metering { + self.frames + .write() + .unwrap() + .last_mut() + .unwrap() + .events + .push(event); + } + } + + fn record_bytecode(&mut self, op: Opcodes, cost: InternalGas) { + if self.metering { + self.record_gas_event(ExecutionGasEvent::Bytecode { op, cost }) + } + } + + fn record_offset(&mut self, offset: CodeOffset) { + if self.metering { + self.record_gas_event(ExecutionGasEvent::Loc(offset)) + } + } + + /// Delegate the charging call to the base gas meter and measure variation in balance. + fn delegate_charge(&mut self, charge: F) -> (InternalGas, R) + where + F: FnOnce(&mut G) -> R, + { + let old = self.base.balance_internal(); + let res = charge(&mut self.base); + let new = self.base.balance_internal(); + let cost = old.checked_sub(new).expect("gas cost must be non-negative"); + + (cost, res) + } +} + +impl GasMeter for GasProfiler { + delegate_mut! { + // Note: we only use this callback for memory tracking, not for charging gas. + fn charge_ld_const_after_deserialization(&mut self, val: impl ValueView) + -> PartialVMResult<()>; + + // Note: we don't use this to charge gas so no need to record anything. + fn charge_native_function_before_execution( + &mut self, + ty_args: impl ExactSizeIterator + Clone, + args: impl ExactSizeIterator + Clone, + ) -> PartialVMResult<()>; + + // Note: we don't use this to charge gas so no need to record anything. + fn charge_drop_frame( + &mut self, + locals: impl Iterator + Clone, + ) -> PartialVMResult<()>; + } + + record_bytecode! { + [POP] + fn charge_pop(&mut self, popped_val: impl ValueView) -> PartialVMResult<()>; + + [LD_CONST] + fn charge_ld_const(&mut self, size: NumBytes) -> PartialVMResult<()>; + + [COPY_LOC] + fn charge_copy_loc(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + [MOVE_LOC] + fn charge_move_loc(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + [ST_LOC] + fn charge_store_loc(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + [PACK] + fn charge_pack( + &mut self, + is_generic: bool, + args: impl ExactSizeIterator + Clone, + ) -> PartialVMResult<()>; + + [UNPACK] + fn charge_unpack( + &mut self, + is_generic: bool, + args: impl ExactSizeIterator + Clone, + ) -> PartialVMResult<()>; + + [READ_REF] + fn charge_read_ref(&mut self, val: impl ValueView) -> PartialVMResult<()>; + + [WRITE_REF] + fn charge_write_ref( + &mut self, + new_val: impl ValueView, + old_val: impl ValueView, + ) -> PartialVMResult<()>; + + [EQ] + fn charge_eq(&mut self, lhs: impl ValueView, rhs: impl ValueView) -> PartialVMResult<()>; + + [NEQ] + fn charge_neq(&mut self, lhs: impl ValueView, rhs: impl ValueView) -> PartialVMResult<()>; + + [ + match (is_mut, is_generic) { + (false, false) => IMM_BORROW_GLOBAL, + (false, true) => IMM_BORROW_GLOBAL_GENERIC, + (true, false) => MUT_BORROW_GLOBAL, + (true, true) => MUT_BORROW_GLOBAL_GENERIC + } + ] + fn charge_borrow_global( + &mut self, + is_mut: bool, + is_generic: bool, + ty: impl TypeView, + is_success: bool, + ) -> PartialVMResult<()>; + + [if is_generic { EXISTS } else { EXISTS_GENERIC }] + fn charge_exists( + &mut self, + is_generic: bool, + ty: impl TypeView, + exists: bool, + ) -> PartialVMResult<()>; + + [if is_generic { MOVE_FROM } else { MOVE_FROM_GENERIC }] + fn charge_move_from( + &mut self, + is_generic: bool, + ty: impl TypeView, + val: Option, + ) -> PartialVMResult<()>; + + [if is_generic { MOVE_TO } else { MOVE_TO_GENERIC }] + fn charge_move_to( + &mut self, + is_generic: bool, + ty: impl TypeView, + val: impl ValueView, + is_success: bool, + ) -> PartialVMResult<()>; + + [VEC_PACK] + fn charge_vec_pack<'a>( + &mut self, + ty: impl TypeView + 'a, + args: impl ExactSizeIterator + Clone, + ) -> PartialVMResult<()>; + + [VEC_LEN] + fn charge_vec_len(&mut self, ty: impl TypeView) -> PartialVMResult<()>; + + [VEC_IMM_BORROW] + fn charge_vec_borrow( + &mut self, + is_mut: bool, + ty: impl TypeView, + is_success: bool, + ) -> PartialVMResult<()>; + + [VEC_PUSH_BACK] + fn charge_vec_push_back( + &mut self, + ty: impl TypeView, + val: impl ValueView, + ) -> PartialVMResult<()>; + + [VEC_POP_BACK] + fn charge_vec_pop_back( + &mut self, + ty: impl TypeView, + val: Option, + ) -> PartialVMResult<()>; + + [VEC_UNPACK] + fn charge_vec_unpack( + &mut self, + ty: impl TypeView, + expect_num_elements: NumArgs, + elems: impl ExactSizeIterator + Clone, + ) -> PartialVMResult<()>; + + [VEC_SWAP] + fn charge_vec_swap(&mut self, ty: impl TypeView) -> PartialVMResult<()>; + } + + fn balance_internal(&self) -> InternalGas { + self.base.balance_internal() + } + + fn charge_simple_instr(&mut self, instr: SimpleInstruction) -> PartialVMResult<()> { + let (cost, res) = self.delegate_charge(|base| base.charge_simple_instr(instr)); + + self.record_bytecode(instr.to_opcode(), cost); + + // If we encounter a Ret instruction, it means the function has exited, + // and we need to convert the current CallFrame into a GasEvent. + // [call_frame_1, call_frame_2, call_frame_3] + // [call_frame_1, call_frame_2(events: [Bytecode::Op, Call(call_frame_3)])] + if matches!(instr, SimpleInstruction::Ret) && self.frames.read().unwrap().len() > 1 { + let cur_frame = self + .frames + .write() + .unwrap() + .pop() + .expect("frame must exist"); + let mut call_frames = self.frames.write().unwrap(); + let last_frame = call_frames.last_mut().expect("frame must exist"); + last_frame.events.push(ExecutionGasEvent::Call(cur_frame)); + } + + res + } + + fn charge_br_true(&mut self, target_offset: Option) -> PartialVMResult<()> { + let (cost, res) = self.delegate_charge(|base| base.charge_br_true(target_offset)); + + self.record_bytecode(Opcodes::BR_TRUE, cost); + if let Some(offset) = target_offset { + self.record_offset(offset); + } + + res + } + + fn charge_br_false(&mut self, target_offset: Option) -> PartialVMResult<()> { + let (cost, res) = self.delegate_charge(|base| base.charge_br_false(target_offset)); + + self.record_bytecode(Opcodes::BR_FALSE, cost); + if let Some(offset) = target_offset { + self.record_offset(offset); + } + + res + } + + fn charge_branch(&mut self, target_offset: CodeOffset) -> PartialVMResult<()> { + let (cost, res) = self.delegate_charge(|base| base.charge_branch(target_offset)); + + self.record_bytecode(Opcodes::BRANCH, cost); + self.record_offset(target_offset); + + res + } + + fn charge_call( + &mut self, + module_id: &ModuleId, + func_name: &str, + args: impl ExactSizeIterator + Clone, + num_locals: NumArgs, + ) -> PartialVMResult<()> { + let (cost, res) = + self.delegate_charge(|base| base.charge_call(module_id, func_name, args, num_locals)); + + //println!("charge_call {:?}::{:?}", module_id, func_name); + + self.record_bytecode(Opcodes::CALL, cost); + self.frames.write().unwrap().push(CallFrame::new_function( + module_id.clone(), + Identifier::new(func_name).unwrap(), + vec![], + )); + + res + } + + fn charge_call_generic( + &mut self, + module_id: &ModuleId, + func_name: &str, + ty_args: impl ExactSizeIterator + Clone, + args: impl ExactSizeIterator + Clone, + num_locals: NumArgs, + ) -> PartialVMResult<()> { + let ty_tags = ty_args + .clone() + .map(|ty| ty.to_type_tag()) + .collect::>(); + + let (cost, res) = self.delegate_charge(|base| { + base.charge_call_generic(module_id, func_name, ty_args, args, num_locals) + }); + + self.record_bytecode(Opcodes::CALL_GENERIC, cost); + self.frames.write().unwrap().push(CallFrame::new_function( + module_id.clone(), + Identifier::new(func_name).unwrap(), + ty_tags, + )); + + res + } + + fn charge_load_resource( + &mut self, + addr: AccountAddress, + ty: impl TypeView, + val: Option, + bytes_loaded: NumBytes, + ) -> PartialVMResult<()> { + let ty_tag = ty.to_type_tag(); + + let (cost, res) = + self.delegate_charge(|base| base.charge_load_resource(addr, ty, val, bytes_loaded)); + + self.record_gas_event(ExecutionGasEvent::LoadResource { + addr, + ty: ty_tag, + cost, + }); + + res + } + + fn charge_native_function( + &mut self, + amount: InternalGas, + ret_vals: Option + Clone>, + ) -> PartialVMResult<()> { + let (cost, res) = + self.delegate_charge(|base| base.charge_native_function(amount, ret_vals)); + + // Whenever a function gets called, the VM will notify the gas profiler + // via `charge_call/charge_call_generic`. + // + // At this point of time, the gas profiler does not yet have an efficient way to determine + // whether the function is a native or not, so it will blindly create a new frame. + // + // Later when it realizes the function is native, it will transform the original frame + // into a native-specific event that does not contain recursive structures. + let cur = self + .frames + .write() + .unwrap() + .pop() + .expect("frame must exist"); + let (module_id, name, ty_args) = match cur.name { + FrameName::Function { + module_id, + name, + ty_args, + } => (module_id, name, ty_args), + FrameName::Script => unreachable!(), + }; + // The following line of code is needed for correctness. + // + // This is because additional gas events may be produced after the frame has been + // created and these events need to be preserved. + self.frames + .write() + .unwrap() + .last_mut() + .unwrap() + .events + .extend(cur.events); + + self.record_gas_event(ExecutionGasEvent::CallNative { + module_id, + fn_name: name, + ty_args, + cost, + }); + + res + } +} + +pub trait ProfileGasMeter { + fn finish(&mut self) -> TransactionGasLog; +} + +impl ProfileGasMeter for GasProfiler { + fn finish(&mut self) -> TransactionGasLog { + while self.frames.read().unwrap().len() > 1 { + let cur = self + .frames + .write() + .unwrap() + .pop() + .expect("frame must exist"); + let mut call_frames = self.frames.write().unwrap(); + let last = call_frames.last_mut().expect("frame must exist"); + last.events.push(ExecutionGasEvent::Call(cur)); + } + + let exec_io = ExecutionAndIOCosts { + total: self.base.balance_internal(), + call_graph: self + .frames + .write() + .unwrap() + .pop() + .expect("frame must exist"), + }; + + self.stop_metering(); + + TransactionGasLog { + exec_io, + storage: InternalGas::zero(), + } + } +} + +impl ClassifiedGasMeter for GasProfiler { + fn charge_execution(&mut self, _gas_cost: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn charge_io_write(&mut self, _data_size: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn check_constrains(&self, _max_gas_amount: u64) -> PartialVMResult<()> { + Ok(()) + } + + fn gas_statement(&self) -> GasStatement { + GasStatement { + execution_gas_used: InternalGas::zero(), + storage_gas_used: InternalGas::zero(), + } + } +} + +impl SwitchableGasMeter for GasProfiler { + fn stop_metering(&mut self) { + self.metering = false; + } + + fn start_metering(&mut self) { + self.metering = true; + } + + fn is_metering(&self) -> bool { + self.metering + } +} + +pub fn new_gas_profiler(action: MoveAction, base_gas_meter: G) -> GasProfiler { + match action { + MoveAction::Script(_) => unreachable!("Script payload is not supported yet"), + MoveAction::Function(call) => GasProfiler::new_function( + base_gas_meter, + call.function_id.module_id, + call.function_id.function_name, + call.ty_args, + ), + MoveAction::ModuleBundle(_) => unreachable!("ModuleBundle payload is not supported yet"), + } +} diff --git a/moveos/moveos-gas-profiling/src/render.rs b/moveos/moveos-gas-profiling/src/render.rs new file mode 100644 index 0000000000..cb6c239de5 --- /dev/null +++ b/moveos/moveos-gas-profiling/src/render.rs @@ -0,0 +1,67 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::log::FrameName; +use move_core_types::account_address::AccountAddress; +use move_core_types::identifier::IdentStr; +use move_core_types::language_storage::{ModuleId, TypeTag}; +use std::fmt; +use std::fmt::Display; + +/// Wrapper to help render the underlying data in human readable formats that are +/// desirable for textual outputs and flamegraphs. +pub(crate) struct Render<'a, T>(pub &'a T); + +impl<'a> Display for Render<'a, AccountAddress> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let addr_short = self.0.short_str_lossless(); + write!(f, "0x")?; + if addr_short.len() > 4 { + write!(f, "{}..", &addr_short[..4]) + } else { + write!(f, "{}", addr_short) + } + } +} + +impl<'a> Display for Render<'a, ModuleId> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}::{}", Render(self.0.address()), self.0.name()) + } +} + +impl<'a> Display for Render<'a, (&'a ModuleId, &'a IdentStr, &'a [TypeTag])> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}::{}", Render(self.0 .0), self.0 .1)?; + if !self.0 .2.is_empty() { + write!( + f, + "<{}>", + self.0 + .2 + .iter() + .map(|ty| format!("{}", ty)) + .collect::>() + .join(",") + )?; + } + Ok(()) + } +} + +impl Display for FrameName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Script => write!(f, "