• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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