1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 #include "base/util/memory_pressure/system_memory_pressure_evaluator_chromeos.h"
5
6 #include <fcntl.h>
7 #include <sys/poll.h>
8 #include <string>
9 #include <vector>
10
11 #include "base/bind.h"
12 #include "base/files/file_util.h"
13 #include "base/metrics/histogram_macros.h"
14 #include "base/no_destructor.h"
15 #include "base/posix/eintr_wrapper.h"
16 #include "base/single_thread_task_runner.h"
17 #include "base/strings/string_number_conversions.h"
18 #include "base/strings/string_split.h"
19 #include "base/strings/string_util.h"
20 #include "base/system/sys_info.h"
21 #include "base/task/post_task.h"
22 #include "base/task/thread_pool.h"
23 #include "base/threading/scoped_blocking_call.h"
24 #include "base/threading/thread_task_runner_handle.h"
25 #include "base/time/time.h"
26
27 namespace util {
28 namespace chromeos {
29
30 const base::Feature kCrOSUserSpaceLowMemoryNotification{
31 "CrOSUserSpaceLowMemoryNotification", base::FEATURE_DISABLED_BY_DEFAULT};
32
33 namespace {
34 // Pointer to the SystemMemoryPressureEvaluator used by TabManagerDelegate for
35 // chromeos to need to call into ScheduleEarlyCheck.
36 SystemMemoryPressureEvaluator* g_system_evaluator = nullptr;
37
38 // We try not to re-notify on moderate too frequently, this time
39 // controls how frequently we will notify after our first notification.
40 constexpr base::TimeDelta kModerateMemoryPressureCooldownTime =
41 base::TimeDelta::FromSeconds(10);
42
43 // The margin mem file contains the two memory levels, the first is the
44 // critical level and the second is the moderate level. Note, this
45 // file may contain more values but only the first two are used for
46 // memory pressure notifications in chromeos.
47 constexpr char kMarginMemFile[] = "/sys/kernel/mm/chromeos-low_mem/margin";
48
49 // The available memory file contains the available memory as determined
50 // by the kernel.
51 constexpr char kAvailableMemFile[] =
52 "/sys/kernel/mm/chromeos-low_mem/available";
53
54 // The reserved file cache.
55 constexpr char kMinFilelist[] = "/proc/sys/vm/min_filelist_kbytes";
56
57 // The estimation of how well zram based swap is compressed.
58 constexpr char kRamVsSwapWeight[] =
59 "/sys/kernel/mm/chromeos-low_mem/ram_vs_swap_weight";
60
61 // Converts an available memory value in MB to a memory pressure level.
62 base::MemoryPressureListener::MemoryPressureLevel
GetMemoryPressureLevelFromAvailable(int available_mb,int moderate_avail_mb,int critical_avail_mb)63 GetMemoryPressureLevelFromAvailable(int available_mb,
64 int moderate_avail_mb,
65 int critical_avail_mb) {
66 if (available_mb < critical_avail_mb)
67 return base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL;
68 if (available_mb < moderate_avail_mb)
69 return base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE;
70
71 return base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE;
72 }
73
ReadFileToUint64(const base::FilePath & file)74 uint64_t ReadFileToUint64(const base::FilePath& file) {
75 std::string file_contents;
76 if (!ReadFileToString(file, &file_contents))
77 return 0;
78 TrimWhitespaceASCII(file_contents, base::TRIM_ALL, &file_contents);
79 uint64_t file_contents_uint64 = 0;
80 if (!base::StringToUint64(file_contents, &file_contents_uint64))
81 return 0;
82 return file_contents_uint64;
83 }
84
ReadAvailableMemoryMB(int available_fd)85 uint64_t ReadAvailableMemoryMB(int available_fd) {
86 // Read the available memory.
87 char buf[32] = {};
88
89 // kernfs/file.c:
90 // "Once poll/select indicates that the value has changed, you
91 // need to close and re-open the file, or seek to 0 and read again.
92 ssize_t bytes_read = HANDLE_EINTR(pread(available_fd, buf, sizeof(buf), 0));
93 PCHECK(bytes_read != -1);
94
95 std::string mem_str(buf, bytes_read);
96 uint64_t available = std::numeric_limits<uint64_t>::max();
97 CHECK(base::StringToUint64(
98 base::TrimWhitespaceASCII(mem_str, base::TrimPositions::TRIM_ALL),
99 &available));
100
101 return available;
102 }
103
104 // This function will wait until the /sys/kernel/mm/chromeos-low_mem/available
105 // file becomes readable and then read the latest value. This file will only
106 // become readable once the available memory cross through one of the margin
107 // values specified in /sys/kernel/mm/chromeos-low_mem/margin, for more
108 // details see https://crrev.com/c/536336.
WaitForMemoryPressureChanges(int available_fd)109 bool WaitForMemoryPressureChanges(int available_fd) {
110 base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
111 base::BlockingType::WILL_BLOCK);
112
113 pollfd pfd = {available_fd, POLLPRI | POLLERR, 0};
114 int res = HANDLE_EINTR(poll(&pfd, 1, -1)); // Wait indefinitely.
115 PCHECK(res != -1);
116
117 if (pfd.revents != (POLLPRI | POLLERR)) {
118 // If we didn't receive POLLPRI | POLLERR it means we likely received
119 // POLLNVAL because the fd has been closed we will only log an error in
120 // other situations.
121 LOG_IF(ERROR, pfd.revents != POLLNVAL)
122 << "WaitForMemoryPressureChanges received unexpected revents: "
123 << pfd.revents;
124
125 // We no longer want to wait for a kernel notification if the fd has been
126 // closed.
127 return false;
128 }
129
130 return true;
131 }
132
133 } // namespace
134
SystemMemoryPressureEvaluator(std::unique_ptr<MemoryPressureVoter> voter)135 SystemMemoryPressureEvaluator::SystemMemoryPressureEvaluator(
136 std::unique_ptr<MemoryPressureVoter> voter)
137 : SystemMemoryPressureEvaluator(
138 kMarginMemFile,
139 kAvailableMemFile,
140 base::BindRepeating(&WaitForMemoryPressureChanges),
141 /*disable_timer_for_testing*/ false,
142 base::FeatureList::IsEnabled(
143 chromeos::kCrOSUserSpaceLowMemoryNotification),
144 std::move(voter)) {}
145
SystemMemoryPressureEvaluator(const std::string & margin_file,const std::string & available_file,base::RepeatingCallback<bool (int)> kernel_waiting_callback,bool disable_timer_for_testing,bool is_user_space_notify,std::unique_ptr<MemoryPressureVoter> voter)146 SystemMemoryPressureEvaluator::SystemMemoryPressureEvaluator(
147 const std::string& margin_file,
148 const std::string& available_file,
149 base::RepeatingCallback<bool(int)> kernel_waiting_callback,
150 bool disable_timer_for_testing,
151 bool is_user_space_notify,
152 std::unique_ptr<MemoryPressureVoter> voter)
153 : util::SystemMemoryPressureEvaluator(std::move(voter)),
154 is_user_space_notify_(is_user_space_notify),
155 weak_ptr_factory_(this) {
156 DCHECK(g_system_evaluator == nullptr);
157 g_system_evaluator = this;
158
159 std::vector<int> margin_parts =
160 SystemMemoryPressureEvaluator::GetMarginFileParts(margin_file);
161
162 // This class SHOULD have verified kernel support by calling
163 // SupportsKernelNotifications() before creating a new instance of this.
164 // Therefore we will check fail if we don't have multiple margin values.
165 CHECK_LE(2u, margin_parts.size());
166 critical_pressure_threshold_mb_ = margin_parts[0];
167 moderate_pressure_threshold_mb_ = margin_parts[1];
168
169 UpdateMemoryParameters();
170
171 if (!is_user_space_notify_) {
172 kernel_waiting_callback_ = base::BindRepeating(
173 std::move(kernel_waiting_callback), available_mem_file_.get());
174 available_mem_file_ =
175 base::ScopedFD(HANDLE_EINTR(open(available_file.c_str(), O_RDONLY)));
176 CHECK(available_mem_file_.is_valid());
177
178 ScheduleWaitForKernelNotification();
179 }
180
181 if (!disable_timer_for_testing) {
182 // We will check the memory pressure and report the metric
183 // (ChromeOS.MemoryPressureLevel) every 1 second.
184 checking_timer_.Start(
185 FROM_HERE, base::TimeDelta::FromSeconds(1),
186 base::BindRepeating(&SystemMemoryPressureEvaluator::
187 CheckMemoryPressureAndRecordStatistics,
188 weak_ptr_factory_.GetWeakPtr()));
189 }
190 }
~SystemMemoryPressureEvaluator()191 SystemMemoryPressureEvaluator::~SystemMemoryPressureEvaluator() {
192 DCHECK(g_system_evaluator);
193 g_system_evaluator = nullptr;
194 }
195
196 // static
Get()197 SystemMemoryPressureEvaluator* SystemMemoryPressureEvaluator::Get() {
198 return g_system_evaluator;
199 }
200
GetMarginFileParts()201 std::vector<int> SystemMemoryPressureEvaluator::GetMarginFileParts() {
202 static const base::NoDestructor<std::vector<int>> margin_file_parts(
203 GetMarginFileParts(kMarginMemFile));
204 return *margin_file_parts;
205 }
206
GetMarginFileParts(const std::string & file)207 std::vector<int> SystemMemoryPressureEvaluator::GetMarginFileParts(
208 const std::string& file) {
209 std::vector<int> margin_values;
210 std::string margin_contents;
211 if (base::ReadFileToString(base::FilePath(file), &margin_contents)) {
212 std::vector<std::string> margins =
213 base::SplitString(margin_contents, base::kWhitespaceASCII,
214 base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
215 for (const auto& v : margins) {
216 int value = -1;
217 if (!base::StringToInt(v, &value)) {
218 // If any of the values weren't parseable as an int we return
219 // nothing as the file format is unexpected.
220 LOG(ERROR) << "Unable to parse margin file contents as integer: " << v;
221 return std::vector<int>();
222 }
223 margin_values.push_back(value);
224 }
225 } else {
226 LOG(ERROR) << "Unable to read margin file: " << kMarginMemFile;
227 }
228 return margin_values;
229 }
230
231 // CalculateReservedFreeKB() calculates the reserved free memory in KiB from
232 // /proc/zoneinfo. Reserved pages are free pages reserved for emergent kernel
233 // allocation and are not available to the user space. It's the sum of high
234 // watermarks and max protection pages of memory zones. It implements the same
235 // reserved pages calculation in linux kernel calculate_totalreserve_pages().
236 //
237 // /proc/zoneinfo example:
238 // ...
239 // Node 0, zone DMA32
240 // pages free 422432
241 // min 16270
242 // low 20337
243 // high 24404
244 // ...
245 // protection: (0, 0, 1953, 1953)
246 //
247 // The high field is the high watermark for this zone. The protection field is
248 // the protected pages for lower zones. See the lowmem_reserve_ratio section in
249 // https://www.kernel.org/doc/Documentation/sysctl/vm.txt.
CalculateReservedFreeKB(const std::string & zoneinfo)250 uint64_t SystemMemoryPressureEvaluator::CalculateReservedFreeKB(
251 const std::string& zoneinfo) {
252 constexpr uint64_t kPageSizeKB = 4;
253
254 uint64_t num_reserved_pages = 0;
255 for (const base::StringPiece& line : base::SplitStringPiece(
256 zoneinfo, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
257 std::vector<base::StringPiece> tokens = base::SplitStringPiece(
258 line, base::kWhitespaceASCII, base::TRIM_WHITESPACE,
259 base::SPLIT_WANT_NONEMPTY);
260
261 // Skip the line if there are not enough tokens.
262 if (tokens.size() < 2) {
263 continue;
264 }
265
266 if (tokens[0] == "high") {
267 // Parse the high watermark.
268 uint64_t high = 0;
269 if (base::StringToUint64(tokens[1], &high)) {
270 num_reserved_pages += high;
271 } else {
272 LOG(ERROR) << "Couldn't parse the high field in /proc/zoneinfo: "
273 << tokens[1];
274 }
275 } else if (tokens[0] == "protection:") {
276 // Parse the protection pages.
277 uint64_t max = 0;
278 for (size_t i = 1; i < tokens.size(); ++i) {
279 uint64_t num = 0;
280 base::StringPiece entry;
281 if (i == 1) {
282 // Exclude the leading '(' and the trailing ','.
283 entry = tokens[i].substr(1, tokens[i].size() - 2);
284 } else {
285 // Exclude the trailing ',' or ')'.
286 entry = tokens[i].substr(0, tokens[i].size() - 1);
287 }
288 if (base::StringToUint64(entry, &num)) {
289 max = std::max(max, num);
290 } else {
291 LOG(ERROR)
292 << "Couldn't parse the protection field in /proc/zoneinfo: "
293 << entry;
294 }
295 }
296 num_reserved_pages += max;
297 }
298 }
299
300 return num_reserved_pages * kPageSizeKB;
301 }
302
GetReservedMemoryKB()303 uint64_t SystemMemoryPressureEvaluator::GetReservedMemoryKB() {
304 std::string file_contents;
305 if (!ReadFileToString(base::FilePath("/proc/zoneinfo"), &file_contents)) {
306 LOG(ERROR) << "Couldn't get /proc/zoneinfo";
307 return 0;
308 }
309 return CalculateReservedFreeKB(file_contents);
310 }
311
312 // CalculateAvailableMemoryUserSpaceKB implements the same available memory
313 // calculation as kernel function get_available_mem_adj(). The available memory
314 // consists of 3 parts: the free memory, the file cache, and the swappable
315 // memory. The available free memory is free memory minus reserved free memory.
316 // The available file cache is the total file cache minus reserved file cache
317 // (min_filelist). Because swapping is prohibited if there is no anonymous
318 // memory or no swap free, the swappable memory is the minimal of anonymous
319 // memory and swap free. As swapping memory is more costly than dropping file
320 // cache, only a fraction (1 / ram_swap_weight) of the swappable memory
321 // contributes to the available memory.
CalculateAvailableMemoryUserSpaceKB(const base::SystemMemoryInfoKB & info,uint64_t reserved_free,uint64_t min_filelist,uint64_t ram_swap_weight)322 uint64_t SystemMemoryPressureEvaluator::CalculateAvailableMemoryUserSpaceKB(
323 const base::SystemMemoryInfoKB& info,
324 uint64_t reserved_free,
325 uint64_t min_filelist,
326 uint64_t ram_swap_weight) {
327 const uint64_t free = info.free;
328 const uint64_t anon = info.active_anon + info.inactive_anon;
329 const uint64_t file = info.active_file + info.inactive_file;
330 const uint64_t dirty = info.dirty;
331 const uint64_t swap_free = info.swap_free;
332
333 uint64_t available = (free > reserved_free) ? free - reserved_free : 0;
334 available += (file > dirty + min_filelist) ? file - dirty - min_filelist : 0;
335 available += std::min<uint64_t>(anon, swap_free) / ram_swap_weight;
336
337 return available;
338 }
339
GetAvailableMemoryKB()340 uint64_t SystemMemoryPressureEvaluator::GetAvailableMemoryKB() {
341 if (is_user_space_notify_) {
342 base::SystemMemoryInfoKB info;
343 CHECK(base::GetSystemMemoryInfo(&info));
344 return CalculateAvailableMemoryUserSpaceKB(info, reserved_free_,
345 min_filelist_, ram_swap_weight_);
346 } else {
347 const uint64_t available_mem_mb =
348 ReadAvailableMemoryMB(available_mem_file_.get());
349 return available_mem_mb * 1024;
350 }
351 }
352
SupportsKernelNotifications()353 bool SystemMemoryPressureEvaluator::SupportsKernelNotifications() {
354 // Unfortunately at the moment the only way to determine if the chromeos
355 // kernel supports polling on the available file is to observe two values
356 // in the margin file, if the critical and moderate levels are specified
357 // there then we know the kernel must support polling on available.
358 return SystemMemoryPressureEvaluator::GetMarginFileParts().size() >= 2;
359 }
360
361 // CheckMemoryPressure will get the current memory pressure level by reading
362 // the available file.
CheckMemoryPressure()363 void SystemMemoryPressureEvaluator::CheckMemoryPressure() {
364 DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
365
366 auto old_vote = current_vote();
367
368 uint64_t mem_avail_mb = GetAvailableMemoryKB() / 1024;
369
370 SetCurrentVote(GetMemoryPressureLevelFromAvailable(
371 mem_avail_mb, moderate_pressure_threshold_mb_,
372 critical_pressure_threshold_mb_));
373 bool notify = true;
374
375 if (current_vote() ==
376 base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE) {
377 last_moderate_notification_ = base::TimeTicks();
378 notify = false;
379 }
380
381 // In the case of MODERATE memory pressure we may be in this state for quite
382 // some time so we limit the rate at which we dispatch notifications.
383 else if (current_vote() ==
384 base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE) {
385 if (old_vote == current_vote()) {
386 if (base::TimeTicks::Now() - last_moderate_notification_ <
387 kModerateMemoryPressureCooldownTime) {
388 notify = false;
389 } else if (old_vote ==
390 base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) {
391 // Reset the moderate notification time if we just crossed back.
392 last_moderate_notification_ = base::TimeTicks::Now();
393 notify = false;
394 }
395 }
396
397 if (notify)
398 last_moderate_notification_ = base::TimeTicks::Now();
399 }
400
401 VLOG(1) << "SystemMemoryPressureEvaluator::CheckMemoryPressure dispatching "
402 "at level: "
403 << current_vote();
404 SendCurrentVote(notify);
405 }
406
HandleKernelNotification(bool result)407 void SystemMemoryPressureEvaluator::HandleKernelNotification(bool result) {
408 DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
409
410 // If WaitForKernelNotification returned false then the FD has been closed and
411 // we just exit without waiting again.
412 if (!result) {
413 return;
414 }
415
416 CheckMemoryPressure();
417
418 // Now we need to schedule back our blocking task to wait for more
419 // kernel notifications.
420 ScheduleWaitForKernelNotification();
421 }
422
CheckMemoryPressureAndRecordStatistics()423 void SystemMemoryPressureEvaluator::CheckMemoryPressureAndRecordStatistics() {
424 DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
425
426 // Note: If we support notifications of memory pressure changes in both
427 // directions we will not have to update the cached value as it will always
428 // be correct.
429 CheckMemoryPressure();
430
431 // Record UMA histogram statistics for the current memory pressure level, it
432 // would seem that only Memory.PressureLevel would be necessary.
433 constexpr int kNumberPressureLevels = 3;
434 UMA_HISTOGRAM_ENUMERATION("ChromeOS.MemoryPressureLevel", current_vote(),
435 kNumberPressureLevels);
436 }
437
ScheduleEarlyCheck()438 void SystemMemoryPressureEvaluator::ScheduleEarlyCheck() {
439 DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
440
441 base::ThreadTaskRunnerHandle::Get()->PostTask(
442 FROM_HERE,
443 base::BindOnce(&SystemMemoryPressureEvaluator::CheckMemoryPressure,
444 weak_ptr_factory_.GetWeakPtr()));
445 }
446
UpdateMemoryParameters()447 void SystemMemoryPressureEvaluator::UpdateMemoryParameters() {
448 if (is_user_space_notify_) {
449 reserved_free_ = GetReservedMemoryKB();
450 min_filelist_ = ReadFileToUint64(base::FilePath(kMinFilelist));
451 ram_swap_weight_ = ReadFileToUint64(base::FilePath(kRamVsSwapWeight));
452 }
453 }
454
ScheduleWaitForKernelNotification()455 void SystemMemoryPressureEvaluator::ScheduleWaitForKernelNotification() {
456 DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
457
458 base::ThreadPool::PostTaskAndReplyWithResult(
459 FROM_HERE,
460 {base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
461 base::BindOnce(kernel_waiting_callback_),
462 base::BindOnce(&SystemMemoryPressureEvaluator::HandleKernelNotification,
463 weak_ptr_factory_.GetWeakPtr()));
464 }
465
466 } // namespace chromeos
467 } // namespace util
468