Skip to main content

miden_node_store/state/
disk_monitor.rs

1use std::path::{Path, PathBuf};
2use std::time::Duration;
3
4use miden_node_tracing::spawn::spawn_blocking_in_current_span;
5use miden_node_tracing::{miden_instrument, miden_span_record};
6use miden_node_utils::shutdown::CancellationToken;
7
8use crate::COMPONENT;
9use crate::state::State;
10
11impl State {
12    /// Spawns a background task that periodically records the on-disk size of every store data path
13    /// as `OTel` span attributes.
14    pub fn spawn_disk_monitor(&self, shutdown: CancellationToken) -> tokio::task::JoinHandle<()> {
15        let data_directory = self.data_directory.clone();
16
17        tokio::spawn(async move {
18            let mut interval = tokio::time::interval(Duration::from_mins(5));
19            loop {
20                tokio::select! {
21                    () = shutdown.cancelled() => return,
22                    _ = interval.tick() => {},
23                }
24                let _ = measure_disk_space_usage(data_directory.clone()).await;
25            }
26        })
27    }
28}
29
30#[miden_instrument(
31    target = COMPONENT,
32    name = "measure_disk_space_usage",
33    err,
34)]
35async fn measure_disk_space_usage(data_dir: PathBuf) -> Result<(), tokio::task::JoinError> {
36    let usage = spawn_blocking_in_current_span(move || measure_disk_usage_bytes(&data_dir)).await?;
37    miden_span_record!(
38        db.sqlite.size = usage.sqlite_db,
39        db.sqlite.wal.size = usage.sqlite_wal,
40        db.block_store.size = usage.block_store
41    );
42    #[cfg(feature = "rocksdb")]
43    {
44        miden_span_record!(
45            db.account_tree.size = usage.account_tree,
46            db.nullifier_tree.size = usage.nullifier_tree,
47            db.account_state_forest.size = usage.account_state_forest
48        );
49    }
50
51    Ok(())
52}
53
54/// Byte counts for each on-disk storage component.
55struct DiskUsage {
56    sqlite_db: u64,
57    sqlite_wal: u64,
58    block_store: u64,
59    #[cfg(feature = "rocksdb")]
60    account_tree: u64,
61    #[cfg(feature = "rocksdb")]
62    nullifier_tree: u64,
63    #[cfg(feature = "rocksdb")]
64    account_state_forest: u64,
65}
66
67/// Collects on-disk byte sizes for every store data path under `data_dir`.
68fn measure_disk_usage_bytes(data_dir: &Path) -> DiskUsage {
69    DiskUsage {
70        sqlite_db: path_size_bytes(&data_dir.join("miden-store.sqlite3")),
71        sqlite_wal: path_size_bytes(&data_dir.join("miden-store.sqlite3-wal")),
72        block_store: dir_size_bytes(&data_dir.join("blocks")),
73        #[cfg(feature = "rocksdb")]
74        account_tree: dir_size_bytes(&data_dir.join("accounttree")),
75        #[cfg(feature = "rocksdb")]
76        nullifier_tree: dir_size_bytes(&data_dir.join("nullifiertree")),
77        #[cfg(feature = "rocksdb")]
78        account_state_forest: dir_size_bytes(&data_dir.join("accountstateforest")),
79    }
80}
81
82/// Returns the byte length of the file at `path`, or `0` if it does not exist.
83fn path_size_bytes(path: &Path) -> u64 {
84    fs_err::metadata(path).map_or(0, |m| m.len())
85}
86
87/// Returns the total byte length of all files in `path` iteratively, or `0` on any error.
88fn dir_size_bytes(path: &Path) -> u64 {
89    let mut to_process = vec![path.to_path_buf()];
90    let mut total = 0u64;
91    while let Some(dir) = to_process.pop() {
92        let Ok(entries) = fs_err::read_dir(&dir) else {
93            continue;
94        };
95        for entry in entries.flatten() {
96            if let Ok(meta) = entry.metadata() {
97                if meta.is_dir() {
98                    to_process.push(entry.path());
99                } else {
100                    total += meta.len();
101                }
102            }
103        }
104    }
105    total
106}