1 // Copyright 2024, The Android Open Source Project
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 
15 //! This module provides the interface for CONFIG_ZRAM_MEMORY_TRACKING feature.
16 
17 use std::time::Duration;
18 
19 use crate::os::MeminfoApi;
20 use crate::zram::SysfsZramApi;
21 
22 /// Sets idle duration in seconds to "/sys/block/zram0/idle".
23 ///
24 /// Fractions of a second are truncated.
set_zram_idle_time<Z: SysfsZramApi>(idle_age: Duration) -> std::io::Result<()>25 pub fn set_zram_idle_time<Z: SysfsZramApi>(idle_age: Duration) -> std::io::Result<()> {
26     Z::set_idle(&idle_age.as_secs().to_string())
27 }
28 
29 /// This parses the content of "/proc/meminfo" and returns the number of "MemTotal" and
30 /// "MemAvailable".
31 ///
32 /// This does not care about the unit, because the user `calculate_idle_time()` use the values to
33 /// calculate memory utilization rate. The unit should be always "kB".
34 ///
35 /// This returns `None` if this fails to parse the content.
parse_meminfo(content: &str) -> Option<(u64, u64)>36 fn parse_meminfo(content: &str) -> Option<(u64, u64)> {
37     let mut total = None;
38     let mut available = None;
39     for line in content.split("\n") {
40         let container = if line.contains("MemTotal:") {
41             &mut total
42         } else if line.contains("MemAvailable:") {
43             &mut available
44         } else {
45             continue;
46         };
47         let Some(number_str) = line.split_whitespace().nth(1) else {
48             continue;
49         };
50         let Ok(number) = number_str.parse::<u64>() else {
51             continue;
52         };
53         *container = Some(number);
54     }
55     if let (Some(total), Some(available)) = (total, available) {
56         Some((total, available))
57     } else {
58         None
59     }
60 }
61 
62 /// Error from [calculate_idle_time].
63 #[derive(Debug, thiserror::Error)]
64 pub enum CalculateError {
65     /// min_idle is longer than max_idle
66     #[error("min_idle is longer than max_idle")]
67     InvalidMinAndMax,
68     /// failed to parse meminfo
69     #[error("failed to parse meminfo")]
70     InvalidMeminfo,
71     /// failed to read meminfo
72     #[error("failed to read meminfo: {0}")]
73     ReadMeminfo(std::io::Error),
74 }
75 
76 /// Calculates idle duration from min_idle and max_idle using meminfo.
calculate_idle_time<M: MeminfoApi>( min_idle: Duration, max_idle: Duration, ) -> std::result::Result<Duration, CalculateError>77 pub fn calculate_idle_time<M: MeminfoApi>(
78     min_idle: Duration,
79     max_idle: Duration,
80 ) -> std::result::Result<Duration, CalculateError> {
81     if min_idle > max_idle {
82         return Err(CalculateError::InvalidMinAndMax);
83     }
84     let content = match M::read_meminfo() {
85         Ok(v) => v,
86         Err(e) => return Err(CalculateError::ReadMeminfo(e)),
87     };
88     let (total, available) = match parse_meminfo(&content) {
89         Some((total, available)) if total > 0 => (total, available),
90         _ => {
91             // Fallback to use the safest value.
92             return Err(CalculateError::InvalidMeminfo);
93         }
94     };
95 
96     let mem_utilization = 1.0 - (available as f64) / (total as f64);
97 
98     // Exponentially decay the age vs. memory utilization. The reason we choose exponential decay is
99     // because we want to do as little work as possible when the system is under very low memory
100     // pressure. As pressure increases we want to start aggressively shrinking our idle age to force
101     // newer pages to be written back/recompressed.
102     const LAMBDA: f64 = 5.0;
103     let seconds = ((max_idle - min_idle).as_secs() as f64)
104         * std::f64::consts::E.powf(-LAMBDA * mem_utilization)
105         + (min_idle.as_secs() as f64);
106 
107     Ok(Duration::from_secs(seconds as u64))
108 }
109 
110 #[cfg(test)]
111 mod tests {
112     use super::*;
113 
114     use mockall::predicate::*;
115 
116     use crate::os::MockMeminfoApi;
117     use crate::os::MEMINFO_API_MTX;
118     use crate::zram::MockSysfsZramApi;
119     use crate::zram::ZRAM_API_MTX;
120 
121     #[test]
test_set_zram_idle_time()122     fn test_set_zram_idle_time() {
123         let _m = ZRAM_API_MTX.lock();
124         let mock = MockSysfsZramApi::set_idle_context();
125         mock.expect().with(eq("3600")).returning(|_| Ok(()));
126 
127         assert!(set_zram_idle_time::<MockSysfsZramApi>(Duration::from_secs(3600)).is_ok());
128     }
129 
130     #[test]
test_set_zram_idle_time_in_seconds()131     fn test_set_zram_idle_time_in_seconds() {
132         let _m = ZRAM_API_MTX.lock();
133         let mock = MockSysfsZramApi::set_idle_context();
134         mock.expect().with(eq("3600")).returning(|_| Ok(()));
135 
136         assert!(set_zram_idle_time::<MockSysfsZramApi>(Duration::from_millis(3600567)).is_ok());
137     }
138 
139     #[test]
test_parse_meminfo()140     fn test_parse_meminfo() {
141         let content = "MemTotal:       123456789 kB
142 MemFree:        12345 kB
143 MemAvailable:   67890 kB
144     ";
145         assert_eq!(parse_meminfo(content).unwrap(), (123456789, 67890));
146     }
147 
148     #[test]
test_parse_meminfo_invalid_format()149     fn test_parse_meminfo_invalid_format() {
150         // empty
151         assert!(parse_meminfo("").is_none());
152         // no number
153         let content = "MemTotal:
154 MemFree:        12345 kB
155 MemAvailable:   67890 kB
156     ";
157         assert!(parse_meminfo(content).is_none());
158         // no number
159         let content = "MemTotal:       kB
160 MemFree:        12345 kB
161 MemAvailable:   67890 kB
162     ";
163         assert!(parse_meminfo(content).is_none());
164         // total memory missing
165         let content = "MemFree:        12345 kB
166 MemAvailable:   67890 kB
167     ";
168         assert!(parse_meminfo(content).is_none());
169         // available memory missing
170         let content = "MemTotal:       123456789 kB
171 MemFree:        12345 kB
172     ";
173         assert!(parse_meminfo(content).is_none());
174     }
175 
176     #[test]
test_calculate_idle_time()177     fn test_calculate_idle_time() {
178         let _m = MEMINFO_API_MTX.lock();
179         let mock = MockMeminfoApi::read_meminfo_context();
180         let meminfo = "MemTotal: 8144296 kB
181     MemAvailable: 346452 kB";
182         mock.expect().returning(|| Ok(meminfo.to_string()));
183 
184         assert_eq!(
185             calculate_idle_time::<MockMeminfoApi>(
186                 Duration::from_secs(72000),
187                 Duration::from_secs(90000)
188             )
189             .unwrap(),
190             Duration::from_secs(72150)
191         );
192     }
193 
194     #[test]
test_calculate_idle_time_same_min_max()195     fn test_calculate_idle_time_same_min_max() {
196         let _m = MEMINFO_API_MTX.lock();
197         let mock = MockMeminfoApi::read_meminfo_context();
198         let meminfo = "MemTotal: 8144296 kB
199     MemAvailable: 346452 kB";
200         mock.expect().returning(|| Ok(meminfo.to_string()));
201 
202         assert_eq!(
203             calculate_idle_time::<MockMeminfoApi>(
204                 Duration::from_secs(90000),
205                 Duration::from_secs(90000)
206             )
207             .unwrap(),
208             Duration::from_secs(90000)
209         );
210     }
211 
212     #[test]
test_calculate_idle_time_min_is_bigger_than_max()213     fn test_calculate_idle_time_min_is_bigger_than_max() {
214         assert!(matches!(
215             calculate_idle_time::<MockMeminfoApi>(
216                 Duration::from_secs(90000),
217                 Duration::from_secs(72000)
218             ),
219             Err(CalculateError::InvalidMinAndMax)
220         ));
221     }
222 
223     #[test]
test_calculate_idle_time_no_available()224     fn test_calculate_idle_time_no_available() {
225         let _m = MEMINFO_API_MTX.lock();
226         let mock = MockMeminfoApi::read_meminfo_context();
227         let meminfo = "MemTotal: 8144296 kB
228     MemAvailable: 0 kB";
229         mock.expect().returning(|| Ok(meminfo.to_string()));
230 
231         assert_eq!(
232             calculate_idle_time::<MockMeminfoApi>(
233                 Duration::from_secs(72000),
234                 Duration::from_secs(90000)
235             )
236             .unwrap(),
237             Duration::from_secs(72121)
238         );
239     }
240 
241     #[test]
test_calculate_idle_time_meminfo_fail()242     fn test_calculate_idle_time_meminfo_fail() {
243         let _m = MEMINFO_API_MTX.lock();
244         let mock = MockMeminfoApi::read_meminfo_context();
245         mock.expect().returning(|| Err(std::io::Error::other("error")));
246 
247         assert!(matches!(
248             calculate_idle_time::<MockMeminfoApi>(
249                 Duration::from_secs(72000),
250                 Duration::from_secs(90000)
251             ),
252             Err(CalculateError::ReadMeminfo(_))
253         ));
254     }
255 
256     #[test]
test_calculate_idle_time_invalid_meminfo()257     fn test_calculate_idle_time_invalid_meminfo() {
258         let _m = MEMINFO_API_MTX.lock();
259         let mock = MockMeminfoApi::read_meminfo_context();
260         let meminfo = "";
261         mock.expect().returning(|| Ok(meminfo.to_string()));
262 
263         assert!(matches!(
264             calculate_idle_time::<MockMeminfoApi>(
265                 Duration::from_secs(72000),
266                 Duration::from_secs(90000)
267             ),
268             Err(CalculateError::InvalidMeminfo)
269         ));
270     }
271 
272     #[test]
test_calculate_idle_time_zero_total_memory()273     fn test_calculate_idle_time_zero_total_memory() {
274         let _m = MEMINFO_API_MTX.lock();
275         let mock = MockMeminfoApi::read_meminfo_context();
276         let meminfo = "MemTotal: 0 kB
277     MemAvailable: 346452 kB";
278         mock.expect().returning(|| Ok(meminfo.to_string()));
279 
280         assert!(matches!(
281             calculate_idle_time::<MockMeminfoApi>(
282                 Duration::from_secs(72000),
283                 Duration::from_secs(90000)
284             ),
285             Err(CalculateError::InvalidMeminfo)
286         ));
287     }
288 }
289