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