diff --git a/Cargo.lock b/Cargo.lock index ac254bb04c..483023e59a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -504,6 +504,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "c-enum" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd17eb909a8c6a894926bfcc3400a4bb0e732f5a57d37b1f14e8b29e329bace8" + [[package]] name = "cc" version = "1.0.83" @@ -1990,6 +1996,15 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -1999,6 +2014,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2135,7 +2159,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -2257,6 +2281,41 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "perf-event-data" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575828d9d7d205188048eb1508560607a03d21eafdbba47b8cade1736c1c28e1" +dependencies = [ + "bitflags 2.4.2", + "c-enum", + "perf-event-open-sys2", +] + +[[package]] +name = "perf-event-open-sys2" +version = "5.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c25955321465255e437600b54296983fab1feac2cd0c38958adeb26dbae49e" +dependencies = [ + "libc", + "memoffset 0.9.1", +] + +[[package]] +name = "perf-event2" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0939b8fad77dfaeb29ebbd35faaeaadbf833167f30975f1b8993bbba09ea0a0f" +dependencies = [ + "bitflags 2.4.2", + "c-enum", + "libc", + "memmap2", + "perf-event-data", + "perf-event-open-sys2", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -3532,6 +3591,7 @@ dependencies = [ "mutants", "nix", "percent-encoding", + "perf-event2", "pox-locking 2.4.0", "prometheus", "proptest", diff --git a/stackslib/Cargo.toml b/stackslib/Cargo.toml index 0b8d920ab3..747aaf16b4 100644 --- a/stackslib/Cargo.toml +++ b/stackslib/Cargo.toml @@ -81,6 +81,10 @@ developer-mode = ["clarity/developer-mode"] monitoring_prom = ["prometheus"] slog_json = ["stacks-common/slog_json", "clarity/slog_json", "pox-locking/slog_json"] testing = ["chrono", "stacks-common/testing", "clarity/testing"] +profiler = ["perf-event2"] + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies] +perf-event2 = { version = "0.7.4", optional = true } [target.'cfg(all(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64"), not(any(target_os="windows"))))'.dependencies] sha2 = { version = "0.10", features = ["asm"] } diff --git a/stackslib/src/net/api/blockreplay.rs b/stackslib/src/net/api/blockreplay.rs index f9b48db20e..dac83bf6a7 100644 --- a/stackslib/src/net/api/blockreplay.rs +++ b/stackslib/src/net/api/blockreplay.rs @@ -22,6 +22,7 @@ use stacks_common::types::net::PeerHost; use stacks_common::util::hash::Sha512Trunc256Sum; use stacks_common::util::secp256k1::MessageSignature; use stacks_common::util::serde_serializers::prefix_hex_codec; +use url::form_urlencoded; use crate::burnchains::Txid; use crate::chainstate::burn::db::sortdb::SortitionDB; @@ -39,10 +40,114 @@ use crate::net::http::{ use crate::net::httpcore::{RPCRequestHandler, StacksHttpResponse}; use crate::net::{Error as NetError, StacksHttpRequest, StacksNodeState}; +#[cfg(all(feature = "profiler", target_os = "linux", target_arch = "x86_64"))] +struct BlockReplayProfiler { + perf_event_cpu_instructions: Option, + perf_event_cpu_cycles: Option, + perf_event_cpu_ref_cycles: Option, +} + +#[cfg(not(all(feature = "profiler", target_os = "linux", target_arch = "x86_64")))] +struct BlockReplayProfiler(); + +#[derive(Default)] +struct BlockReplayProfilerResult { + cpu_instructions: Option, + cpu_cycles: Option, + cpu_ref_cycles: Option, +} + +#[cfg(all(feature = "profiler", target_os = "linux", target_arch = "x86_64"))] +impl BlockReplayProfiler { + fn new() -> Self { + let mut perf_event_cpu_instructions: Option = None; + let mut perf_event_cpu_cycles: Option = None; + let mut perf_event_cpu_ref_cycles: Option = None; + + if let Ok(mut perf_event_cpu_instructions_result) = + perf_event::Builder::new(perf_event::events::Hardware::INSTRUCTIONS).build() + { + if perf_event_cpu_instructions_result.enable().is_ok() { + perf_event_cpu_instructions = Some(perf_event_cpu_instructions_result); + } + } + + if let Ok(mut perf_event_cpu_cycles_result) = + perf_event::Builder::new(perf_event::events::Hardware::CPU_CYCLES).build() + { + if perf_event_cpu_cycles_result.enable().is_ok() { + perf_event_cpu_cycles = Some(perf_event_cpu_cycles_result); + } + } + + if let Ok(mut perf_event_cpu_ref_cycles_result) = + perf_event::Builder::new(perf_event::events::Hardware::REF_CPU_CYCLES).build() + { + if perf_event_cpu_ref_cycles_result.enable().is_ok() { + perf_event_cpu_ref_cycles = Some(perf_event_cpu_ref_cycles_result); + } + } + + Self { + perf_event_cpu_instructions, + perf_event_cpu_cycles, + perf_event_cpu_ref_cycles, + } + } + + fn collect(self) -> BlockReplayProfilerResult { + let mut cpu_instructions: Option = None; + let mut cpu_cycles: Option = None; + let mut cpu_ref_cycles: Option = None; + + if let Some(mut perf_event_cpu_instructions) = self.perf_event_cpu_instructions { + if perf_event_cpu_instructions.disable().is_ok() { + if let Ok(value) = perf_event_cpu_instructions.read() { + cpu_instructions = Some(value); + } + } + } + + if let Some(mut perf_event_cpu_cycles) = self.perf_event_cpu_cycles { + if perf_event_cpu_cycles.disable().is_ok() { + if let Ok(value) = perf_event_cpu_cycles.read() { + cpu_cycles = Some(value); + } + } + } + + if let Some(mut perf_event_cpu_ref_cycles) = self.perf_event_cpu_ref_cycles { + if perf_event_cpu_ref_cycles.disable().is_ok() { + if let Ok(value) = perf_event_cpu_ref_cycles.read() { + cpu_ref_cycles = Some(value); + } + } + } + + BlockReplayProfilerResult { + cpu_instructions, + cpu_cycles, + cpu_ref_cycles, + } + } +} + +#[cfg(not(all(feature = "profiler", target_os = "linux", target_arch = "x86_64")))] +impl BlockReplayProfiler { + fn new() -> Self { + warn!("BlockReplay Profiler is not available in this build."); + Self {} + } + fn collect(self) -> BlockReplayProfilerResult { + BlockReplayProfilerResult::default() + } +} + #[derive(Clone)] pub struct RPCNakamotoBlockReplayRequestHandler { pub block_id: Option, pub auth: Option, + pub profiler: bool, } impl RPCNakamotoBlockReplayRequestHandler { @@ -50,6 +155,7 @@ impl RPCNakamotoBlockReplayRequestHandler { Self { block_id: None, auth, + profiler: false, } } @@ -160,6 +266,13 @@ impl RPCNakamotoBlockReplayRequestHandler { for (i, tx) in block.txs.iter().enumerate() { let tx_len = tx.tx_len(); + let mut profiler: Option = None; + let mut profiler_result = BlockReplayProfilerResult::default(); + + if self.profiler { + profiler = Some(BlockReplayProfiler::new()); + } + let tx_result = builder.try_mine_tx_with_len( &mut tenure_tx, tx, @@ -167,9 +280,14 @@ impl RPCNakamotoBlockReplayRequestHandler { &BlockLimitFunction::NO_LIMIT_HIT, None, ); + + if let Some(profiler) = profiler { + profiler_result = profiler.collect(); + } + let err = match tx_result { TransactionResult::Success(tx_result) => { - txs_receipts.push(tx_result.receipt); + txs_receipts.push((tx_result.receipt, profiler_result)); Ok(()) } _ => Err(format!("Problematic tx {i}")), @@ -194,8 +312,8 @@ impl RPCNakamotoBlockReplayRequestHandler { let mut rpc_replayed_block = RPCReplayedBlock::from_block(block, block_fees, tenure_id, parent_block_id); - for receipt in &txs_receipts { - let transaction = RPCReplayedBlockTransaction::from_receipt(receipt); + for (receipt, profiler_result) in &txs_receipts { + let transaction = RPCReplayedBlockTransaction::from_receipt(receipt, &profiler_result); rpc_replayed_block.transactions.push(transaction); } @@ -231,10 +349,17 @@ pub struct RPCReplayedBlockTransaction { pub post_condition_aborted: bool, /// optional vm error pub vm_error: Option, + /// profiling data based on linux perf_events + pub cpu_instructions: Option, + pub cpu_cycles: Option, + pub cpu_ref_cycles: Option, } impl RPCReplayedBlockTransaction { - pub fn from_receipt(receipt: &StacksTransactionReceipt) -> Self { + fn from_receipt( + receipt: &StacksTransactionReceipt, + profiler_result: &BlockReplayProfilerResult, + ) -> Self { let events = receipt .events .iter() @@ -269,6 +394,9 @@ impl RPCReplayedBlockTransaction { events, post_condition_aborted: receipt.post_condition_aborted, vm_error: receipt.vm_error.clone(), + cpu_instructions: profiler_result.cpu_instructions, + cpu_cycles: profiler_result.cpu_cycles, + cpu_ref_cycles: profiler_result.cpu_ref_cycles, } } } @@ -382,6 +510,17 @@ impl HttpRequest for RPCNakamotoBlockReplayRequestHandler { self.block_id = Some(block_id); + if let Some(query_string) = query { + for (key, value) in form_urlencoded::parse(query_string.as_bytes()) { + if key == "profiler" { + if value == "1" { + self.profiler = true; + } + break; + } + } + } + Ok(HttpRequestContents::new().query_string(query)) } } @@ -446,6 +585,23 @@ impl StacksHttpRequest { ) .expect("FATAL: failed to construct request from infallible data") } + + pub fn new_block_replay_with_profiler( + host: PeerHost, + block_id: &StacksBlockId, + profiler: bool, + ) -> StacksHttpRequest { + StacksHttpRequest::new_for_peer( + host, + "GET".into(), + format!("/v3/blocks/replay/{block_id}"), + HttpRequestContents::new().query_arg( + "profiler".into(), + if profiler { "1".into() } else { "0".into() }, + ), + ) + .expect("FATAL: failed to construct request from infallible data") + } } /// Decode the HTTP response diff --git a/stackslib/src/net/api/tests/blockreplay.rs b/stackslib/src/net/api/tests/blockreplay.rs index c4ae3d0ea0..9bc385c0fb 100644 --- a/stackslib/src/net/api/tests/blockreplay.rs +++ b/stackslib/src/net/api/tests/blockreplay.rs @@ -72,6 +72,43 @@ fn test_try_parse_request() { let (preamble, contents) = parsed_request.destruct(); assert_eq!(&preamble, request.preamble()); + assert_eq!(handler.profiler, false); +} + +#[test] +fn test_try_parse_request_with_profiler() { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); + let mut http = StacksHttp::new(addr.clone(), &ConnectionOptions::default()); + + let mut request = StacksHttpRequest::new_block_replay_with_profiler( + addr.into(), + &StacksBlockId([0x01; 32]), + true, + ); + + // add the authorization header + request.add_header("authorization".into(), "password".into()); + + let bytes = request.try_serialize().unwrap(); + + debug!("Request:\n{}\n", std::str::from_utf8(&bytes).unwrap()); + + let (parsed_preamble, offset) = http.read_preamble(&bytes).unwrap(); + + let mut handler = + blockreplay::RPCNakamotoBlockReplayRequestHandler::new(Some("password".into())); + + let parsed_request = http + .handle_try_parse_request( + &mut handler, + &parsed_preamble.expect_request(), + &bytes[offset..], + ) + .unwrap(); + + let (preamble, contents) = parsed_request.destruct(); + + assert_eq!(handler.profiler, true); } #[test] @@ -110,8 +147,11 @@ fn test_try_make_response() { let mut requests = vec![]; // query existing, non-empty Nakamoto block - let mut request = - StacksHttpRequest::new_block_replay(addr.clone().into(), &rpc_test.canonical_tip); + let mut request = StacksHttpRequest::new_block_replay_with_profiler( + addr.clone().into(), + &rpc_test.canonical_tip, + true, + ); // add the authorization header request.add_header("authorization".into(), "password".into()); requests.push(request);