Roll src/third_party/WebKit eac3800:0237a66 (svn 202606:202607)
[chromium-blink-merge.git] / chrome / browser / memory / oom_priority_manager.cc
blobe56b769e7561cd64cb51c85dc1ef759ea59b134e
1 // Copyright (c) 2012 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.
5 #include "chrome/browser/memory/oom_priority_manager.h"
7 #include <algorithm>
8 #include <set>
9 #include <vector>
11 #include "ash/multi_profile_uma.h"
12 #include "ash/session/session_state_delegate.h"
13 #include "ash/shell.h"
14 #include "base/bind.h"
15 #include "base/bind_helpers.h"
16 #include "base/command_line.h"
17 #include "base/memory/memory_pressure_monitor.h"
18 #include "base/metrics/field_trial.h"
19 #include "base/metrics/histogram.h"
20 #include "base/process/process.h"
21 #include "base/strings/string16.h"
22 #include "base/strings/string_number_conversions.h"
23 #include "base/strings/string_util.h"
24 #include "base/strings/utf_string_conversions.h"
25 #include "base/threading/thread.h"
26 #include "build/build_config.h"
27 #include "chrome/browser/browser_process.h"
28 #include "chrome/browser/memory/oom_memory_details.h"
29 #include "chrome/browser/memory/system_memory_stats_recorder.h"
30 #include "chrome/browser/ui/browser.h"
31 #include "chrome/browser/ui/browser_iterator.h"
32 #include "chrome/browser/ui/browser_list.h"
33 #include "chrome/browser/ui/host_desktop.h"
34 #include "chrome/browser/ui/tab_contents/tab_contents_iterator.h"
35 #include "chrome/browser/ui/tabs/tab_discard_state.h"
36 #include "chrome/browser/ui/tabs/tab_strip_model.h"
37 #include "chrome/browser/ui/tabs/tab_utils.h"
38 #include "chrome/common/chrome_constants.h"
39 #include "chrome/common/url_constants.h"
40 #include "content/public/browser/browser_thread.h"
41 #include "content/public/browser/render_process_host.h"
42 #include "content/public/browser/web_contents.h"
44 #if defined(OS_CHROMEOS)
45 #include "chrome/browser/memory/oom_priority_manager_delegate_chromeos.h"
46 #endif
48 using base::TimeDelta;
49 using base::TimeTicks;
50 using content::BrowserThread;
51 using content::WebContents;
53 namespace memory {
54 namespace {
56 // The default interval in seconds after which to adjust the oom_score_adj
57 // value.
58 const int kAdjustmentIntervalSeconds = 10;
60 // For each period of this length we record a statistic to indicate whether
61 // or not the user experienced a low memory event. If you change this interval
62 // you must replace Tabs.Discard.DiscardInLastMinute with a new statistic.
63 const int kRecentTabDiscardIntervalSeconds = 60;
65 // If there has been no priority adjustment in this interval, we assume the
66 // machine was suspended and correct our timing statistics.
67 const int kSuspendThresholdSeconds = kAdjustmentIntervalSeconds * 4;
69 // Returns a unique ID for a WebContents. Do not cast back to a pointer, as
70 // the WebContents could be deleted if the user closed the tab.
71 int64 IdFromWebContents(WebContents* web_contents) {
72 return reinterpret_cast<int64>(web_contents);
75 int FindTabStripModelById(int64 target_web_contents_id, TabStripModel** model) {
76 DCHECK(model);
77 for (chrome::BrowserIterator it; !it.done(); it.Next()) {
78 Browser* browser = *it;
79 TabStripModel* local_model = browser->tab_strip_model();
80 for (int idx = 0; idx < local_model->count(); idx++) {
81 WebContents* web_contents = local_model->GetWebContentsAt(idx);
82 int64 web_contents_id = IdFromWebContents(web_contents);
83 if (web_contents_id == target_web_contents_id) {
84 *model = local_model;
85 return idx;
90 return -1;
93 } // namespace
95 ////////////////////////////////////////////////////////////////////////////////
96 // OomPriorityManager
98 OomPriorityManager::OomPriorityManager()
99 : discard_count_(0), recent_tab_discard_(false), discard_once_(false) {
100 #if defined(OS_CHROMEOS)
101 delegate_.reset(new OomPriorityManagerDelegate);
102 #endif
105 OomPriorityManager::~OomPriorityManager() {
106 Stop();
109 void OomPriorityManager::Start(bool discard_once) {
110 discard_once_ = discard_once;
111 if (!update_timer_.IsRunning()) {
112 update_timer_.Start(FROM_HERE,
113 TimeDelta::FromSeconds(kAdjustmentIntervalSeconds),
114 this, &OomPriorityManager::UpdateTimerCallback);
116 if (!recent_tab_discard_timer_.IsRunning()) {
117 recent_tab_discard_timer_.Start(
118 FROM_HERE, TimeDelta::FromSeconds(kRecentTabDiscardIntervalSeconds),
119 this, &OomPriorityManager::RecordRecentTabDiscard);
121 start_time_ = TimeTicks::Now();
122 // Create a |MemoryPressureListener| to listen for memory events.
123 base::MemoryPressureMonitor* monitor = base::MemoryPressureMonitor::Get();
124 if (monitor) {
125 memory_pressure_listener_.reset(new base::MemoryPressureListener(base::Bind(
126 &OomPriorityManager::OnMemoryPressure, base::Unretained(this))));
127 base::MemoryPressureListener::MemoryPressureLevel level =
128 monitor->GetCurrentPressureLevel();
129 if (level == base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) {
130 OnMemoryPressure(level);
135 void OomPriorityManager::Stop() {
136 update_timer_.Stop();
137 recent_tab_discard_timer_.Stop();
138 memory_pressure_listener_.reset();
141 // Things we need to collect on the browser thread (because TabStripModel isn't
142 // thread safe):
143 // 1) whether or not a tab is pinned
144 // 2) last time a tab was selected
145 // 3) is the tab currently selected
146 TabStatsList OomPriorityManager::GetTabStats() {
147 DCHECK_CURRENTLY_ON(BrowserThread::UI);
148 TabStatsList stats_list;
149 stats_list.reserve(32); // 99% of users have < 30 tabs open
151 // We go through each window to get all the tabs. Depending on the platform,
152 // windows are either native or ash or both. We want to make sure to go
153 // through them all, starting with the active window first (we use
154 // chrome::GetActiveDesktop to get the current used type).
155 AddTabStats(BrowserList::GetInstance(chrome::GetActiveDesktop()), true,
156 &stats_list);
157 if (chrome::GetActiveDesktop() != chrome::HOST_DESKTOP_TYPE_NATIVE) {
158 AddTabStats(BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_NATIVE),
159 false, &stats_list);
160 } else if (chrome::GetActiveDesktop() != chrome::HOST_DESKTOP_TYPE_ASH) {
161 AddTabStats(BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_ASH), false,
162 &stats_list);
165 // Sort the data we collected so that least desirable to be
166 // killed is first, most desirable is last.
167 std::sort(stats_list.begin(), stats_list.end(), CompareTabStats);
168 return stats_list;
171 // TODO(jamescook): This should consider tabs with references to other tabs,
172 // such as tabs created with JavaScript window.open(). We might want to
173 // discard the entire set together, or use that in the priority computation.
174 bool OomPriorityManager::DiscardTab() {
175 DCHECK_CURRENTLY_ON(BrowserThread::UI);
176 TabStatsList stats = GetTabStats();
177 if (stats.empty())
178 return false;
179 // Loop until we find a non-discarded tab to kill.
180 for (TabStatsList::const_reverse_iterator stats_rit = stats.rbegin();
181 stats_rit != stats.rend(); ++stats_rit) {
182 int64 least_important_tab_id = stats_rit->tab_contents_id;
183 if (CanDiscardTab(least_important_tab_id) &&
184 DiscardTabById(least_important_tab_id))
185 return true;
187 return false;
190 bool OomPriorityManager::DiscardTabById(int64 target_web_contents_id) {
191 TabStripModel* model;
192 int idx = FindTabStripModelById(target_web_contents_id, &model);
194 if (idx == -1)
195 return false;
197 // Can't discard active tabs
198 if (model->active_index() == idx)
199 return false;
201 WebContents* web_contents = model->GetWebContentsAt(idx);
202 // Can't discard tabs that are already discarded.
203 if (TabDiscardState::IsDiscarded(web_contents))
204 return false;
206 VLOG(1) << "Discarding tab " << idx << " id " << target_web_contents_id;
207 // Record statistics before discarding because we want to capture the
208 // memory state that lead to the discard.
209 RecordDiscardStatistics();
210 model->DiscardWebContentsAt(idx);
211 recent_tab_discard_ = true;
213 return true;
216 void OomPriorityManager::LogMemoryAndDiscardTab() {
217 LogMemory("Tab Discards Memory details",
218 base::Bind(&OomPriorityManager::PurgeMemoryAndDiscardTab));
221 void OomPriorityManager::LogMemory(const std::string& title,
222 const base::Closure& callback) {
223 DCHECK_CURRENTLY_ON(BrowserThread::UI);
224 OomMemoryDetails::Log(title, callback);
227 ///////////////////////////////////////////////////////////////////////////////
228 // OomPriorityManager, private:
230 // static
231 void OomPriorityManager::PurgeMemoryAndDiscardTab() {
232 if (g_browser_process && g_browser_process->GetOomPriorityManager()) {
233 OomPriorityManager* manager = g_browser_process->GetOomPriorityManager();
234 manager->PurgeBrowserMemory();
235 manager->DiscardTab();
239 // static
240 bool OomPriorityManager::IsInternalPage(const GURL& url) {
241 // There are many chrome:// UI URLs, but only look for the ones that users
242 // are likely to have open. Most of the benefit is the from NTP URL.
243 const char* const kInternalPagePrefixes[] = {
244 chrome::kChromeUIDownloadsURL,
245 chrome::kChromeUIHistoryURL,
246 chrome::kChromeUINewTabURL,
247 chrome::kChromeUISettingsURL,
249 // Prefix-match against the table above. Use strncmp to avoid allocating
250 // memory to convert the URL prefix constants into std::strings.
251 for (size_t i = 0; i < arraysize(kInternalPagePrefixes); ++i) {
252 if (!strncmp(url.spec().c_str(), kInternalPagePrefixes[i],
253 strlen(kInternalPagePrefixes[i])))
254 return true;
256 return false;
259 void OomPriorityManager::RecordDiscardStatistics() {
260 // Record a raw count so we can compare to discard reloads.
261 discard_count_++;
262 UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.Discard.DiscardCount", discard_count_, 1,
263 1000, 50);
265 // TODO(jamescook): Maybe incorporate extension count?
266 UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.Discard.TabCount", GetTabCount(), 1, 100,
267 50);
268 #if defined(OS_CHROMEOS)
269 // Record the discarded tab in relation to the amount of simultaneously
270 // logged in users.
271 ash::MultiProfileUMA::RecordDiscardedTab(ash::Shell::GetInstance()
272 ->session_state_delegate()
273 ->NumberOfLoggedInUsers());
274 #endif
275 // TODO(jamescook): If the time stats prove too noisy, then divide up users
276 // based on how heavily they use Chrome using tab count as a proxy.
277 // Bin into <= 1, <= 2, <= 4, <= 8, etc.
278 if (last_discard_time_.is_null()) {
279 // This is the first discard this session.
280 TimeDelta interval = TimeTicks::Now() - start_time_;
281 int interval_seconds = static_cast<int>(interval.InSeconds());
282 // Record time in seconds over an interval of approximately 1 day.
283 UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.Discard.InitialTime2", interval_seconds,
284 1, 100000, 50);
285 } else {
286 // Not the first discard, so compute time since last discard.
287 TimeDelta interval = TimeTicks::Now() - last_discard_time_;
288 int interval_ms = static_cast<int>(interval.InMilliseconds());
289 // Record time in milliseconds over an interval of approximately 1 day.
290 // Start at 100 ms to get extra resolution in the target 750 ms range.
291 UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.Discard.IntervalTime2", interval_ms, 100,
292 100000 * 1000, 50);
294 // TODO(georgesak): Remove this #if when RecordMemoryStats is implemented for
295 // all platforms.
296 #if defined(OS_WIN) || defined(OS_CHROMEOS)
297 // Record system memory usage at the time of the discard.
298 RecordMemoryStats(RECORD_MEMORY_STATS_TAB_DISCARDED);
299 #endif
300 // Set up to record the next interval.
301 last_discard_time_ = TimeTicks::Now();
304 void OomPriorityManager::RecordRecentTabDiscard() {
305 // If we are shutting down, do not do anything.
306 if (g_browser_process->IsShuttingDown())
307 return;
309 DCHECK_CURRENTLY_ON(BrowserThread::UI);
310 // If we change the interval we need to change the histogram name.
311 UMA_HISTOGRAM_BOOLEAN("Tabs.Discard.DiscardInLastMinute",
312 recent_tab_discard_);
313 // Reset for the next interval.
314 recent_tab_discard_ = false;
317 void OomPriorityManager::PurgeBrowserMemory() {
318 // Based on experimental evidence, attempts to free memory from renderers
319 // have been too slow to use in OOM situations (V8 garbage collection) or
320 // do not lead to persistent decreased usage (image/bitmap caches). This
321 // function therefore only targets large blocks of memory in the browser.
322 // Note that other objects will listen to MemoryPressureListener events
323 // to release memory.
324 for (TabContentsIterator it; !it.done(); it.Next()) {
325 WebContents* web_contents = *it;
326 // Screenshots can consume ~5 MB per web contents for platforms that do
327 // touch back/forward.
328 web_contents->GetController().ClearAllScreenshots();
332 int OomPriorityManager::GetTabCount() const {
333 int tab_count = 0;
334 for (chrome::BrowserIterator it; !it.done(); it.Next())
335 tab_count += it->tab_strip_model()->count();
336 return tab_count;
339 void OomPriorityManager::AddTabStats(BrowserList* browser_list,
340 bool active_desktop,
341 TabStatsList* stats_list) {
342 // If it's the active desktop, the first window will be the active one.
343 // Otherwise, we assume no active windows.
344 bool browser_active = active_desktop;
345 for (BrowserList::const_reverse_iterator browser_iterator =
346 browser_list->begin_last_active();
347 browser_iterator != browser_list->end_last_active();
348 ++browser_iterator) {
349 Browser* browser = *browser_iterator;
350 bool is_browser_for_app = browser->is_app();
351 const TabStripModel* model = browser->tab_strip_model();
352 for (int i = 0; i < model->count(); i++) {
353 WebContents* contents = model->GetWebContentsAt(i);
354 if (!contents->IsCrashed()) {
355 TabStats stats;
356 stats.is_app = is_browser_for_app;
357 stats.is_internal_page =
358 IsInternalPage(contents->GetLastCommittedURL());
359 stats.is_playing_audio = contents->WasRecentlyAudible();
360 stats.is_pinned = model->IsTabPinned(i);
361 stats.is_selected = browser_active && model->IsTabSelected(i);
362 stats.is_discarded = TabDiscardState::IsDiscarded(contents);
363 stats.discard_count = TabDiscardState::DiscardCount(contents);
364 stats.last_active = contents->GetLastActiveTime();
365 stats.renderer_handle = contents->GetRenderProcessHost()->GetHandle();
366 stats.child_process_host_id = contents->GetRenderProcessHost()->GetID();
367 #if defined(OS_CHROMEOS)
368 stats.oom_score = delegate_->GetOomScore(stats.child_process_host_id);
369 #endif
370 stats.title = contents->GetTitle();
371 stats.tab_contents_id = IdFromWebContents(contents);
372 stats_list->push_back(stats);
375 // We process the active browser window in the first iteration.
376 browser_active = false;
380 // This function is called when |update_timer_| fires. It will adjust the clock
381 // if needed (if we detect that the machine was asleep) and will fire the stats
382 // updating on ChromeOS via the delegate.
383 void OomPriorityManager::UpdateTimerCallback() {
384 // If we shutting down, do not do anything.
385 if (g_browser_process->IsShuttingDown())
386 return;
388 if (BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_ASH)->empty() &&
389 BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_NATIVE)->empty())
390 return;
392 // Check for a discontinuity in time caused by the machine being suspended.
393 if (!last_adjust_time_.is_null()) {
394 TimeDelta suspend_time = TimeTicks::Now() - last_adjust_time_;
395 if (suspend_time.InSeconds() > kSuspendThresholdSeconds) {
396 // We were probably suspended, move our event timers forward in time so
397 // when we subtract them out later we are counting "uptime".
398 start_time_ += suspend_time;
399 if (!last_discard_time_.is_null())
400 last_discard_time_ += suspend_time;
403 last_adjust_time_ = TimeTicks::Now();
405 #if defined(OS_CHROMEOS)
406 TabStatsList stats_list = GetTabStats();
407 // This starts the CrOS specific OOM adjustments in /proc/<pid>/oom_score_adj.
408 delegate_->AdjustOomPriorities(stats_list);
409 #endif
412 bool OomPriorityManager::CanDiscardTab(int64 target_web_contents_id) const {
413 TabStripModel* model;
414 int idx = FindTabStripModelById(target_web_contents_id, &model);
416 if (idx == -1)
417 return false;
419 WebContents* web_contents = model->GetWebContentsAt(idx);
420 // We do not discard tabs that are playing audio as it's too distruptive to
421 // the user experience.
422 if (web_contents->WasRecentlyAudible())
423 return false;
425 // We also make sure not to discard a previously discarded tab if that's the
426 // desired behavior.
427 if (discard_once_ && TabDiscardState::DiscardCount(web_contents) > 0)
428 return false;
430 return true;
433 void OomPriorityManager::OnMemoryPressure(
434 base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level) {
435 // If we are shutting down, do not do anything.
436 if (g_browser_process->IsShuttingDown())
437 return;
439 // For the moment we only do something when we reach a critical state.
440 if (memory_pressure_level ==
441 base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) {
442 LogMemoryAndDiscardTab();
444 // TODO(skuhne): If more memory pressure levels are introduced, we might
445 // consider to call PurgeBrowserMemory() before CRITICAL is reached.
448 // static
449 bool OomPriorityManager::CompareTabStats(TabStats first, TabStats second) {
450 // Being currently selected is most important to protect.
451 if (first.is_selected != second.is_selected)
452 return first.is_selected;
454 // Protect streaming audio and video conferencing tabs as these are similar to
455 // active tabs.
456 if (first.is_playing_audio != second.is_playing_audio)
457 return first.is_playing_audio;
459 // Tab with internal web UI like NTP or Settings are good choices to discard,
460 // so protect non-Web UI and let the other conditionals finish the sort.
461 if (first.is_internal_page != second.is_internal_page)
462 return !first.is_internal_page;
464 // Being pinned is important to protect.
465 if (first.is_pinned != second.is_pinned)
466 return first.is_pinned;
468 // Being an app is important too, as you're the only visible surface in the
469 // window and we don't want to discard that.
470 if (first.is_app != second.is_app)
471 return first.is_app;
473 // TODO(jamescook): Incorporate sudden_termination_allowed into the sort
474 // order. We don't do this now because pages with unload handlers set
475 // sudden_termination_allowed false, and that covers too many common pages
476 // with ad networks and statistics scripts. Ideally we would like to check
477 // for beforeUnload handlers, which are likely to present a dialog asking
478 // if the user wants to discard state. crbug.com/123049
480 // Being more recently active is more important.
481 return first.last_active > second.last_active;
484 } // namespace memory