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 //! `/sys/block/zram0/idle` marks idle pages based on boottime clock timestamp which keeps ticking 16 //! even while the device is suspended. This can end up marking relatively new pages as idle. For 17 //! example, when the threshold for idle page is 25 hours and the user suspends the device whole the 18 //! weekend (i.e. 2days), all pages in zram are marked as idle which is too aggressive. 19 //! 20 //! [SuspendHistory] mitigates the issue by adjusting the idle threshold by the actual duration of 21 //! the device is suspended because fixing the kernel to use monotonic clock instead of boottime 22 //! clock can break existing user space behavior. 23 //! 24 //! In this module, we don't use [std::time::Instant] because the Rust standard 25 //! library used in Android uses [libc::CLOCK_BOOTTIME] while the official Rust 26 //! standard library implementation uses [libc::CLOCK_MONOTONIC]. 27 28 #[cfg(test)] 29 mod tests; 30 31 use std::collections::VecDeque; 32 use std::marker::PhantomData; 33 use std::time::Duration; 34 35 use crate::time::BootTime; 36 use crate::time::MonotonicTime; 37 use crate::time::TimeApi; 38 39 /// Estimates the suspend duration by comparing the elapsed times on monotonic clock and boot time 40 /// clock. 41 /// 42 /// In Linux kernel, boot time is calculated as <monotonic time> + <boot time offset>. However the 43 /// kernel does not provides API to expose the boot time offset (The internal API is 44 /// `ktime_get_offs_boot_ns()`). 45 pub struct SuspendMonitor<T: TimeApi> { 46 monitonic_time: MonotonicTime, 47 boot_time: BootTime, 48 negative_adjustment: Duration, 49 _phantom_data: PhantomData<T>, 50 } 51 52 impl<T: TimeApi> SuspendMonitor<T> { 53 /// Creates [SuspendMonitor]. new() -> Self54 pub fn new() -> Self { 55 Self { 56 monitonic_time: T::get_monotonic_time(), 57 boot_time: T::get_boot_time(), 58 negative_adjustment: Duration::ZERO, 59 _phantom_data: PhantomData, 60 } 61 } 62 63 /// Estimate suspend duration by comparing the elapsed time between monotonic clock and boottime 64 /// clock. 65 /// 66 /// This returns the estimated suspend duration and the boot time timestamp of now. generate_suspend_duration(&mut self) -> (Duration, BootTime)67 pub fn generate_suspend_duration(&mut self) -> (Duration, BootTime) { 68 let monotonic_time = T::get_monotonic_time(); 69 let boot_time = T::get_boot_time(); 70 71 let monotonic_diff = monotonic_time.saturating_duration_since(self.monitonic_time); 72 let boot_diff = boot_time.saturating_duration_since(self.boot_time); 73 74 let suspend_duration = if boot_diff < monotonic_diff { 75 // Since kernel does not provide API to get both boot time and 76 // monotonic time atomically, the elapsed time on monotonic time 77 // can be longer than boot time. Store the diff in 78 // negative_adjustment and adjust it at the next call. 79 self.negative_adjustment = 80 self.negative_adjustment.saturating_add(monotonic_diff - boot_diff); 81 Duration::ZERO 82 } else { 83 let suspend_duration = boot_diff - monotonic_diff; 84 if suspend_duration >= self.negative_adjustment { 85 let negative_adjustment = self.negative_adjustment; 86 self.negative_adjustment = Duration::ZERO; 87 suspend_duration - negative_adjustment 88 } else { 89 self.negative_adjustment = 90 self.negative_adjustment.saturating_sub(suspend_duration); 91 Duration::ZERO 92 } 93 }; 94 95 self.monitonic_time = monotonic_time; 96 self.boot_time = boot_time; 97 98 (suspend_duration, boot_time) 99 } 100 } 101 102 impl<T: TimeApi> Default for SuspendMonitor<T> { default() -> Self103 fn default() -> Self { 104 Self::new() 105 } 106 } 107 108 struct Entry { 109 suspend_duration: Duration, 110 time: BootTime, 111 } 112 113 /// [SuspendHistory] tracks the duration of suspends. 114 /// 115 /// The adjustment duration is calculated by [SuspendHistory::calculate_total_suspend_duration]. 116 /// For example, if the idle threshold is 4 hours just after these usage log: 117 /// 118 /// * User suspends 1 hours (A) and use the device for 2 hours and, 119 /// * User suspends 5 hours (B) and use the device for 1 hours and, 120 /// * User suspends 2 hours (C) and use the device for 1 hours and, 121 /// * User suspends 1 hours (D) and use the device for 1 hours 122 /// 123 /// In this case, the threshold need to be adjusted by 8 hours (B + C + D). 124 /// 125 /// ``` 126 /// now 127 /// log : |-A-| |----B----| |--C--| |-D-| | 128 /// threshold : |---original---| 129 /// adjustment: |----B----|--C--|-D-| 130 /// ``` 131 /// 132 /// SuspendHistory uses deque to store the suspend logs. Each entry is 32 bytes. mmd will add a 133 /// record every hour and evict obsolete records. At worst case, Even if a user uses the device only 134 /// 10 seconds per hour and device is in suspend for 59 min 50 seconds, the history consumes only 135 /// 281KiB (= 32 bytes * 25 hours / (10 seconds / 3600 seconds)). Traversing < 300KiB on each zram 136 /// maintenance is an acceptable cost. 137 pub struct SuspendHistory { 138 history: VecDeque<Entry>, 139 total_awake_duration: Duration, 140 } 141 142 impl SuspendHistory { 143 /// Creates [SuspendHistory]. new() -> Self144 pub fn new() -> Self { 145 let mut history = VecDeque::new(); 146 history.push_front(Entry { suspend_duration: Duration::ZERO, time: BootTime::ZERO }); 147 Self { history, total_awake_duration: Duration::ZERO } 148 } 149 150 /// Add a record of suspended duration. 151 /// 152 /// This also evicts records which will exceeds `max_idle_duration`. record_suspend_duration( &mut self, suspend_duration: Duration, time: BootTime, max_idle_duration: Duration, )153 pub fn record_suspend_duration( 154 &mut self, 155 suspend_duration: Duration, 156 time: BootTime, 157 max_idle_duration: Duration, 158 ) { 159 // self.history never be empty while expired entries are popped out in the following loop 160 // because the loop pop one entry at a time only when self.history has at least 2 entries. 161 assert!(!self.history.is_empty()); 162 let awake_duration = time 163 .saturating_duration_since(self.history.front().expect("history is not empty").time) 164 .saturating_sub(suspend_duration); 165 self.total_awake_duration = self.total_awake_duration.saturating_add(awake_duration); 166 167 while self.total_awake_duration > max_idle_duration && self.history.len() >= 2 { 168 // The oldest entry must not None because the history had at least 2 entries. 169 let oldest_wake_at = self.history.pop_back().expect("history is not empty").time; 170 171 // oldest_entry must not None because the history had at least 2 entries. 172 let oldest_entry = self.history.back().expect("history had at least 2 entries"); 173 let oldest_awake_duration = oldest_entry 174 .time 175 .saturating_duration_since(oldest_wake_at) 176 .saturating_sub(oldest_entry.suspend_duration); 177 self.total_awake_duration = 178 self.total_awake_duration.saturating_sub(oldest_awake_duration); 179 } 180 181 self.history.push_front(Entry { suspend_duration, time }); 182 } 183 184 /// Calculates total suspend duration which overlaps the target_idle_duration. 185 /// 186 /// See the comment of [SuspendHistory] for details. calculate_total_suspend_duration( &self, target_idle_duration: Duration, now: BootTime, ) -> Duration187 pub fn calculate_total_suspend_duration( 188 &self, 189 target_idle_duration: Duration, 190 now: BootTime, 191 ) -> Duration { 192 let Some(target_time) = now.checked_sub(target_idle_duration) else { 193 return Duration::ZERO; 194 }; 195 196 let mut total_suspend_duration = Duration::ZERO; 197 198 for entry in self.history.iter() { 199 let Some(adjusted_target_time) = target_time.checked_sub(total_suspend_duration) else { 200 break; 201 }; 202 if entry.time > adjusted_target_time { 203 total_suspend_duration = 204 total_suspend_duration.saturating_add(entry.suspend_duration); 205 } else { 206 break; 207 } 208 } 209 210 total_suspend_duration 211 } 212 } 213 214 impl Default for SuspendHistory { default() -> Self215 fn default() -> Self { 216 Self::new() 217 } 218 } 219