diff --git a/ledger/Cargo.toml b/ledger/Cargo.toml index 34f7da2426..2f139bd949 100644 --- a/ledger/Cargo.toml +++ b/ledger/Cargo.toml @@ -80,7 +80,7 @@ locktick = [ "snarkvm-ledger-store/locktick", "snarkvm-synthesizer/locktick" ] -metrics = [ "snarkvm-ledger-committee/metrics" ] +metrics = [ "snarkvm-ledger-committee/metrics", "snarkvm-ledger-store/metrics" ] prop-tests = [ "snarkvm-ledger-committee/prop-tests" ] rocks = [ "snarkvm-ledger-store/rocks" ] serial = [ diff --git a/ledger/store/Cargo.toml b/ledger/store/Cargo.toml index 90637b2996..ef1a71c67c 100644 --- a/ledger/store/Cargo.toml +++ b/ledger/store/Cargo.toml @@ -22,6 +22,7 @@ history = [ ] history-staking-rewards = [ ] slipstream-plugins = [ "dep:snarkvm-slipstream-plugin-manager" ] locktick = [ "dep:locktick", "snarkvm-ledger-puzzle/locktick" ] +metrics = [ "dep:snarkvm-metrics" ] rocks = [ "rocksdb", "smallvec" ] serial = [ "snarkvm-console/serial", @@ -43,6 +44,10 @@ wasm = [ ] test = [ ] +[dependencies.snarkvm-metrics] +workspace = true +optional = true + [dependencies.snarkvm-slipstream-plugin-manager] workspace = true optional = true diff --git a/ledger/store/src/block/mod.rs b/ledger/store/src/block/mod.rs index 37ab0eb173..ecd2eca6b5 100644 --- a/ledger/store/src/block/mod.rs +++ b/ledger/store/src/block/mod.rs @@ -1559,6 +1559,15 @@ impl> BlockStore { } } +#[cfg(all(feature = "rocks", feature = "metrics"))] +impl BlockStore> { + /// Reads RocksDB internal properties and publishes them to the metrics registry. + /// This is a cheap, synchronous call — properties come from in-memory counters with no I/O. + pub fn export_rocksdb_metrics(&self) { + self.storage.export_rocksdb_metrics(); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/ledger/store/src/helpers/rocksdb/block.rs b/ledger/store/src/helpers/rocksdb/block.rs index 036ce45a07..8215164bee 100644 --- a/ledger/store/src/helpers/rocksdb/block.rs +++ b/ledger/store/src/helpers/rocksdb/block.rs @@ -268,3 +268,12 @@ impl BlockStorage for BlockDB { } } } + +#[cfg(all(feature = "rocks", feature = "metrics"))] +impl BlockDB { + /// Reads RocksDB internal properties and publishes them to the metrics registry. + /// Any map can be used since they all share the same underlying RocksDB instance. + pub fn export_rocksdb_metrics(&self) { + self.id_map.export_rocksdb_metrics(); + } +} diff --git a/ledger/store/src/helpers/rocksdb/internal/map.rs b/ledger/store/src/helpers/rocksdb/internal/map.rs index b303cf86eb..4ebf5c2c41 100644 --- a/ledger/store/src/helpers/rocksdb/internal/map.rs +++ b/ledger/store/src/helpers/rocksdb/internal/map.rs @@ -55,6 +55,13 @@ impl InnerData let checkpoint = rocksdb::checkpoint::Checkpoint::new(&self.database)?; checkpoint.create_checkpoint(path).map_err(|e| e.into_string()) } + + /// Reads RocksDB internal properties and publishes them to the metrics registry. + /// Any map can be used since they all share the same underlying `RocksDB` instance. + #[cfg(feature = "metrics")] + pub fn export_rocksdb_metrics(&self) { + self.database.export_rocksdb_metrics(); + } } impl< diff --git a/ledger/store/src/helpers/rocksdb/internal/mod.rs b/ledger/store/src/helpers/rocksdb/internal/mod.rs index 2c5fa5693b..9ed65f6e95 100644 --- a/ledger/store/src/helpers/rocksdb/internal/mod.rs +++ b/ledger/store/src/helpers/rocksdb/internal/mod.rs @@ -297,6 +297,64 @@ impl RocksDB { fn are_atomic_writes_paused(&self) -> bool { self.atomic_writes_paused.load(Ordering::SeqCst) } + + /// Reads key RocksDB internal properties and publishes them to the metrics registry. + /// + /// This is a lightweight, synchronous call — properties are read from RocksDB's in-memory + /// counters with no disk I/O. Call it from an existing background loop (e.g. the + /// auto-checkpoint polling loop in snarkOS); there is no need to spawn a dedicated thread. + #[cfg(feature = "metrics")] + pub fn export_rocksdb_metrics(&self) { + use snarkvm_metrics::rocksdb as names; + + /// Read a single integer property; silently skip on error (DB may be closing). + fn prop(db: &rocksdb::DB, key: &str) -> Option { + db.property_int_value(key).ok().flatten() + } + + let db = &self.rocksdb; + + // Compaction pressure + if let Some(v) = prop(db, "rocksdb.compaction-pending") { + snarkvm_metrics::gauge(names::COMPACTION_PENDING, v as f64); + } + if let Some(v) = prop(db, "rocksdb.estimate-pending-compaction-bytes") { + snarkvm_metrics::gauge(names::ESTIMATE_PENDING_COMPACTION_BYTES, v as f64); + } + if let Some(v) = prop(db, "rocksdb.num-running-compactions") { + snarkvm_metrics::gauge(names::NUM_RUNNING_COMPACTIONS, v as f64); + } + if let Some(v) = prop(db, "rocksdb.num-running-flushes") { + snarkvm_metrics::gauge(names::NUM_RUNNING_FLUSHES, v as f64); + } + if let Some(v) = prop(db, "rocksdb.mem-table-flush-pending") { + snarkvm_metrics::gauge(names::MEM_TABLE_FLUSH_PENDING, v as f64); + } + + // Disk footprint + if let Some(v) = prop(db, "rocksdb.total-sst-files-size") { + snarkvm_metrics::gauge(names::TOTAL_SST_FILES_SIZE, v as f64); + } + if let Some(v) = prop(db, "rocksdb.live-sst-files-size") { + snarkvm_metrics::gauge(names::LIVE_SST_FILES_SIZE, v as f64); + } + + // General state + if let Some(v) = prop(db, "rocksdb.estimate-num-keys") { + snarkvm_metrics::gauge(names::ESTIMATE_NUM_KEYS, v as f64); + } + if let Some(v) = prop(db, "rocksdb.num-snapshots") { + snarkvm_metrics::gauge(names::NUM_SNAPSHOTS, v as f64); + } + + // Per-level SST file counts (levels 0–6) + for (level, &name) in names::NUM_FILES_AT_LEVEL.iter().enumerate() { + let key = format!("rocksdb.num-files-at-level{level}"); + if let Some(v) = prop(db, &key) { + snarkvm_metrics::gauge(name, v as f64); + } + } + } } // impl RocksDB { diff --git a/metrics/src/lib.rs b/metrics/src/lib.rs index ca901c3635..97614eae37 100644 --- a/metrics/src/lib.rs +++ b/metrics/src/lib.rs @@ -15,12 +15,66 @@ #![forbid(unsafe_code)] -const GAUGE_NAMES: [&str; 1] = [committee::TOTAL_STAKE]; +const GAUGE_NAMES: &[&str] = &[ + committee::TOTAL_STAKE, + rocksdb::COMPACTION_PENDING, + rocksdb::ESTIMATE_PENDING_COMPACTION_BYTES, + rocksdb::NUM_RUNNING_COMPACTIONS, + rocksdb::NUM_RUNNING_FLUSHES, + rocksdb::MEM_TABLE_FLUSH_PENDING, + rocksdb::TOTAL_SST_FILES_SIZE, + rocksdb::LIVE_SST_FILES_SIZE, + rocksdb::ESTIMATE_NUM_KEYS, + rocksdb::NUM_SNAPSHOTS, + rocksdb::NUM_FILES_AT_LEVEL[0], + rocksdb::NUM_FILES_AT_LEVEL[1], + rocksdb::NUM_FILES_AT_LEVEL[2], + rocksdb::NUM_FILES_AT_LEVEL[3], + rocksdb::NUM_FILES_AT_LEVEL[4], + rocksdb::NUM_FILES_AT_LEVEL[5], + rocksdb::NUM_FILES_AT_LEVEL[6], +]; pub mod committee { pub const TOTAL_STAKE: &str = "snarkvm_ledger_committee_total_stake"; } +/// RocksDB internal database metrics. +/// +/// Polled and published by calling `BlockStore::export_rocksdb_metrics()` from an existing +/// background loop (e.g. the auto-checkpoint task in snarkOS). All sizes are in bytes; +/// counts are dimensionless. Requires the `rocks` and `metrics` features on `snarkvm-ledger-store`. +pub mod rocksdb { + /// 1 if a compaction is pending (background compaction requested but not yet running), else 0. + pub const COMPACTION_PENDING: &str = "snarkvm_rocksdb_compaction_pending"; + /// Estimated total bytes of data to be compacted. A sustained non-zero value signals backpressure. + pub const ESTIMATE_PENDING_COMPACTION_BYTES: &str = "snarkvm_rocksdb_estimate_pending_compaction_bytes"; + /// Number of compactions currently running in the background. + pub const NUM_RUNNING_COMPACTIONS: &str = "snarkvm_rocksdb_num_running_compactions"; + /// Number of memtable flushes currently running. + pub const NUM_RUNNING_FLUSHES: &str = "snarkvm_rocksdb_num_running_flushes"; + /// 1 if a memtable flush is pending (memtable full but flush not yet started), else 0. + pub const MEM_TABLE_FLUSH_PENDING: &str = "snarkvm_rocksdb_mem_table_flush_pending"; + /// Total size of all SST files on disk (includes files pending deletion). + pub const TOTAL_SST_FILES_SIZE: &str = "snarkvm_rocksdb_total_sst_files_size_bytes"; + /// Size of live (referenced) SST files only. + pub const LIVE_SST_FILES_SIZE: &str = "snarkvm_rocksdb_live_sst_files_size_bytes"; + /// Estimated number of keys in the database. + pub const ESTIMATE_NUM_KEYS: &str = "snarkvm_rocksdb_estimate_num_keys"; + /// Number of snapshots currently held (non-zero blocks deletion of old SST files). + pub const NUM_SNAPSHOTS: &str = "snarkvm_rocksdb_num_snapshots"; + /// Number of SST files per LSM level (levels 0–6). + pub const NUM_FILES_AT_LEVEL: [&str; 7] = [ + "snarkvm_rocksdb_num_files_at_level0", + "snarkvm_rocksdb_num_files_at_level1", + "snarkvm_rocksdb_num_files_at_level2", + "snarkvm_rocksdb_num_files_at_level3", + "snarkvm_rocksdb_num_files_at_level4", + "snarkvm_rocksdb_num_files_at_level5", + "snarkvm_rocksdb_num_files_at_level6", + ]; +} + /// Registers all snarkVM metrics. pub fn register_metrics() { for name in GAUGE_NAMES {