Skip to main content

miden_node_store/state/
disk_monitor.rs

1use std::path::{Path, PathBuf};
2use std::time::Duration;
3
4use miden_node_utils::shutdown::CancellationToken;
5use miden_node_utils::spawn::spawn_blocking_in_current_span;
6use miden_node_utils::tracing::{miden_instrument, miden_span_record};
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    skip_all,
34    err,
35)]
36async fn measure_disk_space_usage(data_dir: PathBuf) -> Result<(), tokio::task::JoinError> {
37    let usage = spawn_blocking_in_current_span(move || measure_disk_usage_bytes(&data_dir)).await?;
38    miden_span_record!(
39        db.sqlite.size = usage.sqlite_db,
40        db.sqlite.wal.size = usage.sqlite_wal,
41        db.block_store.size = usage.block_store,
42    );
43    #[cfg(feature = "rocksdb")]
44    {
45        miden_span_record!(
46            db.account_tree.size = usage.account_tree,
47            db.nullifier_tree.size = usage.nullifier_tree,
48            db.account_state_forest.size = usage.account_state_forest,
49        );
50    }
51
52    Ok(())
53}
54
55/// Byte counts for each on-disk storage component.
56struct DiskUsage {
57    sqlite_db: u64,
58    sqlite_wal: u64,
59    block_store: u64,
60    #[cfg(feature = "rocksdb")]
61    account_tree: u64,
62    #[cfg(feature = "rocksdb")]
63    nullifier_tree: u64,
64    #[cfg(feature = "rocksdb")]
65    account_state_forest: u64,
66}
67
68/// Collects on-disk byte sizes for every store data path under `data_dir`.
69fn measure_disk_usage_bytes(data_dir: &Path) -> DiskUsage {
70    DiskUsage {
71        sqlite_db: path_size_bytes(&data_dir.join("miden-store.sqlite3")),
72        sqlite_wal: path_size_bytes(&data_dir.join("miden-store.sqlite3-wal")),
73        block_store: dir_size_bytes(&data_dir.join("blocks")),
74        #[cfg(feature = "rocksdb")]
75        account_tree: dir_size_bytes(&data_dir.join("accounttree")),
76        #[cfg(feature = "rocksdb")]
77        nullifier_tree: dir_size_bytes(&data_dir.join("nullifiertree")),
78        #[cfg(feature = "rocksdb")]
79        account_state_forest: dir_size_bytes(&data_dir.join("accountstateforest")),
80    }
81}
82
83/// Returns the byte length of the file at `path`, or `0` if it does not exist.
84fn path_size_bytes(path: &Path) -> u64 {
85    fs_err::metadata(path).map(|m| m.len()).unwrap_or(0)
86}
87
88/// Returns the total byte length of all files in `path` iteratively, or `0` on any error.
89fn dir_size_bytes(path: &Path) -> u64 {
90    let mut to_process = vec![path.to_path_buf()];
91    let mut total = 0u64;
92    while let Some(dir) = to_process.pop() {
93        let Ok(entries) = fs_err::read_dir(&dir) else {
94            continue;
95        };
96        for entry in entries.flatten() {
97            if let Ok(meta) = entry.metadata() {
98                if meta.is_dir() {
99                    to_process.push(entry.path());
100                } else {
101                    total += meta.len();
102                }
103            }
104        }
105    }
106    total
107}