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"
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_strip_model.h"
36 #include "chrome/browser/ui/tabs/tab_utils.h"
37 #include "chrome/common/chrome_constants.h"
38 #include "chrome/common/url_constants.h"
39 #include "content/public/browser/browser_thread.h"
40 #include "content/public/browser/render_process_host.h"
41 #include "content/public/browser/web_contents.h"
43 #if defined(OS_CHROMEOS)
44 #include "chrome/browser/memory/oom_priority_manager_delegate_chromeos.h"
47 using base::TimeDelta
;
48 using base::TimeTicks
;
49 using content::BrowserThread
;
50 using content::WebContents
;
55 // The default interval in seconds after which to adjust the oom_score_adj
57 const int kAdjustmentIntervalSeconds
= 10;
59 // For each period of this length we record a statistic to indicate whether
60 // or not the user experienced a low memory event. If you change this interval
61 // you must replace Tabs.Discard.DiscardInLastMinute with a new statistic.
62 const int kRecentTabDiscardIntervalSeconds
= 60;
64 // If there has been no priority adjustment in this interval, we assume the
65 // machine was suspended and correct our timing statistics.
66 const int kSuspendThresholdSeconds
= kAdjustmentIntervalSeconds
* 4;
68 // Returns a unique ID for a WebContents. Do not cast back to a pointer, as
69 // the WebContents could be deleted if the user closed the tab.
70 int64
IdFromWebContents(WebContents
* web_contents
) {
71 return reinterpret_cast<int64
>(web_contents
);
74 int FindTabStripModelById(int64 target_web_contents_id
, TabStripModel
** model
) {
76 for (chrome::BrowserIterator it
; !it
.done(); it
.Next()) {
77 Browser
* browser
= *it
;
78 TabStripModel
* local_model
= browser
->tab_strip_model();
79 for (int idx
= 0; idx
< local_model
->count(); idx
++) {
80 WebContents
* web_contents
= local_model
->GetWebContentsAt(idx
);
81 int64 web_contents_id
= IdFromWebContents(web_contents
);
82 if (web_contents_id
== target_web_contents_id
) {
94 ////////////////////////////////////////////////////////////////////////////////
97 OomPriorityManager::OomPriorityManager()
98 : discard_count_(0), recent_tab_discard_(false) {
99 #if defined(OS_CHROMEOS)
100 delegate_
.reset(new OomPriorityManagerDelegate
);
104 OomPriorityManager::~OomPriorityManager() {
108 void OomPriorityManager::Start() {
109 if (!update_timer_
.IsRunning()) {
110 update_timer_
.Start(FROM_HERE
,
111 TimeDelta::FromSeconds(kAdjustmentIntervalSeconds
),
112 this, &OomPriorityManager::UpdateTimerCallback
);
114 if (!recent_tab_discard_timer_
.IsRunning()) {
115 recent_tab_discard_timer_
.Start(
116 FROM_HERE
, TimeDelta::FromSeconds(kRecentTabDiscardIntervalSeconds
),
117 this, &OomPriorityManager::RecordRecentTabDiscard
);
119 start_time_
= TimeTicks::Now();
120 // Create a |MemoryPressureListener| to listen for memory events.
121 base::MemoryPressureMonitor
* monitor
= base::MemoryPressureMonitor::Get();
123 memory_pressure_listener_
.reset(new base::MemoryPressureListener(base::Bind(
124 &OomPriorityManager::OnMemoryPressure
, base::Unretained(this))));
125 base::MemoryPressureListener::MemoryPressureLevel level
=
126 monitor
->GetCurrentPressureLevel();
127 if (level
== base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL
) {
128 OnMemoryPressure(level
);
133 void OomPriorityManager::Stop() {
134 update_timer_
.Stop();
135 recent_tab_discard_timer_
.Stop();
136 memory_pressure_listener_
.reset();
139 // Things we need to collect on the browser thread (because TabStripModel isn't
141 // 1) whether or not a tab is pinned
142 // 2) last time a tab was selected
143 // 3) is the tab currently selected
144 TabStatsList
OomPriorityManager::GetTabStats() {
145 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
146 TabStatsList stats_list
;
147 stats_list
.reserve(32); // 99% of users have < 30 tabs open
149 // We go through each window to get all the tabs. Depending on the platform,
150 // windows are either native or ash or both. We want to make sure to go
151 // through them all, starting with the active window first (we use
152 // chrome::GetActiveDesktop to get the current used type).
153 AddTabStats(BrowserList::GetInstance(chrome::GetActiveDesktop()), true,
155 if (chrome::GetActiveDesktop() != chrome::HOST_DESKTOP_TYPE_NATIVE
) {
156 AddTabStats(BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_NATIVE
),
158 } else if (chrome::GetActiveDesktop() != chrome::HOST_DESKTOP_TYPE_ASH
) {
159 AddTabStats(BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_ASH
), false,
163 // Sort the data we collected so that least desirable to be
164 // killed is first, most desirable is last.
165 std::sort(stats_list
.begin(), stats_list
.end(), CompareTabStats
);
169 // TODO(jamescook): This should consider tabs with references to other tabs,
170 // such as tabs created with JavaScript window.open(). We might want to
171 // discard the entire set together, or use that in the priority computation.
172 bool OomPriorityManager::DiscardTab() {
173 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
174 TabStatsList stats
= GetTabStats();
177 // Loop until we find a non-discarded tab to kill.
178 for (TabStatsList::const_reverse_iterator stats_rit
= stats
.rbegin();
179 stats_rit
!= stats
.rend(); ++stats_rit
) {
180 int64 least_important_tab_id
= stats_rit
->tab_contents_id
;
181 if (DiscardTabById(least_important_tab_id
))
187 bool OomPriorityManager::DiscardTabById(int64 target_web_contents_id
) {
188 TabStripModel
* model
;
189 int idx
= FindTabStripModelById(target_web_contents_id
, &model
);
194 // Can't discard tabs that are already discarded or active.
195 if (model
->IsTabDiscarded(idx
) || (model
->active_index() == idx
))
198 // We also ignore tabs that are playing audio as it's too distruptive to
199 // the user experience.
200 if (model
->GetWebContentsAt(idx
)->WasRecentlyAudible())
203 VLOG(1) << "Discarding tab " << idx
<< " id " << target_web_contents_id
;
204 // Record statistics before discarding because we want to capture the
205 // memory state that lead to the discard.
206 RecordDiscardStatistics();
207 model
->DiscardWebContentsAt(idx
);
208 recent_tab_discard_
= true;
213 void OomPriorityManager::LogMemoryAndDiscardTab() {
214 LogMemory("Tab Discards Memory details",
215 base::Bind(&OomPriorityManager::PurgeMemoryAndDiscardTab
));
218 void OomPriorityManager::LogMemory(const std::string
& title
,
219 const base::Closure
& callback
) {
220 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
221 OomMemoryDetails::Log(title
, callback
);
224 ///////////////////////////////////////////////////////////////////////////////
225 // OomPriorityManager, private:
228 void OomPriorityManager::PurgeMemoryAndDiscardTab() {
229 if (g_browser_process
&& g_browser_process
->GetOomPriorityManager()) {
230 OomPriorityManager
* manager
= g_browser_process
->GetOomPriorityManager();
231 manager
->PurgeBrowserMemory();
232 manager
->DiscardTab();
237 bool OomPriorityManager::IsInternalPage(const GURL
& url
) {
238 // There are many chrome:// UI URLs, but only look for the ones that users
239 // are likely to have open. Most of the benefit is the from NTP URL.
240 const char* const kInternalPagePrefixes
[] = {
241 chrome::kChromeUIDownloadsURL
,
242 chrome::kChromeUIHistoryURL
,
243 chrome::kChromeUINewTabURL
,
244 chrome::kChromeUISettingsURL
,
246 // Prefix-match against the table above. Use strncmp to avoid allocating
247 // memory to convert the URL prefix constants into std::strings.
248 for (size_t i
= 0; i
< arraysize(kInternalPagePrefixes
); ++i
) {
249 if (!strncmp(url
.spec().c_str(), kInternalPagePrefixes
[i
],
250 strlen(kInternalPagePrefixes
[i
])))
256 void OomPriorityManager::RecordDiscardStatistics() {
257 // Record a raw count so we can compare to discard reloads.
259 UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.Discard.DiscardCount", discard_count_
, 1,
262 // TODO(jamescook): Maybe incorporate extension count?
263 UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.Discard.TabCount", GetTabCount(), 1, 100,
265 #if defined(OS_CHROMEOS)
266 // Record the discarded tab in relation to the amount of simultaneously
268 ash::MultiProfileUMA::RecordDiscardedTab(ash::Shell::GetInstance()
269 ->session_state_delegate()
270 ->NumberOfLoggedInUsers());
272 // TODO(jamescook): If the time stats prove too noisy, then divide up users
273 // based on how heavily they use Chrome using tab count as a proxy.
274 // Bin into <= 1, <= 2, <= 4, <= 8, etc.
275 if (last_discard_time_
.is_null()) {
276 // This is the first discard this session.
277 TimeDelta interval
= TimeTicks::Now() - start_time_
;
278 int interval_seconds
= static_cast<int>(interval
.InSeconds());
279 // Record time in seconds over an interval of approximately 1 day.
280 UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.Discard.InitialTime2", interval_seconds
,
283 // Not the first discard, so compute time since last discard.
284 TimeDelta interval
= TimeTicks::Now() - last_discard_time_
;
285 int interval_ms
= static_cast<int>(interval
.InMilliseconds());
286 // Record time in milliseconds over an interval of approximately 1 day.
287 // Start at 100 ms to get extra resolution in the target 750 ms range.
288 UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.Discard.IntervalTime2", interval_ms
, 100,
291 // TODO(georgesak): Remove this #if when RecordMemoryStats is implemented for
293 #if defined(OS_WIN) || defined(OS_CHROMEOS)
294 // Record system memory usage at the time of the discard.
295 RecordMemoryStats(RECORD_MEMORY_STATS_TAB_DISCARDED
);
297 // Set up to record the next interval.
298 last_discard_time_
= TimeTicks::Now();
301 void OomPriorityManager::RecordRecentTabDiscard() {
302 // If we are shutting down, do not do anything.
303 if (g_browser_process
->IsShuttingDown())
306 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
307 // If we change the interval we need to change the histogram name.
308 UMA_HISTOGRAM_BOOLEAN("Tabs.Discard.DiscardInLastMinute",
309 recent_tab_discard_
);
310 // Reset for the next interval.
311 recent_tab_discard_
= false;
314 void OomPriorityManager::PurgeBrowserMemory() {
315 // Based on experimental evidence, attempts to free memory from renderers
316 // have been too slow to use in OOM situations (V8 garbage collection) or
317 // do not lead to persistent decreased usage (image/bitmap caches). This
318 // function therefore only targets large blocks of memory in the browser.
319 // Note that other objects will listen to MemoryPressureListener events
320 // to release memory.
321 for (TabContentsIterator it
; !it
.done(); it
.Next()) {
322 WebContents
* web_contents
= *it
;
323 // Screenshots can consume ~5 MB per web contents for platforms that do
324 // touch back/forward.
325 web_contents
->GetController().ClearAllScreenshots();
329 int OomPriorityManager::GetTabCount() const {
331 for (chrome::BrowserIterator it
; !it
.done(); it
.Next())
332 tab_count
+= it
->tab_strip_model()->count();
336 // Returns true if |first| is considered less desirable to be killed
338 bool OomPriorityManager::CompareTabStats(TabStats first
, TabStats second
) {
339 // Being currently selected is most important to protect.
340 if (first
.is_selected
!= second
.is_selected
)
341 return first
.is_selected
;
343 // Protect streaming audio and video conferencing tabs as these are similar to
345 if (first
.is_playing_audio
!= second
.is_playing_audio
)
346 return first
.is_playing_audio
;
348 // Tab with internal web UI like NTP or Settings are good choices to discard,
349 // so protect non-Web UI and let the other conditionals finish the sort.
350 if (first
.is_internal_page
!= second
.is_internal_page
)
351 return !first
.is_internal_page
;
353 // Being pinned is important to protect.
354 if (first
.is_pinned
!= second
.is_pinned
)
355 return first
.is_pinned
;
357 // Being an app is important too, as you're the only visible surface in the
358 // window and we don't want to discard that.
359 if (first
.is_app
!= second
.is_app
)
362 // TODO(jamescook): Incorporate sudden_termination_allowed into the sort
363 // order. We don't do this now because pages with unload handlers set
364 // sudden_termination_allowed false, and that covers too many common pages
365 // with ad networks and statistics scripts. Ideally we would like to check
366 // for beforeUnload handlers, which are likely to present a dialog asking
367 // if the user wants to discard state. crbug.com/123049
369 // Being more recently active is more important.
370 return first
.last_active
> second
.last_active
;
373 // This function is called when |update_timer_| fires. It will adjust the clock
374 // if needed (if we detect that the machine was asleep) and will fire the stats
375 // updating on ChromeOS via the delegate.
376 void OomPriorityManager::UpdateTimerCallback() {
377 // If we shutting down, do not do anything.
378 if (g_browser_process
->IsShuttingDown())
381 if (BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_ASH
)->empty() &&
382 BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_NATIVE
)->empty())
385 // Check for a discontinuity in time caused by the machine being suspended.
386 if (!last_adjust_time_
.is_null()) {
387 TimeDelta suspend_time
= TimeTicks::Now() - last_adjust_time_
;
388 if (suspend_time
.InSeconds() > kSuspendThresholdSeconds
) {
389 // We were probably suspended, move our event timers forward in time so
390 // when we subtract them out later we are counting "uptime".
391 start_time_
+= suspend_time
;
392 if (!last_discard_time_
.is_null())
393 last_discard_time_
+= suspend_time
;
396 last_adjust_time_
= TimeTicks::Now();
398 #if defined(OS_CHROMEOS)
399 TabStatsList stats_list
= GetTabStats();
400 // This starts the CrOS specific OOM adjustments in /proc/<pid>/oom_score_adj.
401 delegate_
->AdjustOomPriorities(stats_list
);
405 void OomPriorityManager::AddTabStats(BrowserList
* browser_list
,
407 TabStatsList
* stats_list
) {
408 // If it's the active desktop, the first window will be the active one.
409 // Otherwise, we assume no active windows.
410 bool browser_active
= active_desktop
;
411 for (BrowserList::const_reverse_iterator browser_iterator
=
412 browser_list
->begin_last_active();
413 browser_iterator
!= browser_list
->end_last_active();
414 ++browser_iterator
) {
415 Browser
* browser
= *browser_iterator
;
416 bool is_browser_for_app
= browser
->is_app();
417 const TabStripModel
* model
= browser
->tab_strip_model();
418 for (int i
= 0; i
< model
->count(); i
++) {
419 WebContents
* contents
= model
->GetWebContentsAt(i
);
420 if (!contents
->IsCrashed()) {
422 stats
.is_app
= is_browser_for_app
;
423 stats
.is_internal_page
=
424 IsInternalPage(contents
->GetLastCommittedURL());
425 stats
.is_playing_audio
= contents
->WasRecentlyAudible();
426 stats
.is_pinned
= model
->IsTabPinned(i
);
427 stats
.is_selected
= browser_active
&& model
->IsTabSelected(i
);
428 stats
.is_discarded
= model
->IsTabDiscarded(i
);
429 stats
.last_active
= contents
->GetLastActiveTime();
430 stats
.renderer_handle
= contents
->GetRenderProcessHost()->GetHandle();
431 stats
.child_process_host_id
= contents
->GetRenderProcessHost()->GetID();
432 #if defined(OS_CHROMEOS)
433 stats
.oom_score
= delegate_
->GetOomScore(stats
.child_process_host_id
);
435 stats
.title
= contents
->GetTitle();
436 stats
.tab_contents_id
= IdFromWebContents(contents
);
437 stats_list
->push_back(stats
);
440 // We process the active browser window in the first iteration.
441 browser_active
= false;
445 void OomPriorityManager::OnMemoryPressure(
446 base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level
) {
447 // If we are shutting down, do not do anything.
448 if (g_browser_process
->IsShuttingDown())
451 // For the moment we only do something when we reach a critical state.
452 if (memory_pressure_level
==
453 base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL
) {
454 LogMemoryAndDiscardTab();
456 // TODO(skuhne): If more memory pressure levels are introduced, we might
457 // consider to call PurgeBrowserMemory() before CRITICAL is reached.
460 } // namespace memory