From 1298731699a53a4d85a9d66bb1810c91320b9446 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 21 Nov 2025 13:48:51 +0400 Subject: [PATCH 1/3] feat: add missing rpc methods and CORS headers --- .../src/requests/http/mocked.rs | 55 ++++++-- magicblock-aperture/src/requests/mod.rs | 118 ++++++++---------- magicblock-aperture/src/requests/payload.rs | 4 +- .../src/server/http/dispatch.rs | 52 ++++---- magicblock-aperture/tests/mocked.rs | 13 +- 5 files changed, 139 insertions(+), 103 deletions(-) diff --git a/magicblock-aperture/src/requests/http/mocked.rs b/magicblock-aperture/src/requests/http/mocked.rs index 2cb8185cb..8aa532b4a 100644 --- a/magicblock-aperture/src/requests/http/mocked.rs +++ b/magicblock-aperture/src/requests/http/mocked.rs @@ -11,10 +11,12 @@ use magicblock_core::link::blocks::BlockHash; use solana_account_decoder::parse_token::UiTokenAmount; use solana_rpc_client_api::response::{ - RpcBlockCommitment, RpcContactInfo, RpcSnapshotSlotInfo, RpcSupply, + RpcBlockCommitment, RpcContactInfo, RpcPerfSample, RpcSnapshotSlotInfo, + RpcSupply, RpcVoteAccountStatus, }; use super::prelude::*; +const SLOTS_IN_EPOCH: u64 = 432_000; impl HttpDispatcher { /// Handles the `getSlotLeader` RPC request. @@ -37,7 +39,8 @@ impl HttpDispatcher { &self, request: &JsonRequest, ) -> HandlerResult { - Ok(ResponsePayload::encode_no_context(&request.id, 0)) + let count = self.ledger.count_transactions()?; + Ok(ResponsePayload::encode_no_context(&request.id, count)) } /// Handles the `getSlotLeaders` RPC request. @@ -161,13 +164,15 @@ impl HttpDispatcher { &self, request: &JsonRequest, ) -> HandlerResult { + let slot = self.blocks.block_height(); + let transaction_count = self.ledger.count_transactions()?; let info = json::json! {{ - "epoch": 0, - "slotIndex": 0, - "slotsInEpoch": u64::MAX, - "absoluteSlot": 0, - "blockHeight": 0, - "transactionCount": Some(0), + "epoch": slot / SLOTS_IN_EPOCH, + "slotIndex": slot % SLOTS_IN_EPOCH, + "slotsInEpoch": SLOTS_IN_EPOCH, + "absoluteSlot": slot, + "blockHeight": slot, + "transactionCount": Some(transaction_count), }}; Ok(ResponsePayload::encode_no_context(&request.id, info)) } @@ -182,8 +187,8 @@ impl HttpDispatcher { "firstNormalEpoch": 0, "firstNormalSlot": 0, "leaderScheduleSlotOffset": 0, - "slotsPerEpoch": u64::MAX, - "warmup": true + "slotsPerEpoch": SLOTS_IN_EPOCH, + "warmup": false }}; Ok(ResponsePayload::encode_no_context(&request.id, schedule)) } @@ -226,4 +231,34 @@ impl HttpDispatcher { }; Ok(ResponsePayload::encode_no_context(&request.id, [info])) } + + pub(crate) fn get_recent_performance_samples( + &self, + request: &JsonRequest, + ) -> HandlerResult { + const PERIOD: u16 = 60; + const SLOTS_PER_PERIOD: u64 = 20; + + let num_transactions = + self.ledger.count_transactions().map(|c| c as _)?; + let samples = RpcPerfSample { + slot: self.blocks.block_height(), + num_slots: PERIOD as u64 * SLOTS_PER_PERIOD, + num_transactions, + num_non_vote_transactions: Some(num_transactions), + sample_period_secs: PERIOD, + }; + Ok(ResponsePayload::encode_no_context(&request.id, [samples])) + } + + pub(crate) fn get_vote_accounts( + &self, + request: &JsonRequest, + ) -> HandlerResult { + let status = RpcVoteAccountStatus { + current: Vec::new(), + delinquent: Vec::new(), + }; + Ok(ResponsePayload::encode_no_context(&request.id, status)) + } } diff --git a/magicblock-aperture/src/requests/mod.rs b/magicblock-aperture/src/requests/mod.rs index 7528a74c3..e5dc3e99a 100644 --- a/magicblock-aperture/src/requests/mod.rs +++ b/magicblock-aperture/src/requests/mod.rs @@ -51,6 +51,7 @@ pub(crate) enum JsonRpcHttpMethod { GetLatestBlockhash, GetMultipleAccounts, GetProgramAccounts, + GetRecentPerformanceSamples, GetSignatureStatuses, GetSignaturesForAddress, GetSlot, @@ -65,6 +66,7 @@ pub(crate) enum JsonRpcHttpMethod { GetTransaction, GetTransactionCount, GetVersion, + GetVoteAccounts, IsBlockhashValid, MinimumLedgerSlot, RequestAirdrop, @@ -91,60 +93,48 @@ pub(crate) enum JsonRpcWsMethod { impl JsonRpcHttpMethod { pub(crate) fn as_str(&self) -> &'static str { match self { - JsonRpcHttpMethod::GetAccountInfo => "getAccountInfo", - JsonRpcHttpMethod::GetBalance => "getBalance", - JsonRpcHttpMethod::GetBlock => "getBlock", - JsonRpcHttpMethod::GetBlockCommitment => "getBlockCommitment", - JsonRpcHttpMethod::GetBlockHeight => "getBlockHeight", - JsonRpcHttpMethod::GetBlockTime => "getBlockTime", - JsonRpcHttpMethod::GetBlocks => "getBlocks", - JsonRpcHttpMethod::GetBlocksWithLimit => "getBlocksWithLimit", - JsonRpcHttpMethod::GetClusterNodes => "getClusterNodes", - JsonRpcHttpMethod::GetEpochInfo => "getEpochInfo", - JsonRpcHttpMethod::GetEpochSchedule => "getEpochSchedule", - JsonRpcHttpMethod::GetFeeForMessage => "getFeeForMessage", - JsonRpcHttpMethod::GetFirstAvailableBlock => { - "getFirstAvailableBlock" - } - JsonRpcHttpMethod::GetGenesisHash => "getGenesisHash", - JsonRpcHttpMethod::GetHealth => "getHealth", - JsonRpcHttpMethod::GetHighestSnapshotSlot => { - "getHighestSnapshotSlot" - } - JsonRpcHttpMethod::GetIdentity => "getIdentity", - JsonRpcHttpMethod::GetLargestAccounts => "getLargestAccounts", - JsonRpcHttpMethod::GetLatestBlockhash => "getLatestBlockhash", - JsonRpcHttpMethod::GetMultipleAccounts => "getMultipleAccounts", - JsonRpcHttpMethod::GetProgramAccounts => "getProgramAccounts", - JsonRpcHttpMethod::GetSignatureStatuses => "getSignatureStatuses", - JsonRpcHttpMethod::GetSignaturesForAddress => { - "getSignaturesForAddress" - } - JsonRpcHttpMethod::GetSlot => "getSlot", - JsonRpcHttpMethod::GetSlotLeader => "getSlotLeader", - JsonRpcHttpMethod::GetSlotLeaders => "getSlotLeaders", - JsonRpcHttpMethod::GetSupply => "getSupply", - JsonRpcHttpMethod::GetTokenAccountBalance => { - "getTokenAccountBalance" - } - JsonRpcHttpMethod::GetTokenAccountsByDelegate => { - "getTokenAccountsByDelegate" - } - JsonRpcHttpMethod::GetTokenAccountsByOwner => { - "getTokenAccountsByOwner" - } - JsonRpcHttpMethod::GetTokenLargestAccounts => { - "getTokenLargestAccounts" - } - JsonRpcHttpMethod::GetTokenSupply => "getTokenSupply", - JsonRpcHttpMethod::GetTransaction => "getTransaction", - JsonRpcHttpMethod::GetTransactionCount => "getTransactionCount", - JsonRpcHttpMethod::GetVersion => "getVersion", - JsonRpcHttpMethod::IsBlockhashValid => "isBlockhashValid", - JsonRpcHttpMethod::MinimumLedgerSlot => "minimumLedgerSlot", - JsonRpcHttpMethod::RequestAirdrop => "requestAirdrop", - JsonRpcHttpMethod::SendTransaction => "sendTransaction", - JsonRpcHttpMethod::SimulateTransaction => "simulateTransaction", + Self::GetAccountInfo => "getAccountInfo", + Self::GetBalance => "getBalance", + Self::GetBlock => "getBlock", + Self::GetBlockCommitment => "getBlockCommitment", + Self::GetBlockHeight => "getBlockHeight", + Self::GetBlockTime => "getBlockTime", + Self::GetBlocks => "getBlocks", + Self::GetBlocksWithLimit => "getBlocksWithLimit", + Self::GetClusterNodes => "getClusterNodes", + Self::GetEpochInfo => "getEpochInfo", + Self::GetEpochSchedule => "getEpochSchedule", + Self::GetFeeForMessage => "getFeeForMessage", + Self::GetFirstAvailableBlock => "getFirstAvailableBlock", + Self::GetGenesisHash => "getGenesisHash", + Self::GetHealth => "getHealth", + Self::GetHighestSnapshotSlot => "getHighestSnapshotSlot", + Self::GetIdentity => "getIdentity", + Self::GetLargestAccounts => "getLargestAccounts", + Self::GetLatestBlockhash => "getLatestBlockhash", + Self::GetMultipleAccounts => "getMultipleAccounts", + Self::GetProgramAccounts => "getProgramAccounts", + Self::GetRecentPerformanceSamples => "getRecentPerformanceSamples", + Self::GetSignatureStatuses => "getSignatureStatuses", + Self::GetSignaturesForAddress => "getSignaturesForAddress", + Self::GetSlot => "getSlot", + Self::GetSlotLeader => "getSlotLeader", + Self::GetSlotLeaders => "getSlotLeaders", + Self::GetSupply => "getSupply", + Self::GetTokenAccountBalance => "getTokenAccountBalance", + Self::GetTokenAccountsByDelegate => "getTokenAccountsByDelegate", + Self::GetTokenAccountsByOwner => "getTokenAccountsByOwner", + Self::GetTokenLargestAccounts => "getTokenLargestAccounts", + Self::GetTokenSupply => "getTokenSupply", + Self::GetTransaction => "getTransaction", + Self::GetTransactionCount => "getTransactionCount", + Self::GetVersion => "getVersion", + Self::GetVoteAccounts => "getVoteAccounts", + Self::IsBlockhashValid => "isBlockhashValid", + Self::MinimumLedgerSlot => "minimumLedgerSlot", + Self::RequestAirdrop => "requestAirdrop", + Self::SendTransaction => "sendTransaction", + Self::SimulateTransaction => "simulateTransaction", } } } @@ -152,16 +142,16 @@ impl JsonRpcHttpMethod { impl JsonRpcWsMethod { pub(crate) fn as_str(&self) -> &'static str { match self { - JsonRpcWsMethod::AccountSubscribe => "accountSubscribe", - JsonRpcWsMethod::AccountUnsubscribe => "accountUnsubscribe", - JsonRpcWsMethod::LogsSubscribe => "logsSubscribe", - JsonRpcWsMethod::LogsUnsubscribe => "logsUnsubscribe", - JsonRpcWsMethod::ProgramSubscribe => "programSubscribe", - JsonRpcWsMethod::ProgramUnsubscribe => "programUnsubscribe", - JsonRpcWsMethod::SignatureSubscribe => "signatureSubscribe", - JsonRpcWsMethod::SignatureUnsubscribe => "signatureUnsubscribe", - JsonRpcWsMethod::SlotSubscribe => "slotSubscribe", - JsonRpcWsMethod::SlotUnsubscribe => "slotUnsubscribe", + Self::AccountSubscribe => "accountSubscribe", + Self::AccountUnsubscribe => "accountUnsubscribe", + Self::LogsSubscribe => "logsSubscribe", + Self::LogsUnsubscribe => "logsUnsubscribe", + Self::ProgramSubscribe => "programSubscribe", + Self::ProgramUnsubscribe => "programUnsubscribe", + Self::SignatureSubscribe => "signatureSubscribe", + Self::SignatureUnsubscribe => "signatureUnsubscribe", + Self::SlotSubscribe => "slotSubscribe", + Self::SlotUnsubscribe => "slotUnsubscribe", } } } diff --git a/magicblock-aperture/src/requests/payload.rs b/magicblock-aperture/src/requests/payload.rs index 7a02576ad..951bc4196 100644 --- a/magicblock-aperture/src/requests/payload.rs +++ b/magicblock-aperture/src/requests/payload.rs @@ -1,4 +1,4 @@ -use hyper::{body::Bytes, Response}; +use hyper::{body::Bytes, header::CONTENT_TYPE, Response}; use json::{Serialize, Value}; use magicblock_core::Slot; @@ -160,10 +160,8 @@ impl<'id, T: Serialize> ResponsePayload<'id, T> { /// Builds a standard `200 OK` JSON HTTP response with appropriate headers. fn build_json_response(payload: T) -> Response { - use hyper::header::{ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE}; Response::builder() .header(CONTENT_TYPE, "application/json") - .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") .body(JsonBody::from(payload)) // SAFETY: Safe with static values .expect("Building JSON response failed") diff --git a/magicblock-aperture/src/server/http/dispatch.rs b/magicblock-aperture/src/server/http/dispatch.rs index 37238b9fc..583609cd0 100644 --- a/magicblock-aperture/src/server/http/dispatch.rs +++ b/magicblock-aperture/src/server/http/dispatch.rs @@ -1,6 +1,14 @@ use std::{convert::Infallible, sync::Arc}; -use hyper::{body::Incoming, Method, Request, Response}; +use hyper::{ + body::Incoming, + header::{ + HeaderValue, ACCESS_CONTROL_ALLOW_HEADERS, + ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, + ACCESS_CONTROL_MAX_AGE, + }, + Method, Request, Response, +}; use magicblock_accounts_db::AccountsDb; use magicblock_core::link::{ transactions::TransactionSchedulerHandle, DispatchEndpoints, @@ -86,7 +94,9 @@ impl HttpDispatcher { request: Request, ) -> Result, Infallible> { if request.method() == Method::OPTIONS { - return Self::handle_cors_preflight(); + let mut response = Response::new(JsonBody::from("")); + Self::set_access_control_headers(&mut response); + return Ok(response); } // A local macro to simplify error handling. If a Result is an Err, // it immediately formats it into a JSON-RPC error response and returns. @@ -119,7 +129,8 @@ impl HttpDispatcher { let _timer = RPC_REQUEST_HANDLING_TIME .with_label_values(&[method]) .start_timer(); - match request.method { + + let mut result = match request.method { GetAccountInfo => self.get_account_info(request).await, GetBalance => self.get_balance(request).await, GetBlock => self.get_block(request), @@ -141,6 +152,9 @@ impl HttpDispatcher { GetLatestBlockhash => self.get_latest_blockhash(request), GetMultipleAccounts => self.get_multiple_accounts(request).await, GetProgramAccounts => self.get_program_accounts(request), + GetRecentPerformanceSamples => { + self.get_recent_performance_samples(request) + } GetSignatureStatuses => self.get_signature_statuses(request), GetSignaturesForAddress => self.get_signatures_for_address(request), GetSlot => self.get_slot(request), @@ -161,33 +175,29 @@ impl HttpDispatcher { GetTransaction => self.get_transaction(request), GetTransactionCount => self.get_transaction_count(request), GetVersion => self.get_version(request), + GetVoteAccounts => self.get_vote_accounts(request), IsBlockhashValid => self.is_blockhash_valid(request), MinimumLedgerSlot => self.get_first_available_block(request), RequestAirdrop => self.request_airdrop(request).await, SendTransaction => self.send_transaction(request).await, SimulateTransaction => self.simulate_transaction(request).await, + }; + if let Ok(response) = &mut result { + Self::set_access_control_headers(response); } + result } - /// Handles CORS preflight OPTIONS requests. - /// - /// Responds with a `200 OK` and the necessary `Access-Control-*` headers to - /// authorize subsequent `POST` requests from any origin (e.g. explorers) - fn handle_cors_preflight() -> Result, Infallible> { - use hyper::header::{ - ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, - ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE, - }; + /// Set CORS/Access control related headers (required by explorers/web apps) + fn set_access_control_headers(response: &mut Response) { + static HV: fn(&'static str) -> HeaderValue = + |v| HeaderValue::from_static(v); - let response = Response::builder() - .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .header(ACCESS_CONTROL_ALLOW_METHODS, "POST, OPTIONS, GET") - .header(ACCESS_CONTROL_ALLOW_HEADERS, "*") - .header(ACCESS_CONTROL_MAX_AGE, "86400") - .body(JsonBody::from("")) - // SAFETY: This is safe with static, valid headers - .expect("Building CORS response failed"); + let headers = response.headers_mut(); - Ok(response) + headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HV("*")); + headers.insert(ACCESS_CONTROL_ALLOW_METHODS, HV("POST, OPTIONS, GET")); + headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, HV("*")); + headers.insert(ACCESS_CONTROL_MAX_AGE, HV("86400")); } } diff --git a/magicblock-aperture/tests/mocked.rs b/magicblock-aperture/tests/mocked.rs index c9740d0f7..d497d3355 100644 --- a/magicblock-aperture/tests/mocked.rs +++ b/magicblock-aperture/tests/mocked.rs @@ -159,7 +159,11 @@ async fn test_get_epoch_info() { .expect("get_epoch_info request failed"); assert_eq!(epoch_info.epoch, 0, "epoch should be 0"); - assert_eq!(epoch_info.absolute_slot, 0, "absolute_slot should be 0"); + assert_eq!( + epoch_info.absolute_slot, + env.latest_slot(), + "absolute_slot should be equal to env slot" + ); } /// Verifies the mocked `getEpochSchedule` RPC method. @@ -173,11 +177,10 @@ async fn test_get_epoch_schedule() { .expect("get_epoch_schedule request failed"); assert_eq!( - schedule.slots_per_epoch, - u64::MAX, - "slots_per_epoch should be u64::MAX" + schedule.slots_per_epoch, 432_000, + "slots_per_epoch should be the same as solana's" ); - assert!(schedule.warmup, "warmup should be true"); + assert!(!schedule.warmup, "warmup should be false"); } /// Verifies the mocked `getClusterNodes` RPC method. From f6389abd41999d8320539e66e039865c39f85753 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 21 Nov 2025 22:22:15 +0400 Subject: [PATCH 2/3] fix: correct perf samples deltas --- .../http/get_recent_performance_samples.rs | 84 +++++++++++++++++++ .../src/requests/http/mocked.rs | 26 +----- magicblock-aperture/src/requests/http/mod.rs | 1 + magicblock-aperture/src/server/http/mod.rs | 7 +- magicblock-api/src/magic_validator.rs | 3 +- magicblock-metrics/src/metrics/mod.rs | 4 + .../src/executor/processing.rs | 5 +- 7 files changed, 104 insertions(+), 26 deletions(-) create mode 100644 magicblock-aperture/src/requests/http/get_recent_performance_samples.rs diff --git a/magicblock-aperture/src/requests/http/get_recent_performance_samples.rs b/magicblock-aperture/src/requests/http/get_recent_performance_samples.rs new file mode 100644 index 000000000..573204b74 --- /dev/null +++ b/magicblock-aperture/src/requests/http/get_recent_performance_samples.rs @@ -0,0 +1,84 @@ +use std::{ + cmp::Reverse, + sync::{Arc, OnceLock}, + time::Duration, +}; + +use magicblock_metrics::metrics::TRANSACTION_COUNT; +use scc::{ebr::Guard, TreeIndex}; +use solana_rpc_client_api::response::RpcPerfSample; +use tokio::time; +use tokio_util::sync::CancellationToken; + +use super::prelude::*; + +const PERIOD: u64 = 60; +const MAX_PERF_SAMPLES: usize = 720; + +static PERF_SAMPLES: OnceLock, Sample>> = + OnceLock::new(); + +#[derive(Clone, Copy)] +struct Sample { + transactions: u64, + slots: u64, +} + +impl HttpDispatcher { + pub(crate) fn get_recent_performance_samples( + &self, + request: &mut JsonRequest, + ) -> HandlerResult { + let count = parse_params!(request.params()?, usize); + let mut count: usize = some_or_err!(count); + count = count.min(MAX_PERF_SAMPLES); + let index = PERF_SAMPLES.get_or_init(|| TreeIndex::default()); + let mut samples = Vec::with_capacity(count); + for (slot, &sample) in index.iter(&Guard::new()).take(count) { + let sample = RpcPerfSample { + slot: slot.0, + num_slots: sample.slots, + num_transactions: sample.transactions, + num_non_vote_transactions: None, + sample_period_secs: PERIOD as u16, + }; + samples.push(sample); + } + + Ok(ResponsePayload::encode_no_context(&request.id, samples)) + } + + pub(crate) async fn run_perf_samples_collector( + self: Arc, + cancel: CancellationToken, + ) { + let mut interval = time::interval(Duration::from_secs(PERIOD)); + let mut last_slot = self.blocks.block_height(); + let mut last_count = TRANSACTION_COUNT.get(); + loop { + tokio::select! { + _ = interval.tick() => { + let count = TRANSACTION_COUNT.get(); + let index = PERF_SAMPLES.get_or_init(|| TreeIndex::default()); + let slot = self.blocks.block_height(); + let sample = Sample { + slots: slot.saturating_sub(last_slot).max(1), + transactions: count.saturating_sub(last_count) as u64, + }; + let _ = index.insert_async(Reverse(slot), sample).await; + if index.len() > MAX_PERF_SAMPLES { + const RANGE: u64 = MAX_PERF_SAMPLES as u64 * 20; + let upper = Reverse(slot.saturating_sub(RANGE)); + let lower = Reverse(upper.0.saturating_sub(RANGE)); + index.remove_range_async(lower..upper).await; + } + last_slot = slot; + last_count = count; + } + _ = cancel.cancelled() => { + break; + } + } + } + } +} diff --git a/magicblock-aperture/src/requests/http/mocked.rs b/magicblock-aperture/src/requests/http/mocked.rs index 8aa532b4a..85a9c2725 100644 --- a/magicblock-aperture/src/requests/http/mocked.rs +++ b/magicblock-aperture/src/requests/http/mocked.rs @@ -9,10 +9,11 @@ //! returning default or empty responses, rather than 'method not found' errors. use magicblock_core::link::blocks::BlockHash; +use magicblock_metrics::metrics::TRANSACTION_COUNT; use solana_account_decoder::parse_token::UiTokenAmount; use solana_rpc_client_api::response::{ - RpcBlockCommitment, RpcContactInfo, RpcPerfSample, RpcSnapshotSlotInfo, - RpcSupply, RpcVoteAccountStatus, + RpcBlockCommitment, RpcContactInfo, RpcSnapshotSlotInfo, RpcSupply, + RpcVoteAccountStatus, }; use super::prelude::*; @@ -39,7 +40,7 @@ impl HttpDispatcher { &self, request: &JsonRequest, ) -> HandlerResult { - let count = self.ledger.count_transactions()?; + let count = TRANSACTION_COUNT.get(); Ok(ResponsePayload::encode_no_context(&request.id, count)) } @@ -232,25 +233,6 @@ impl HttpDispatcher { Ok(ResponsePayload::encode_no_context(&request.id, [info])) } - pub(crate) fn get_recent_performance_samples( - &self, - request: &JsonRequest, - ) -> HandlerResult { - const PERIOD: u16 = 60; - const SLOTS_PER_PERIOD: u64 = 20; - - let num_transactions = - self.ledger.count_transactions().map(|c| c as _)?; - let samples = RpcPerfSample { - slot: self.blocks.block_height(), - num_slots: PERIOD as u64 * SLOTS_PER_PERIOD, - num_transactions, - num_non_vote_transactions: Some(num_transactions), - sample_period_secs: PERIOD, - }; - Ok(ResponsePayload::encode_no_context(&request.id, [samples])) - } - pub(crate) fn get_vote_accounts( &self, request: &JsonRequest, diff --git a/magicblock-aperture/src/requests/http/mod.rs b/magicblock-aperture/src/requests/http/mod.rs index ea599e999..b7a8c0beb 100644 --- a/magicblock-aperture/src/requests/http/mod.rs +++ b/magicblock-aperture/src/requests/http/mod.rs @@ -270,6 +270,7 @@ pub(crate) mod get_identity; pub(crate) mod get_latest_blockhash; pub(crate) mod get_multiple_accounts; pub(crate) mod get_program_accounts; +pub(crate) mod get_recent_performance_samples; pub(crate) mod get_signature_statuses; pub(crate) mod get_signatures_for_address; pub(crate) mod get_slot; diff --git a/magicblock-aperture/src/server/http/mod.rs b/magicblock-aperture/src/server/http/mod.rs index 7f36aefbd..fd71b28f9 100644 --- a/magicblock-aperture/src/server/http/mod.rs +++ b/magicblock-aperture/src/server/http/mod.rs @@ -64,7 +64,10 @@ impl HttpServer { /// 2. The server then waits for all active connections (which hold a clone of the /// `shutdown` handle) to complete their work and drop their handles. Only then /// does the `run` method return. - pub(crate) async fn run(mut self) { + pub(crate) async fn run(self) { + let dispatcher = self.dispatcher.clone(); + let cancel = self.cancel.clone(); + tokio::spawn(dispatcher.run_perf_samples_collector(cancel)); loop { tokio::select! { biased; @@ -87,7 +90,7 @@ impl HttpServer { /// /// Each connection is managed by a Hyper connection handler and is integrated with /// the server's cancellation mechanism for graceful shutdown. - fn handle(&mut self, stream: TcpStream) { + fn handle(&self, stream: TcpStream) { // Create a child token so this specific connection can be cancelled. let cancel = self.cancel.child_token(); diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index e1e856c3a..2c85c14d5 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -48,7 +48,7 @@ use magicblock_ledger::{ ledger_truncator::{LedgerTruncator, DEFAULT_TRUNCATION_TIME_INTERVAL}, LatestBlock, Ledger, }; -use magicblock_metrics::MetricsService; +use magicblock_metrics::{metrics::TRANSACTION_COUNT, MetricsService}; use magicblock_processor::{ build_svm_env, scheduler::{state::TransactionSchedulerState, TransactionScheduler}, @@ -288,6 +288,7 @@ impl MagicValidator { .auto_airdrop_lamports > 0, }; + TRANSACTION_COUNT.inc_by(ledger.count_transactions()? as u64); txn_scheduler_state .load_upgradeable_programs(&programs_to_load(&config.programs)) .map_err(|err| { diff --git a/magicblock-metrics/src/metrics/mod.rs b/magicblock-metrics/src/metrics/mod.rs index b2f1b44ad..d6645da89 100644 --- a/magicblock-metrics/src/metrics/mod.rs +++ b/magicblock-metrics/src/metrics/mod.rs @@ -233,6 +233,10 @@ lazy_static::lazy_static! { // ----------------- // Transaction Execution // ----------------- + pub static ref TRANSACTION_COUNT: IntCounter = IntCounter::new( + "transaction_count", "Total number of executed transactions" + ).unwrap(); + pub static ref FAILED_TRANSACTIONS_COUNT: IntCounter = IntCounter::new( "failed_transactions_count", "Total number of failed transactions" ).unwrap(); diff --git a/magicblock-processor/src/executor/processing.rs b/magicblock-processor/src/executor/processing.rs index 98a656452..6d23c3060 100644 --- a/magicblock-processor/src/executor/processing.rs +++ b/magicblock-processor/src/executor/processing.rs @@ -9,7 +9,9 @@ use magicblock_core::{ }, tls::ExecutionTlsStash, }; -use magicblock_metrics::metrics::FAILED_TRANSACTIONS_COUNT; +use magicblock_metrics::metrics::{ + FAILED_TRANSACTIONS_COUNT, TRANSACTION_COUNT, +}; use solana_account::ReadableAccount; use solana_pubkey::Pubkey; use solana_svm::{ @@ -51,6 +53,7 @@ impl super::TransactionExecutor { ) { let (result, balances) = self.process(&transaction); let [txn] = transaction; + TRANSACTION_COUNT.inc(); let processed = match result { Ok(processed) => processed, From 56fe05b137b9f7ff049516950d5329f1c64913e4 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Sat, 22 Nov 2025 16:08:10 +0400 Subject: [PATCH 3/3] fix: correct performance samples collection --- .../http/get_recent_performance_samples.rs | 69 +++++++++++++------ 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/magicblock-aperture/src/requests/http/get_recent_performance_samples.rs b/magicblock-aperture/src/requests/http/get_recent_performance_samples.rs index 573204b74..45cdbbcec 100644 --- a/magicblock-aperture/src/requests/http/get_recent_performance_samples.rs +++ b/magicblock-aperture/src/requests/http/get_recent_performance_samples.rs @@ -12,9 +12,18 @@ use tokio_util::sync::CancellationToken; use super::prelude::*; -const PERIOD: u64 = 60; +/// 60 seconds per sample +const PERIOD_SECS: u64 = 60; + +/// Keep 12 hours of history (720 minutes) const MAX_PERF_SAMPLES: usize = 720; +/// Estimated blocks per minute: +/// Nominal = 1200 (20 blocks/sec * 60s). +/// We use 1500 (25% buffer) to ensure the cleanup +/// logic never accidentally prunes valid history +const ESTIMATED_SLOTS_PER_SAMPLE: u64 = 1500; + static PERF_SAMPLES: OnceLock, Sample>> = OnceLock::new(); @@ -31,18 +40,22 @@ impl HttpDispatcher { ) -> HandlerResult { let count = parse_params!(request.params()?, usize); let mut count: usize = some_or_err!(count); + + // Cap request at max history size (12h) count = count.min(MAX_PERF_SAMPLES); - let index = PERF_SAMPLES.get_or_init(|| TreeIndex::default()); + + let index = PERF_SAMPLES.get_or_init(TreeIndex::default); let mut samples = Vec::with_capacity(count); + + // Index is keyed by Reverse(Slot), so iter() yields Newest -> Oldest for (slot, &sample) in index.iter(&Guard::new()).take(count) { - let sample = RpcPerfSample { + samples.push(RpcPerfSample { slot: slot.0, num_slots: sample.slots, num_transactions: sample.transactions, num_non_vote_transactions: None, - sample_period_secs: PERIOD as u16, - }; - samples.push(sample); + sample_period_secs: PERIOD_SECS as u16, + }); } Ok(ResponsePayload::encode_no_context(&request.id, samples)) @@ -52,28 +65,44 @@ impl HttpDispatcher { self: Arc, cancel: CancellationToken, ) { - let mut interval = time::interval(Duration::from_secs(PERIOD)); + let mut interval = time::interval(Duration::from_secs(PERIOD_SECS)); + let mut last_slot = self.blocks.block_height(); - let mut last_count = TRANSACTION_COUNT.get(); + let mut last_tx_count = TRANSACTION_COUNT.get(); + loop { tokio::select! { _ = interval.tick() => { - let count = TRANSACTION_COUNT.get(); - let index = PERF_SAMPLES.get_or_init(|| TreeIndex::default()); - let slot = self.blocks.block_height(); + // Capture current state + let current_slot = self.blocks.block_height(); + let current_tx_count = TRANSACTION_COUNT.get(); + + // Calculate Deltas (Activity within the last 60s) + let slots_delta = current_slot.saturating_sub(last_slot).max(1); + let tx_delta = current_tx_count.saturating_sub(last_tx_count); + + let index = PERF_SAMPLES.get_or_init(TreeIndex::default); let sample = Sample { - slots: slot.saturating_sub(last_slot).max(1), - transactions: count.saturating_sub(last_count) as u64, + slots: slots_delta, + transactions: tx_delta, }; - let _ = index.insert_async(Reverse(slot), sample).await; + let _ = index.insert_async(Reverse(current_slot), sample).await; + + // Prune old history if index.len() > MAX_PERF_SAMPLES { - const RANGE: u64 = MAX_PERF_SAMPLES as u64 * 20; - let upper = Reverse(slot.saturating_sub(RANGE)); - let lower = Reverse(upper.0.saturating_sub(RANGE)); - index.remove_range_async(lower..upper).await; + // Calculate cutoff: 720 samples * 1500 blocks = ~1.08M blocks history + let retention_range = MAX_PERF_SAMPLES as u64 * ESTIMATED_SLOTS_PER_SAMPLE; + let cutoff_slot = current_slot.saturating_sub(retention_range); + + // Remove everything OLDER than the cutoff. + // In Reverse(), "Older" (Smaller Slot) == "Greater Value". + // RangeFrom (cutoff..) removes the tail of the tree. + index.remove_range_async(Reverse(cutoff_slot)..).await; } - last_slot = slot; - last_count = count; + + // Update baseline for next tick + last_slot = current_slot; + last_tx_count = current_tx_count; } _ = cancel.cancelled() => { break;