diff --git a/Cargo.lock b/Cargo.lock index 6d782049426..406216086b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5194,6 +5194,7 @@ dependencies = [ "serde_json", "tempfile", "tokio", + "tokio-metrics", "tracing", "tracing-subscriber", "zeroize", @@ -6370,6 +6371,7 @@ dependencies = [ "simple-signer", "thiserror 2.0.18", "tokio", + "tokio-metrics", "tracing", "tracing-subscriber", "zeroize", @@ -7701,6 +7703,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-metrics" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9e81d53caf955549b1dec7af4ac2149e94cc25ed97b4a545151140281e2f528" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 37ea3b705e9..f6dcbd68292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,8 @@ key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = " dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "981e97f1015960ae5d277afdabcba1cbbc0b3a63" } dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "981e97f1015960ae5d277afdabcba1cbbc0b3a63" } +tokio-metrics = "0.5" + # Size-tuned profile for the iOS `rs-unified-sdk-ffi` staticlib, which # otherwise ships huge. Inherits `release` and is ONLY used by the iOS # build (`build_ios.sh --profile release`) diff --git a/packages/rs-platform-wallet-ffi/Cargo.toml b/packages/rs-platform-wallet-ffi/Cargo.toml index 1a96817351d..8a2bd4ef2b4 100644 --- a/packages/rs-platform-wallet-ffi/Cargo.toml +++ b/packages/rs-platform-wallet-ffi/Cargo.toml @@ -23,6 +23,7 @@ once_cell = "1.19" parking_lot = { version = "0.12", features = ["send_guard"] } lazy_static = "1.4" tokio = { version = "1", features = ["rt-multi-thread"] } +tokio-metrics = { workspace = true, optional = true } # Core dependencies (for Network type) dashcore = { workspace = true } @@ -65,6 +66,7 @@ cbindgen = "0.27" [features] default = [] mocks = [] +tokio-metrics = ["dep:tokio-metrics", "tokio/time"] # Opt-in Orchard / shielded support. Pulls in the heavy # `grovedb-commitment-tree` + `zip32` dependencies via # `platform-wallet/shielded`. Builds that don't enable this feature diff --git a/packages/rs-platform-wallet-ffi/src/logging.rs b/packages/rs-platform-wallet-ffi/src/logging.rs index 835ef9ae5b0..58a6bfc3f1a 100644 --- a/packages/rs-platform-wallet-ffi/src/logging.rs +++ b/packages/rs-platform-wallet-ffi/src/logging.rs @@ -40,9 +40,15 @@ fn enable_file_logging(log_level: &str, path: &Path) -> bool { let Some(f_sdk) = open_file(path.join("dash_sdk").join("run.log")) else { return false; }; + let Some(f_sdk_metrics) = open_file(path.join("dash_sdk").join("metrics.log")) else { + return false; + }; let Some(f_pw) = open_file(path.join("platform_wallet").join("run.log")) else { return false; }; + let Some(f_pw_metrics) = open_file(path.join("platform_wallet").join("metrics.log")) else { + return false; + }; let Some(f_spv) = open_file(path.join("dash_spv").join("run.log")) else { return false; }; @@ -57,14 +63,29 @@ fn enable_file_logging(log_level: &str, path: &Path) -> bool { .with_writer(Mutex::new(f_sdk)) .with_ansi(false) .with_filter(tracing_subscriber::EnvFilter::new(format!( - "dash_sdk={log_level},rs_sdk_ffi={log_level}" + "dash_sdk={log_level},rs_sdk_ffi={log_level},rs_sdk_ffi::metrics=off" + ))); + + let l_sdk_metrics = tracing_subscriber::fmt::layer() + .with_writer(Mutex::new(f_sdk_metrics)) + .with_ansi(false) + .with_filter(tracing_subscriber::EnvFilter::new(format!( + "rs_sdk_ffi::metrics={log_level}" ))); let l_pw = tracing_subscriber::fmt::layer() .with_writer(Mutex::new(f_pw)) .with_ansi(false) .with_filter(tracing_subscriber::EnvFilter::new(format!( - "platform_wallet={log_level},platform_wallet_ffi={log_level}" + "platform_wallet={log_level},platform_wallet_ffi={log_level},\ + platform_wallet_ffi::metrics=off" + ))); + + let l_pw_metrics = tracing_subscriber::fmt::layer() + .with_writer(Mutex::new(f_pw_metrics)) + .with_ansi(false) + .with_filter(tracing_subscriber::EnvFilter::new(format!( + "platform_wallet_ffi::metrics={log_level}" ))); let l_spv = tracing_subscriber::fmt::layer() @@ -95,13 +116,19 @@ fn enable_file_logging(log_level: &str, path: &Path) -> bool { let stdout_layer = tracing_subscriber::fmt::layer().with_filter(broad_env_filter(log_level)); + let layers: Vec + Send + Sync>> = vec![ + stdout_layer.boxed(), + l_sdk.boxed(), + l_sdk_metrics.boxed(), + l_pw.boxed(), + l_pw_metrics.boxed(), + l_spv.boxed(), + l_kw.boxed(), + l_grpc.boxed(), + ]; + if tracing_subscriber::registry() - .with(stdout_layer) - .with(l_sdk) - .with(l_pw) - .with(l_spv) - .with(l_kw) - .with(l_grpc) + .with(layers) .try_init() .is_err() { diff --git a/packages/rs-platform-wallet-ffi/src/runtime.rs b/packages/rs-platform-wallet-ffi/src/runtime.rs index 5c3ba69335a..965ef9f5ed5 100644 --- a/packages/rs-platform-wallet-ffi/src/runtime.rs +++ b/packages/rs-platform-wallet-ffi/src/runtime.rs @@ -31,11 +31,16 @@ const WORKER_STACK_BYTES: usize = 8 * 1024 * 1024; /// here, rather than the (small) calling thread. pub(crate) fn runtime() -> &'static tokio::runtime::Runtime { static RT: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { - tokio::runtime::Builder::new_multi_thread() + let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .thread_stack_size(WORKER_STACK_BYTES) .build() - .expect("Failed to create tokio runtime for platform-wallet-ffi") + .expect("Failed to create tokio runtime for platform-wallet-ffi"); + + #[cfg(feature = "tokio-metrics")] + metrics::spawn_sampler(&rt); + + rt }); &RT } @@ -54,3 +59,33 @@ where let rt = runtime(); rt.block_on(async move { rt.spawn(future).await.expect("tokio worker panicked") }) } + +#[cfg(feature = "tokio-metrics")] +mod metrics { + use std::time::Duration; + + pub(super) fn spawn_sampler(rt: &tokio::runtime::Runtime) { + let runtime_monitor = tokio_metrics::RuntimeMonitor::new(rt.handle()); + let mut rt_intervals = runtime_monitor.intervals(); + + rt.spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + let Some(r) = rt_intervals.next() else { break }; + + tracing::info!( + target: "platform_wallet_ffi::metrics", + workers = r.workers_count, + live_tasks = r.live_tasks_count, + busy_ratio = r.busy_ratio(), + mean_poll_us = r.mean_poll_duration.as_micros() as u64, + mean_polls_per_park = r.mean_polls_per_park(), + steals = r.total_steal_count, + global_queue_depth = r.global_queue_depth, + local_queue_depth = r.total_local_queue_depth, + overflow = r.total_overflow_count, + ); + } + }); + } +} diff --git a/packages/rs-sdk-ffi/Cargo.toml b/packages/rs-sdk-ffi/Cargo.toml index f56c7c9a88f..99ca8b6b279 100644 --- a/packages/rs-sdk-ffi/Cargo.toml +++ b/packages/rs-sdk-ffi/Cargo.toml @@ -45,6 +45,7 @@ bincode = { version = "=2.0.1", features = ["serde"] } # Async runtime tokio = { version = "1.41", features = ["rt-multi-thread", "macros", "sync", "time"] } +tokio-metrics = { workspace = true, optional = true } # Error handling thiserror = "2.0" @@ -92,6 +93,7 @@ log = "0.4" [features] default = [] +tokio-metrics = ["dep:tokio-metrics"] # Optional mocks for development/testing; maps to dash-sdk's mocks mocks = ["dash-sdk/mocks"] diff --git a/packages/rs-sdk-ffi/src/runtime.rs b/packages/rs-sdk-ffi/src/runtime.rs index 19d2e02a174..cc8d8378812 100644 --- a/packages/rs-sdk-ffi/src/runtime.rs +++ b/packages/rs-sdk-ffi/src/runtime.rs @@ -22,6 +22,8 @@ use std::future::Future; use std::ops::Deref; +#[cfg(feature = "tokio-metrics")] +use std::sync::atomic::{AtomicUsize, Ordering}; use tokio::runtime::{Builder, Runtime}; /// Stack size for the OS thread that drives blocking FFI calls. @@ -61,6 +63,20 @@ impl BigStackRuntime { .thread_stack_size(WORKER_STACK_SIZE) .enable_all() .build()?; + + #[cfg(feature = "tokio-metrics")] + static COUNTER: AtomicUsize = AtomicUsize::new(0); + + #[cfg(feature = "tokio-metrics")] + metrics::spawn_sampler( + &runtime, + format!( + "dash-sdk-ffi-shared-runtime-{}", + COUNTER.fetch_add(1, Ordering::SeqCst) + ) + .as_str(), + ); + Ok(BigStackRuntime(runtime)) } @@ -71,6 +87,20 @@ impl BigStackRuntime { .thread_stack_size(WORKER_STACK_SIZE) .enable_all() .build()?; + + #[cfg(feature = "tokio-metrics")] + static COUNTER: AtomicUsize = AtomicUsize::new(0); + + #[cfg(feature = "tokio-metrics")] + metrics::spawn_sampler( + &runtime, + format!( + "dash-sdk-ffi-isolated-runtime-{}", + COUNTER.fetch_add(1, Ordering::SeqCst) + ) + .as_str(), + ); + Ok(BigStackRuntime(runtime)) } @@ -86,6 +116,9 @@ impl BigStackRuntime { where F: Future, { + #[cfg(feature = "tokio-metrics")] + let _block_on_guard = metrics::BlockOnGuard::new(); + // `std::thread::Builder::spawn_scoped` is the only API that both (a) lets // the worker borrow non-`'static` data — the FFI call's references — and // (b) lets us set the stack size. It requires the moved-in closure (and @@ -144,6 +177,70 @@ impl Deref for BigStackRuntime { } } +#[cfg(feature = "tokio-metrics")] +mod metrics { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::time::{Duration, Instant}; + + static BLOCK_ON_IN_FLIGHT: AtomicUsize = AtomicUsize::new(0); + + pub(super) struct BlockOnGuard { + start: Instant, + in_flight: usize, + } + + impl BlockOnGuard { + pub(super) fn new() -> Self { + let in_flight = BLOCK_ON_IN_FLIGHT.fetch_add(1, Ordering::Relaxed) + 1; + Self { + start: Instant::now(), + in_flight, + } + } + } + + impl Drop for BlockOnGuard { + fn drop(&mut self) { + let elapsed_us = self.start.elapsed().as_micros() as u64; + BLOCK_ON_IN_FLIGHT.fetch_sub(1, Ordering::Relaxed); + tracing::info!( + target: "rs_sdk_ffi::metrics", + kind = "block_on", + elapsed_us, + in_flight = self.in_flight, + ); + } + } + + pub(super) fn spawn_sampler(rt: &tokio::runtime::Runtime, runtime_name: &str) { + let runtime_monitor = tokio_metrics::RuntimeMonitor::new(rt.handle()); + let mut rt_intervals = runtime_monitor.intervals(); + + let runtime_name = runtime_name.to_string(); + + rt.spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + let Some(r) = rt_intervals.next() else { break }; + + tracing::info!( + target: "rs_sdk_ffi::metrics", + runtime = %runtime_name, + workers = r.workers_count, + live_tasks = r.live_tasks_count, + busy_ratio = r.busy_ratio(), + mean_poll_us = r.mean_poll_duration.as_micros() as u64, + mean_polls_per_park = r.mean_polls_per_park(), + steals = r.total_steal_count, + global_queue_depth = r.global_queue_depth, + local_queue_depth = r.total_local_queue_depth, + overflow = r.total_overflow_count, + ); + } + }); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-unified-sdk-ffi/Cargo.toml b/packages/rs-unified-sdk-ffi/Cargo.toml index f853c1319df..f1a47c35e42 100644 --- a/packages/rs-unified-sdk-ffi/Cargo.toml +++ b/packages/rs-unified-sdk-ffi/Cargo.toml @@ -18,4 +18,8 @@ default = [] # Forwards to `platform-wallet-ffi/shielded`. Pulls Orchard / ZK # shielded sync support into the unified iOS framework. Off by # default so the framework's transparent surface stays slim. -shielded = ["platform-wallet-ffi/shielded"] \ No newline at end of file +shielded = ["platform-wallet-ffi/shielded"] +tokio-metrics = [ + "platform-wallet-ffi/tokio-metrics", + "rs-sdk-ffi/tokio-metrics", +] \ No newline at end of file diff --git a/packages/swift-sdk/build_ios.sh b/packages/swift-sdk/build_ios.sh index a0da4b907a8..a41a432ced1 100755 --- a/packages/swift-sdk/build_ios.sh +++ b/packages/swift-sdk/build_ios.sh @@ -196,6 +196,11 @@ EOF # the bundled SDK exposes the platform-wallet shielded FFI. CARGO_FEATURES="shielded" +if [ "$PROFILE" = "dev-ios" ]; then + CARGO_FEATURES="$CARGO_FEATURES tokio-metrics" + log_info " → tokio-metrics enabled (dev profile)" +fi + # iOS device if $BUILD_IOS; then IOS_TARGET="aarch64-apple-ios"