• 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 //! `/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