foyer_storage/io/device/
statistics.rs

1// Copyright 2026 foyer Project Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    sync::atomic::{AtomicIsize, AtomicUsize, Ordering},
17    time::Duration,
18};
19
20use fastant::{Atomic, Instant};
21
22use crate::Throttle;
23
24#[derive(Debug)]
25struct Metric {
26    value: AtomicUsize,
27    throttle: f64,
28    quota: AtomicIsize,
29    update: Atomic,
30}
31
32impl Metric {
33    fn new(throttle: f64) -> Self {
34        Self {
35            value: AtomicUsize::new(0),
36            throttle,
37            quota: AtomicIsize::new(0),
38            update: Atomic::new(Instant::now()),
39        }
40    }
41
42    fn load(&self) -> usize {
43        self.value.load(Ordering::Relaxed)
44    }
45
46    fn record(&self, value: usize) {
47        self.value.fetch_add(value, Ordering::Relaxed);
48        // If throttle is set, update the quota.
49        if self.throttle != 0.0 {
50            self.quota.fetch_sub(value as _, Ordering::Relaxed);
51        }
52    }
53
54    /// Get the nearest time to retry to check if there is quota.
55    ///
56    /// Return `Duration::ZERO` if no need to wait.
57    fn throttle(&self) -> Duration {
58        // If throttle is not set, no need to wait.
59        if self.throttle == 0.0 {
60            return Duration::ZERO;
61        }
62
63        let now = Instant::now();
64        let update = self.update.load(Ordering::Relaxed);
65
66        let dur = now.duration_since(update).as_secs_f64();
67        let fill = dur * self.throttle;
68
69        let quota = f64::min(self.throttle, self.quota.load(Ordering::Relaxed) as f64 + fill);
70
71        self.update.store(now, Ordering::Relaxed);
72        self.quota.store(quota as isize, Ordering::Relaxed);
73
74        if quota >= 0.0 {
75            Duration::ZERO
76        } else {
77            Duration::from_secs_f64(-quota / self.throttle)
78        }
79    }
80}
81
82/// The statistics of the device.
83#[derive(Debug)]
84pub struct Statistics {
85    throttle: Throttle,
86
87    disk_write_bytes: Metric,
88    disk_read_bytes: Metric,
89    disk_write_ios: Metric,
90    disk_read_ios: Metric,
91}
92
93impl Statistics {
94    /// Create a new statistics.
95    pub fn new(throttle: Throttle) -> Self {
96        let disk_write_bytes = Metric::new(throttle.write_throughput.map(|v| v.get()).unwrap_or_default() as f64);
97        let disk_read_bytes = Metric::new(throttle.read_throughput.map(|v| v.get()).unwrap_or_default() as f64);
98        let disk_write_ios = Metric::new(throttle.write_iops.map(|v| v.get()).unwrap_or_default() as f64);
99        let disk_read_ios = Metric::new(throttle.read_iops.map(|v| v.get()).unwrap_or_default() as f64);
100        Self {
101            throttle,
102            disk_write_bytes,
103            disk_read_bytes,
104            disk_write_ios,
105            disk_read_ios,
106        }
107    }
108
109    /// Get the disk cache written bytes.
110    pub fn disk_write_bytes(&self) -> usize {
111        self.disk_write_bytes.load()
112    }
113
114    /// Get the disk cache read bytes.
115    pub fn disk_read_bytes(&self) -> usize {
116        self.disk_read_bytes.load()
117    }
118
119    /// Get the disk cache written ios.
120    pub fn disk_write_ios(&self) -> usize {
121        self.disk_write_ios.load()
122    }
123
124    /// Get the disk cache read bytes.
125    pub fn disk_read_ios(&self) -> usize {
126        self.disk_read_ios.load()
127    }
128
129    /// Record the write IO and update the statistics.
130    pub fn record_disk_write(&self, bytes: usize) {
131        self.disk_write_bytes.record(bytes);
132        self.disk_write_ios.record(self.throttle.iops_counter.count(bytes));
133    }
134
135    /// Record the read IO and update the statistics.
136    pub fn record_disk_read(&self, bytes: usize) {
137        self.disk_read_bytes.record(bytes);
138        self.disk_read_ios.record(self.throttle.iops_counter.count(bytes));
139    }
140
141    /// Get the nearest time to retry to check if there is quota for read ops.
142    ///
143    /// Return `Duration::ZERO` if no need to wait.
144    pub fn read_throttle(&self) -> Duration {
145        std::cmp::max(self.disk_read_bytes.throttle(), self.disk_read_ios.throttle())
146    }
147
148    /// Get the nearest time to retry to check if there is quota for write ops.
149    ///
150    /// Return `Duration::ZERO` if no need to wait.
151    pub fn write_throttle(&self) -> Duration {
152        std::cmp::max(self.disk_write_bytes.throttle(), self.disk_write_ios.throttle())
153    }
154
155    /// Check if the read ops are throttled.
156    pub fn is_read_throttled(&self) -> bool {
157        self.read_throttle() > Duration::ZERO
158    }
159
160    /// Check if the write ops are throttled.
161    pub fn is_write_throttled(&self) -> bool {
162        self.write_throttle() > Duration::ZERO
163    }
164
165    /// Get the throttle configuration.
166    pub fn throttle(&self) -> &Throttle {
167        &self.throttle
168    }
169}