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 mockall::predicate::*;
113
114 use super::*;
115 use crate::os::MockMeminfoApi;
116 use crate::os::MEMINFO_API_MTX;
117 use crate::zram::MockSysfsZramApi;
118 use crate::zram::ZRAM_API_MTX;
119
120 #[test]
test_set_zram_idle_time()121 fn test_set_zram_idle_time() {
122 let _m = ZRAM_API_MTX.lock();
123 let mock = MockSysfsZramApi::set_idle_context();
124 mock.expect().with(eq("3600")).returning(|_| Ok(()));
125
126 assert!(set_zram_idle_time::<MockSysfsZramApi>(Duration::from_secs(3600)).is_ok());
127 }
128
129 #[test]
test_set_zram_idle_time_in_seconds()130 fn test_set_zram_idle_time_in_seconds() {
131 let _m = ZRAM_API_MTX.lock();
132 let mock = MockSysfsZramApi::set_idle_context();
133 mock.expect().with(eq("3600")).returning(|_| Ok(()));
134
135 assert!(set_zram_idle_time::<MockSysfsZramApi>(Duration::from_millis(3600567)).is_ok());
136 }
137
138 #[test]
test_parse_meminfo()139 fn test_parse_meminfo() {
140 let content = "MemTotal: 123456789 kB
141 MemFree: 12345 kB
142 MemAvailable: 67890 kB
143 ";
144 assert_eq!(parse_meminfo(content).unwrap(), (123456789, 67890));
145 }
146
147 #[test]
test_parse_meminfo_invalid_format()148 fn test_parse_meminfo_invalid_format() {
149 // empty
150 assert!(parse_meminfo("").is_none());
151 // no number
152 let content = "MemTotal:
153 MemFree: 12345 kB
154 MemAvailable: 67890 kB
155 ";
156 assert!(parse_meminfo(content).is_none());
157 // no number
158 let content = "MemTotal: kB
159 MemFree: 12345 kB
160 MemAvailable: 67890 kB
161 ";
162 assert!(parse_meminfo(content).is_none());
163 // total memory missing
164 let content = "MemFree: 12345 kB
165 MemAvailable: 67890 kB
166 ";
167 assert!(parse_meminfo(content).is_none());
168 // available memory missing
169 let content = "MemTotal: 123456789 kB
170 MemFree: 12345 kB
171 ";
172 assert!(parse_meminfo(content).is_none());
173 }
174
175 #[test]
test_calculate_idle_time()176 fn test_calculate_idle_time() {
177 let _m = MEMINFO_API_MTX.lock();
178 let mock = MockMeminfoApi::read_meminfo_context();
179 let meminfo = "MemTotal: 8144296 kB
180 MemAvailable: 346452 kB";
181 mock.expect().returning(|| Ok(meminfo.to_string()));
182
183 assert_eq!(
184 calculate_idle_time::<MockMeminfoApi>(
185 Duration::from_secs(72000),
186 Duration::from_secs(90000)
187 )
188 .unwrap(),
189 Duration::from_secs(72150)
190 );
191 }
192
193 #[test]
test_calculate_idle_time_same_min_max()194 fn test_calculate_idle_time_same_min_max() {
195 let _m = MEMINFO_API_MTX.lock();
196 let mock = MockMeminfoApi::read_meminfo_context();
197 let meminfo = "MemTotal: 8144296 kB
198 MemAvailable: 346452 kB";
199 mock.expect().returning(|| Ok(meminfo.to_string()));
200
201 assert_eq!(
202 calculate_idle_time::<MockMeminfoApi>(
203 Duration::from_secs(90000),
204 Duration::from_secs(90000)
205 )
206 .unwrap(),
207 Duration::from_secs(90000)
208 );
209 }
210
211 #[test]
test_calculate_idle_time_min_is_bigger_than_max()212 fn test_calculate_idle_time_min_is_bigger_than_max() {
213 assert!(matches!(
214 calculate_idle_time::<MockMeminfoApi>(
215 Duration::from_secs(90000),
216 Duration::from_secs(72000)
217 ),
218 Err(CalculateError::InvalidMinAndMax)
219 ));
220 }
221
222 #[test]
test_calculate_idle_time_no_available()223 fn test_calculate_idle_time_no_available() {
224 let _m = MEMINFO_API_MTX.lock();
225 let mock = MockMeminfoApi::read_meminfo_context();
226 let meminfo = "MemTotal: 8144296 kB
227 MemAvailable: 0 kB";
228 mock.expect().returning(|| Ok(meminfo.to_string()));
229
230 assert_eq!(
231 calculate_idle_time::<MockMeminfoApi>(
232 Duration::from_secs(72000),
233 Duration::from_secs(90000)
234 )
235 .unwrap(),
236 Duration::from_secs(72121)
237 );
238 }
239
240 #[test]
test_calculate_idle_time_meminfo_fail()241 fn test_calculate_idle_time_meminfo_fail() {
242 let _m = MEMINFO_API_MTX.lock();
243 let mock = MockMeminfoApi::read_meminfo_context();
244 mock.expect().returning(|| Err(std::io::Error::other("error")));
245
246 assert!(matches!(
247 calculate_idle_time::<MockMeminfoApi>(
248 Duration::from_secs(72000),
249 Duration::from_secs(90000)
250 ),
251 Err(CalculateError::ReadMeminfo(_))
252 ));
253 }
254
255 #[test]
test_calculate_idle_time_invalid_meminfo()256 fn test_calculate_idle_time_invalid_meminfo() {
257 let _m = MEMINFO_API_MTX.lock();
258 let mock = MockMeminfoApi::read_meminfo_context();
259 let meminfo = "";
260 mock.expect().returning(|| Ok(meminfo.to_string()));
261
262 assert!(matches!(
263 calculate_idle_time::<MockMeminfoApi>(
264 Duration::from_secs(72000),
265 Duration::from_secs(90000)
266 ),
267 Err(CalculateError::InvalidMeminfo)
268 ));
269 }
270
271 #[test]
test_calculate_idle_time_zero_total_memory()272 fn test_calculate_idle_time_zero_total_memory() {
273 let _m = MEMINFO_API_MTX.lock();
274 let mock = MockMeminfoApi::read_meminfo_context();
275 let meminfo = "MemTotal: 0 kB
276 MemAvailable: 346452 kB";
277 mock.expect().returning(|| Ok(meminfo.to_string()));
278
279 assert!(matches!(
280 calculate_idle_time::<MockMeminfoApi>(
281 Duration::from_secs(72000),
282 Duration::from_secs(90000)
283 ),
284 Err(CalculateError::InvalidMeminfo)
285 ));
286 }
287 }
288