OLD | NEW |
| (Empty) |
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. | |
4 | |
5 #include "chrome/browser/memory/tab_manager.h" | |
6 | |
7 #include <stddef.h> | |
8 | |
9 #include <algorithm> | |
10 #include <set> | |
11 #include <vector> | |
12 | |
13 #include "base/bind.h" | |
14 #include "base/bind_helpers.h" | |
15 #include "base/command_line.h" | |
16 #include "base/feature_list.h" | |
17 #include "base/macros.h" | |
18 #include "base/memory/memory_pressure_monitor.h" | |
19 #include "base/metrics/field_trial.h" | |
20 #include "base/metrics/histogram_macros.h" | |
21 #include "base/observer_list.h" | |
22 #include "base/process/process.h" | |
23 #include "base/rand_util.h" | |
24 #include "base/strings/string16.h" | |
25 #include "base/strings/string_number_conversions.h" | |
26 #include "base/strings/string_util.h" | |
27 #include "base/strings/utf_string_conversions.h" | |
28 #include "base/threading/thread.h" | |
29 #include "base/time/tick_clock.h" | |
30 #include "build/build_config.h" | |
31 #include "chrome/browser/browser_process.h" | |
32 #include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" | |
33 #include "chrome/browser/media/webrtc/media_stream_capture_indicator.h" | |
34 #include "chrome/browser/memory/oom_memory_details.h" | |
35 #include "chrome/browser/memory/tab_manager_observer.h" | |
36 #include "chrome/browser/memory/tab_manager_web_contents_data.h" | |
37 #include "chrome/browser/profiles/profile.h" | |
38 #include "chrome/browser/ui/browser.h" | |
39 #include "chrome/browser/ui/browser_list.h" | |
40 #include "chrome/browser/ui/browser_window.h" | |
41 #include "chrome/browser/ui/tab_contents/tab_contents_iterator.h" | |
42 #include "chrome/browser/ui/tabs/tab_strip_model.h" | |
43 #include "chrome/browser/ui/tabs/tab_utils.h" | |
44 #include "chrome/common/chrome_constants.h" | |
45 #include "chrome/common/chrome_features.h" | |
46 #include "chrome/common/chrome_switches.h" | |
47 #include "chrome/common/url_constants.h" | |
48 #include "components/metrics/system_memory_stats_recorder.h" | |
49 #include "components/variations/variations_associated_data.h" | |
50 #include "content/public/browser/browser_thread.h" | |
51 #include "content/public/browser/navigation_controller.h" | |
52 #include "content/public/browser/render_process_host.h" | |
53 #include "content/public/browser/web_contents.h" | |
54 #include "content/public/common/page_importance_signals.h" | |
55 | |
56 #if defined(OS_CHROMEOS) | |
57 #include "ash/multi_profile_uma.h" | |
58 #include "ash/shell_port.h" | |
59 #include "chrome/browser/memory/tab_manager_delegate_chromeos.h" | |
60 #include "components/user_manager/user_manager.h" | |
61 #endif | |
62 | |
63 using base::TimeDelta; | |
64 using base::TimeTicks; | |
65 using content::BrowserThread; | |
66 using content::WebContents; | |
67 | |
68 namespace memory { | |
69 namespace { | |
70 | |
71 // The default interval in seconds after which to adjust the oom_score_adj | |
72 // value. | |
73 const int kAdjustmentIntervalSeconds = 10; | |
74 | |
75 #if defined(OS_WIN) || defined(OS_MACOSX) || defined(OS_CHROMEOS) | |
76 // For each period of this length record a statistic to indicate whether or not | |
77 // the user experienced a low memory event. If this interval is changed, | |
78 // Tabs.Discard.DiscardInLastMinute must be replaced with a new statistic. | |
79 const int kRecentTabDiscardIntervalSeconds = 60; | |
80 #endif | |
81 | |
82 // The time during which a tab is protected from discarding after it stops being | |
83 // audible. | |
84 const int kAudioProtectionTimeSeconds = 60; | |
85 | |
86 int FindWebContentsById(const TabStripModel* model, | |
87 int64_t target_web_contents_id) { | |
88 for (int idx = 0; idx < model->count(); idx++) { | |
89 WebContents* web_contents = model->GetWebContentsAt(idx); | |
90 int64_t web_contents_id = TabManager::IdFromWebContents(web_contents); | |
91 if (web_contents_id == target_web_contents_id) | |
92 return idx; | |
93 } | |
94 | |
95 return -1; | |
96 } | |
97 | |
98 } // namespace | |
99 | |
100 //////////////////////////////////////////////////////////////////////////////// | |
101 // TabManager | |
102 | |
103 constexpr base::TimeDelta TabManager::kDefaultMinTimeToPurge; | |
104 | |
105 TabManager::TabManager() | |
106 : discard_count_(0), | |
107 recent_tab_discard_(false), | |
108 discard_once_(false), | |
109 #if !defined(OS_CHROMEOS) | |
110 minimum_protection_time_(base::TimeDelta::FromMinutes(10)), | |
111 #endif | |
112 browser_tab_strip_tracker_(this, nullptr, nullptr), | |
113 test_tick_clock_(nullptr), | |
114 weak_ptr_factory_(this) { | |
115 #if defined(OS_CHROMEOS) | |
116 delegate_.reset(new TabManagerDelegate(weak_ptr_factory_.GetWeakPtr())); | |
117 #endif | |
118 browser_tab_strip_tracker_.Init(); | |
119 } | |
120 | |
121 TabManager::~TabManager() { | |
122 Stop(); | |
123 } | |
124 | |
125 void TabManager::Start() { | |
126 #if defined(OS_WIN) || defined(OS_MACOSX) | |
127 // Note that discarding is now enabled by default. This check is kept as a | |
128 // kill switch. | |
129 // TODO(georgesak): remote this when deemed not needed anymore. | |
130 if (!base::FeatureList::IsEnabled(features::kAutomaticTabDiscarding)) | |
131 return; | |
132 | |
133 // Check the variation parameter to see if a tab is to be protected for an | |
134 // amount of time after being backgrounded. The value is in seconds. Default | |
135 // is 10 minutes if the variation is absent. | |
136 std::string minimum_protection_time_string = | |
137 variations::GetVariationParamValue(features::kAutomaticTabDiscarding.name, | |
138 "MinimumProtectionTime"); | |
139 if (!minimum_protection_time_string.empty()) { | |
140 unsigned int minimum_protection_time_seconds = 0; | |
141 if (base::StringToUint(minimum_protection_time_string, | |
142 &minimum_protection_time_seconds)) { | |
143 if (minimum_protection_time_seconds > 0) | |
144 minimum_protection_time_ = | |
145 base::TimeDelta::FromSeconds(minimum_protection_time_seconds); | |
146 } | |
147 } | |
148 #endif | |
149 | |
150 // Check if only one discard is allowed. | |
151 discard_once_ = CanOnlyDiscardOnce(); | |
152 | |
153 if (!update_timer_.IsRunning()) { | |
154 update_timer_.Start(FROM_HERE, | |
155 TimeDelta::FromSeconds(kAdjustmentIntervalSeconds), | |
156 this, &TabManager::UpdateTimerCallback); | |
157 } | |
158 | |
159 // MemoryPressureMonitor is not implemented on Linux so far and tabs are never | |
160 // discarded. | |
161 #if defined(OS_WIN) || defined(OS_MACOSX) || defined(OS_CHROMEOS) | |
162 if (!recent_tab_discard_timer_.IsRunning()) { | |
163 recent_tab_discard_timer_.Start( | |
164 FROM_HERE, TimeDelta::FromSeconds(kRecentTabDiscardIntervalSeconds), | |
165 this, &TabManager::RecordRecentTabDiscard); | |
166 } | |
167 start_time_ = NowTicks(); | |
168 // Create a |MemoryPressureListener| to listen for memory events. | |
169 base::MemoryPressureMonitor* monitor = base::MemoryPressureMonitor::Get(); | |
170 if (monitor) { | |
171 memory_pressure_listener_.reset(new base::MemoryPressureListener( | |
172 base::Bind(&TabManager::OnMemoryPressure, base::Unretained(this)))); | |
173 base::MemoryPressureListener::MemoryPressureLevel level = | |
174 monitor->GetCurrentPressureLevel(); | |
175 if (level == base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) { | |
176 OnMemoryPressure(level); | |
177 } | |
178 } | |
179 #endif | |
180 // purge-and-suspend param is used for Purge+Suspend finch experiment | |
181 // in the following way: | |
182 // https://docs.google.com/document/d/1hPHkKtXXBTlsZx9s-9U17XC-ofEIzPo9FYbBEc7
PPbk/edit?usp=sharing | |
183 std::string purge_and_suspend_time = variations::GetVariationParamValue( | |
184 "PurgeAndSuspend", "purge-and-suspend-time"); | |
185 unsigned int min_time_to_purge_sec = 0; | |
186 if (purge_and_suspend_time.empty() || | |
187 !base::StringToUint(purge_and_suspend_time, &min_time_to_purge_sec)) | |
188 min_time_to_purge_ = kDefaultMinTimeToPurge; | |
189 else | |
190 min_time_to_purge_ = base::TimeDelta::FromSeconds(min_time_to_purge_sec); | |
191 } | |
192 | |
193 void TabManager::Stop() { | |
194 update_timer_.Stop(); | |
195 recent_tab_discard_timer_.Stop(); | |
196 memory_pressure_listener_.reset(); | |
197 } | |
198 | |
199 int TabManager::FindTabStripModelById(int64_t target_web_contents_id, | |
200 TabStripModel** model) const { | |
201 DCHECK(model); | |
202 // TODO(tasak): Move this code to a TabStripModel enumeration delegate! | |
203 if (!test_tab_strip_models_.empty()) { | |
204 for (size_t i = 0; i < test_tab_strip_models_.size(); ++i) { | |
205 TabStripModel* local_model = | |
206 const_cast<TabStripModel*>(test_tab_strip_models_[i].first); | |
207 int idx = FindWebContentsById(local_model, target_web_contents_id); | |
208 if (idx != -1) { | |
209 *model = local_model; | |
210 return idx; | |
211 } | |
212 } | |
213 | |
214 return -1; | |
215 } | |
216 | |
217 for (auto* browser : *BrowserList::GetInstance()) { | |
218 TabStripModel* local_model = browser->tab_strip_model(); | |
219 int idx = FindWebContentsById(local_model, target_web_contents_id); | |
220 if (idx != -1) { | |
221 *model = local_model; | |
222 return idx; | |
223 } | |
224 } | |
225 | |
226 return -1; | |
227 } | |
228 | |
229 TabStatsList TabManager::GetTabStats() const { | |
230 TabStatsList stats_list(GetUnsortedTabStats()); | |
231 | |
232 // Sort the collected data so that least desirable to be killed is first, most | |
233 // desirable is last. | |
234 std::sort(stats_list.begin(), stats_list.end(), CompareTabStats); | |
235 | |
236 return stats_list; | |
237 } | |
238 | |
239 bool TabManager::IsTabDiscarded(content::WebContents* contents) const { | |
240 return GetWebContentsData(contents)->IsDiscarded(); | |
241 } | |
242 | |
243 bool TabManager::CanDiscardTab(int64_t target_web_contents_id) const { | |
244 TabStripModel* model; | |
245 int idx = FindTabStripModelById(target_web_contents_id, &model); | |
246 | |
247 if (idx == -1) | |
248 return false; | |
249 | |
250 WebContents* web_contents = model->GetWebContentsAt(idx); | |
251 | |
252 // Do not discard tabs that don't have a valid URL (most probably they have | |
253 // just been opened and dicarding them would lose the URL). | |
254 // TODO(georgesak): Look into a workaround to be able to kill the tab without | |
255 // losing the pending navigation. | |
256 if (!web_contents->GetLastCommittedURL().is_valid() || | |
257 web_contents->GetLastCommittedURL().is_empty()) { | |
258 return false; | |
259 } | |
260 | |
261 // Do not discard tabs in which the user has entered text in a form, lest that | |
262 // state gets lost. | |
263 if (web_contents->GetPageImportanceSignals().had_form_interaction) | |
264 return false; | |
265 | |
266 // Do not discard tabs that are playing either playing audio or accessing the | |
267 // microphone or camera as it's too distruptive to the user experience. Note | |
268 // that tabs that have recently stopped playing audio by at least | |
269 // |kAudioProtectionTimeSeconds| seconds are protected as well. | |
270 if (IsMediaTab(web_contents)) | |
271 return false; | |
272 | |
273 // Do not discard PDFs as they might contain entry that is not saved and they | |
274 // don't remember their scrolling positions. See crbug.com/547286 and | |
275 // crbug.com/65244. | |
276 // TODO(georgesak): Remove this workaround when the bugs are fixed. | |
277 if (web_contents->GetContentsMimeType() == "application/pdf") | |
278 return false; | |
279 | |
280 // Do not discard a previously discarded tab if that's the desired behavior. | |
281 if (discard_once_ && GetWebContentsData(web_contents)->DiscardCount() > 0) | |
282 return false; | |
283 | |
284 // Do not discard a recently used tab. | |
285 if (minimum_protection_time_.InSeconds() > 0) { | |
286 auto delta = | |
287 NowTicks() - GetWebContentsData(web_contents)->LastInactiveTime(); | |
288 if (delta < minimum_protection_time_) | |
289 return false; | |
290 } | |
291 | |
292 // Do not discard a tab that was explicitly disallowed to. | |
293 if (!IsTabAutoDiscardable(web_contents)) | |
294 return false; | |
295 | |
296 return true; | |
297 } | |
298 | |
299 void TabManager::DiscardTab() { | |
300 #if defined(OS_CHROMEOS) | |
301 // Call Chrome OS specific low memory handling process. | |
302 if (base::FeatureList::IsEnabled(features::kArcMemoryManagement)) { | |
303 delegate_->LowMemoryKill(GetUnsortedTabStats()); | |
304 return; | |
305 } | |
306 #endif | |
307 DiscardTabImpl(); | |
308 } | |
309 | |
310 WebContents* TabManager::DiscardTabById(int64_t target_web_contents_id) { | |
311 TabStripModel* model; | |
312 int index = FindTabStripModelById(target_web_contents_id, &model); | |
313 | |
314 if (index == -1) | |
315 return nullptr; | |
316 | |
317 VLOG(1) << "Discarding tab " << index << " id " << target_web_contents_id; | |
318 | |
319 return DiscardWebContentsAt(index, model); | |
320 } | |
321 | |
322 WebContents* TabManager::DiscardTabByExtension(content::WebContents* contents) { | |
323 if (contents) | |
324 return DiscardTabById(IdFromWebContents(contents)); | |
325 | |
326 return DiscardTabImpl(); | |
327 } | |
328 | |
329 void TabManager::LogMemoryAndDiscardTab() { | |
330 LogMemory("Tab Discards Memory details", | |
331 base::Bind(&TabManager::PurgeMemoryAndDiscardTab)); | |
332 } | |
333 | |
334 void TabManager::LogMemory(const std::string& title, | |
335 const base::Closure& callback) { | |
336 DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
337 OomMemoryDetails::Log(title, callback); | |
338 } | |
339 | |
340 void TabManager::set_test_tick_clock(base::TickClock* test_tick_clock) { | |
341 test_tick_clock_ = test_tick_clock; | |
342 } | |
343 | |
344 // Things to collect on the browser thread (because TabStripModel isn't thread | |
345 // safe): | |
346 // 1) whether or not a tab is pinned | |
347 // 2) last time a tab was selected | |
348 // 3) is the tab currently selected | |
349 TabStatsList TabManager::GetUnsortedTabStats() const { | |
350 DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
351 TabStatsList stats_list; | |
352 stats_list.reserve(32); // 99% of users have < 30 tabs open. | |
353 | |
354 // TODO(chrisha): Move this code to a TabStripModel enumeration delegate! | |
355 if (!test_tab_strip_models_.empty()) { | |
356 for (size_t i = 0; i < test_tab_strip_models_.size(); ++i) { | |
357 AddTabStats(test_tab_strip_models_[i].first, // tab_strip_model | |
358 test_tab_strip_models_[i].second, // is_app | |
359 i == 0, // is_active | |
360 &stats_list); | |
361 } | |
362 } else { | |
363 // The code here can only be tested under a full browser test. | |
364 AddTabStats(&stats_list); | |
365 } | |
366 | |
367 return stats_list; | |
368 } | |
369 | |
370 void TabManager::AddObserver(TabManagerObserver* observer) { | |
371 observers_.AddObserver(observer); | |
372 } | |
373 | |
374 void TabManager::RemoveObserver(TabManagerObserver* observer) { | |
375 observers_.RemoveObserver(observer); | |
376 } | |
377 | |
378 void TabManager::set_minimum_protection_time_for_tests( | |
379 base::TimeDelta minimum_protection_time) { | |
380 minimum_protection_time_ = minimum_protection_time; | |
381 } | |
382 | |
383 bool TabManager::IsTabAutoDiscardable(content::WebContents* contents) const { | |
384 return GetWebContentsData(contents)->IsAutoDiscardable(); | |
385 } | |
386 | |
387 void TabManager::SetTabAutoDiscardableState(content::WebContents* contents, | |
388 bool state) { | |
389 GetWebContentsData(contents)->SetAutoDiscardableState(state); | |
390 } | |
391 | |
392 content::WebContents* TabManager::GetWebContentsById( | |
393 int64_t tab_contents_id) const { | |
394 TabStripModel* model = nullptr; | |
395 int index = FindTabStripModelById(tab_contents_id, &model); | |
396 if (index == -1) | |
397 return nullptr; | |
398 return model->GetWebContentsAt(index); | |
399 } | |
400 | |
401 bool TabManager::CanSuspendBackgroundedRenderer(int render_process_id) const { | |
402 // A renderer can be purged if it's not playing media. | |
403 auto tab_stats = GetUnsortedTabStats(); | |
404 for (auto& tab : tab_stats) { | |
405 if (tab.child_process_host_id != render_process_id) | |
406 continue; | |
407 WebContents* web_contents = GetWebContentsById(tab.tab_contents_id); | |
408 if (!web_contents) | |
409 return false; | |
410 if (IsMediaTab(web_contents)) | |
411 return false; | |
412 } | |
413 return true; | |
414 } | |
415 | |
416 // static | |
417 bool TabManager::CompareTabStats(const TabStats& first, | |
418 const TabStats& second) { | |
419 // Being currently selected is most important to protect. | |
420 if (first.is_selected != second.is_selected) | |
421 return first.is_selected; | |
422 | |
423 // Non auto-discardable tabs are more important to protect. | |
424 if (first.is_auto_discardable != second.is_auto_discardable) | |
425 return !first.is_auto_discardable; | |
426 | |
427 // Protect tabs with pending form entries. | |
428 if (first.has_form_entry != second.has_form_entry) | |
429 return first.has_form_entry; | |
430 | |
431 // Protect streaming audio and video conferencing tabs as these are similar to | |
432 // active tabs. | |
433 if (first.is_media != second.is_media) | |
434 return first.is_media; | |
435 | |
436 // Tab with internal web UI like NTP or Settings are good choices to discard, | |
437 // so protect non-Web UI and let the other conditionals finish the sort. | |
438 if (first.is_internal_page != second.is_internal_page) | |
439 return !first.is_internal_page; | |
440 | |
441 // Being pinned is important to protect. | |
442 if (first.is_pinned != second.is_pinned) | |
443 return first.is_pinned; | |
444 | |
445 // Being an app is important too, as it's the only visible surface in the | |
446 // window and should not be discarded. | |
447 if (first.is_app != second.is_app) | |
448 return first.is_app; | |
449 | |
450 // TODO(jamescook): Incorporate sudden_termination_allowed into the sort | |
451 // order. This is currently not done because pages with unload handlers set | |
452 // sudden_termination_allowed false, and that covers too many common pages | |
453 // with ad networks and statistics scripts. Ideally check for beforeUnload | |
454 // handlers, which are likely to present a dialog asking if the user wants to | |
455 // discard state. crbug.com/123049. | |
456 | |
457 // Being more recently active is more important. | |
458 return first.last_active > second.last_active; | |
459 } | |
460 | |
461 // static | |
462 int64_t TabManager::IdFromWebContents(WebContents* web_contents) { | |
463 return reinterpret_cast<int64_t>(web_contents); | |
464 } | |
465 | |
466 /////////////////////////////////////////////////////////////////////////////// | |
467 // TabManager, private: | |
468 | |
469 void TabManager::OnDiscardedStateChange(content::WebContents* contents, | |
470 bool is_discarded) { | |
471 for (TabManagerObserver& observer : observers_) | |
472 observer.OnDiscardedStateChange(contents, is_discarded); | |
473 } | |
474 | |
475 void TabManager::OnAutoDiscardableStateChange(content::WebContents* contents, | |
476 bool is_auto_discardable) { | |
477 for (TabManagerObserver& observer : observers_) | |
478 observer.OnAutoDiscardableStateChange(contents, is_auto_discardable); | |
479 } | |
480 | |
481 // static | |
482 void TabManager::PurgeMemoryAndDiscardTab() { | |
483 TabManager* manager = g_browser_process->GetTabManager(); | |
484 manager->PurgeBrowserMemory(); | |
485 manager->DiscardTab(); | |
486 } | |
487 | |
488 // static | |
489 bool TabManager::IsInternalPage(const GURL& url) { | |
490 // There are many chrome:// UI URLs, but only look for the ones that users | |
491 // are likely to have open. Most of the benefit is the from NTP URL. | |
492 const char* const kInternalPagePrefixes[] = { | |
493 chrome::kChromeUIDownloadsURL, chrome::kChromeUIHistoryURL, | |
494 chrome::kChromeUINewTabURL, chrome::kChromeUISettingsURL, | |
495 }; | |
496 // Prefix-match against the table above. Use strncmp to avoid allocating | |
497 // memory to convert the URL prefix constants into std::strings. | |
498 for (size_t i = 0; i < arraysize(kInternalPagePrefixes); ++i) { | |
499 if (!strncmp(url.spec().c_str(), kInternalPagePrefixes[i], | |
500 strlen(kInternalPagePrefixes[i]))) | |
501 return true; | |
502 } | |
503 return false; | |
504 } | |
505 | |
506 void TabManager::RecordDiscardStatistics() { | |
507 discard_count_++; | |
508 | |
509 // TODO(jamescook): Maybe incorporate extension count? | |
510 UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.Discard.TabCount", GetTabCount(), 1, 100, | |
511 50); | |
512 #if defined(OS_CHROMEOS) | |
513 // Record the discarded tab in relation to the amount of simultaneously | |
514 // logged in users. | |
515 if (ash::ShellPort::HasInstance()) { | |
516 ash::MultiProfileUMA::RecordDiscardedTab( | |
517 user_manager::UserManager::Get()->GetLoggedInUsers().size()); | |
518 } | |
519 #endif | |
520 // TODO(jamescook): If the time stats prove too noisy, then divide up users | |
521 // based on how heavily they use Chrome using tab count as a proxy. | |
522 // Bin into <= 1, <= 2, <= 4, <= 8, etc. | |
523 if (last_discard_time_.is_null()) { | |
524 // This is the first discard this session. | |
525 TimeDelta interval = NowTicks() - start_time_; | |
526 int interval_seconds = static_cast<int>(interval.InSeconds()); | |
527 // Record time in seconds over an interval of approximately 1 day. | |
528 UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.Discard.InitialTime2", interval_seconds, | |
529 1, 100000, 50); | |
530 } else { | |
531 // Not the first discard, so compute time since last discard. | |
532 TimeDelta interval = NowTicks() - last_discard_time_; | |
533 int interval_ms = static_cast<int>(interval.InMilliseconds()); | |
534 // Record time in milliseconds over an interval of approximately 1 day. | |
535 // Start at 100 ms to get extra resolution in the target 750 ms range. | |
536 UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.Discard.IntervalTime2", interval_ms, 100, | |
537 100000 * 1000, 50); | |
538 } | |
539 // TODO(georgesak): Remove this #if when RecordMemoryStats is implemented for | |
540 // all platforms. | |
541 #if defined(OS_WIN) || defined(OS_CHROMEOS) | |
542 // Record system memory usage at the time of the discard. | |
543 metrics::RecordMemoryStats(metrics::RECORD_MEMORY_STATS_TAB_DISCARDED); | |
544 #endif | |
545 // Set up to record the next interval. | |
546 last_discard_time_ = NowTicks(); | |
547 } | |
548 | |
549 void TabManager::RecordRecentTabDiscard() { | |
550 // If Chrome is shutting down, do not do anything. | |
551 if (g_browser_process->IsShuttingDown()) | |
552 return; | |
553 | |
554 DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
555 // If the interval is changed, so should the histogram name. | |
556 UMA_HISTOGRAM_BOOLEAN("Tabs.Discard.DiscardInLastMinute", | |
557 recent_tab_discard_); | |
558 // Reset for the next interval. | |
559 recent_tab_discard_ = false; | |
560 } | |
561 | |
562 void TabManager::PurgeBrowserMemory() { | |
563 // Based on experimental evidence, attempts to free memory from renderers | |
564 // have been too slow to use in OOM situations (V8 garbage collection) or | |
565 // do not lead to persistent decreased usage (image/bitmap caches). This | |
566 // function therefore only targets large blocks of memory in the browser. | |
567 // Note that other objects will listen to MemoryPressureListener events | |
568 // to release memory. | |
569 for (TabContentsIterator it; !it.done(); it.Next()) { | |
570 WebContents* web_contents = *it; | |
571 // Screenshots can consume ~5 MB per web contents for platforms that do | |
572 // touch back/forward. | |
573 web_contents->GetController().ClearAllScreenshots(); | |
574 } | |
575 } | |
576 | |
577 int TabManager::GetTabCount() const { | |
578 int tab_count = 0; | |
579 for (auto* browser : *BrowserList::GetInstance()) | |
580 tab_count += browser->tab_strip_model()->count(); | |
581 return tab_count; | |
582 } | |
583 | |
584 void TabManager::AddTabStats(TabStatsList* stats_list) const { | |
585 BrowserList* browser_list = BrowserList::GetInstance(); | |
586 for (BrowserList::const_reverse_iterator browser_iterator = | |
587 browser_list->begin_last_active(); | |
588 browser_iterator != browser_list->end_last_active(); | |
589 ++browser_iterator) { | |
590 Browser* browser = *browser_iterator; | |
591 // |is_active_window| tells us whether this browser window is active. It is | |
592 // possible that none of the browser windows is active because it's some | |
593 // other application window in the foreground. | |
594 bool is_active_window = browser->window()->IsActive(); | |
595 AddTabStats(browser->tab_strip_model(), browser->is_app(), is_active_window, | |
596 stats_list); | |
597 } | |
598 } | |
599 | |
600 void TabManager::AddTabStats(const TabStripModel* model, | |
601 bool is_app, | |
602 bool active_model, | |
603 TabStatsList* stats_list) const { | |
604 for (int i = 0; i < model->count(); i++) { | |
605 WebContents* contents = model->GetWebContentsAt(i); | |
606 if (!contents->IsCrashed()) { | |
607 TabStats stats; | |
608 stats.is_app = is_app; | |
609 stats.is_internal_page = IsInternalPage(contents->GetLastCommittedURL()); | |
610 stats.is_media = IsMediaTab(contents); | |
611 stats.is_pinned = model->IsTabPinned(i); | |
612 stats.is_selected = active_model && model->IsTabSelected(i); | |
613 stats.is_discarded = GetWebContentsData(contents)->IsDiscarded(); | |
614 stats.has_form_entry = | |
615 contents->GetPageImportanceSignals().had_form_interaction; | |
616 stats.discard_count = GetWebContentsData(contents)->DiscardCount(); | |
617 stats.last_active = contents->GetLastActiveTime(); | |
618 stats.last_hidden = contents->GetLastHiddenTime(); | |
619 stats.render_process_host = contents->GetRenderProcessHost(); | |
620 stats.renderer_handle = contents->GetRenderProcessHost()->GetHandle(); | |
621 stats.child_process_host_id = contents->GetRenderProcessHost()->GetID(); | |
622 #if defined(OS_CHROMEOS) | |
623 stats.oom_score = delegate_->GetCachedOomScore(stats.renderer_handle); | |
624 #endif | |
625 stats.title = contents->GetTitle(); | |
626 stats.tab_contents_id = IdFromWebContents(contents); | |
627 stats_list->push_back(stats); | |
628 } | |
629 } | |
630 } | |
631 | |
632 // This function is called when |update_timer_| fires. It will adjust the clock | |
633 // if needed (if it detects that the machine was asleep) and will fire the stats | |
634 // updating on ChromeOS via the delegate. This function also tries to purge | |
635 // cache memory. | |
636 void TabManager::UpdateTimerCallback() { | |
637 // If Chrome is shutting down, do not do anything. | |
638 if (g_browser_process->IsShuttingDown()) | |
639 return; | |
640 | |
641 if (BrowserList::GetInstance()->empty()) | |
642 return; | |
643 | |
644 last_adjust_time_ = NowTicks(); | |
645 | |
646 #if defined(OS_CHROMEOS) | |
647 TabStatsList stats_list = GetTabStats(); | |
648 // This starts the CrOS specific OOM adjustments in /proc/<pid>/oom_score_adj. | |
649 delegate_->AdjustOomPriorities(stats_list); | |
650 #endif | |
651 | |
652 PurgeBackgroundedTabsIfNeeded(); | |
653 } | |
654 | |
655 base::TimeDelta TabManager::GetTimeToPurge( | |
656 base::TimeDelta min_time_to_purge) const { | |
657 return base::TimeDelta::FromSeconds( | |
658 base::RandInt(min_time_to_purge.InSeconds(), | |
659 min_time_to_purge.InSeconds() * kMinMaxTimeToPurgeRatio)); | |
660 } | |
661 | |
662 bool TabManager::ShouldPurgeNow(content::WebContents* content) const { | |
663 if (GetWebContentsData(content)->is_purged()) | |
664 return false; | |
665 | |
666 base::TimeDelta time_passed = | |
667 NowTicks() - GetWebContentsData(content)->LastInactiveTime(); | |
668 return time_passed > GetWebContentsData(content)->time_to_purge(); | |
669 } | |
670 | |
671 void TabManager::PurgeBackgroundedTabsIfNeeded() { | |
672 auto tab_stats = GetUnsortedTabStats(); | |
673 for (auto& tab : tab_stats) { | |
674 if (!tab.render_process_host->IsProcessBackgrounded()) | |
675 continue; | |
676 if (!CanSuspendBackgroundedRenderer(tab.child_process_host_id)) | |
677 continue; | |
678 | |
679 WebContents* content = GetWebContentsById(tab.tab_contents_id); | |
680 if (!content) | |
681 continue; | |
682 | |
683 bool purge_now = ShouldPurgeNow(content); | |
684 if (!purge_now) | |
685 continue; | |
686 | |
687 // Since |content|'s tab is kept inactive and background for more than | |
688 // time-to-purge time, its purged state changes: false => true. | |
689 GetWebContentsData(content)->set_is_purged(true); | |
690 // TODO(tasak): rename PurgeAndSuspend with a better name, e.g. | |
691 // RequestPurgeCache, because we don't suspend any renderers. | |
692 tab.render_process_host->PurgeAndSuspend(); | |
693 } | |
694 } | |
695 | |
696 WebContents* TabManager::DiscardWebContentsAt(int index, TabStripModel* model) { | |
697 // Can't discard active index. | |
698 if (model->active_index() == index) | |
699 return nullptr; | |
700 | |
701 WebContents* old_contents = model->GetWebContentsAt(index); | |
702 | |
703 // Can't discard tabs that are already discarded. | |
704 if (GetWebContentsData(old_contents)->IsDiscarded()) | |
705 return nullptr; | |
706 | |
707 // Record statistics before discarding to capture the memory state that leads | |
708 // to the discard. | |
709 RecordDiscardStatistics(); | |
710 | |
711 UMA_HISTOGRAM_BOOLEAN( | |
712 "TabManager.Discarding.DiscardedTabHasBeforeUnloadHandler", | |
713 old_contents->NeedToFireBeforeUnload()); | |
714 | |
715 WebContents* null_contents = | |
716 WebContents::Create(WebContents::CreateParams(model->profile())); | |
717 // Copy over the state from the navigation controller to preserve the | |
718 // back/forward history and to continue to display the correct title/favicon. | |
719 null_contents->GetController().CopyStateFrom(old_contents->GetController()); | |
720 | |
721 // Make sure to persist the last active time property. | |
722 null_contents->SetLastActiveTime(old_contents->GetLastActiveTime()); | |
723 // Copy over the discard count. | |
724 WebContentsData::CopyState(old_contents, null_contents); | |
725 | |
726 // Replace the discarded tab with the null version. | |
727 model->ReplaceWebContentsAt(index, null_contents); | |
728 // Mark the tab so it will reload when clicked on. | |
729 GetWebContentsData(null_contents)->SetDiscardState(true); | |
730 GetWebContentsData(null_contents)->IncrementDiscardCount(); | |
731 | |
732 // Make the tab PURGED to avoid purging null_contents. | |
733 GetWebContentsData(null_contents)->set_is_purged(true); | |
734 | |
735 // Discard the old tab's renderer. | |
736 // TODO(jamescook): This breaks script connections with other tabs. | |
737 // Find a different approach that doesn't do that, perhaps based on navigation | |
738 // to swappedout://. | |
739 delete old_contents; | |
740 recent_tab_discard_ = true; | |
741 | |
742 return null_contents; | |
743 } | |
744 | |
745 void TabManager::OnMemoryPressure( | |
746 base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level) { | |
747 // If Chrome is shutting down, do not do anything. | |
748 if (g_browser_process->IsShuttingDown()) | |
749 return; | |
750 | |
751 // Under critical pressure try to discard a tab. | |
752 if (memory_pressure_level == | |
753 base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) { | |
754 LogMemoryAndDiscardTab(); | |
755 } | |
756 // TODO(skuhne): If more memory pressure levels are introduced, consider | |
757 // calling PurgeBrowserMemory() before CRITICAL is reached. | |
758 } | |
759 | |
760 void TabManager::TabChangedAt(content::WebContents* contents, | |
761 int index, | |
762 TabChangeType change_type) { | |
763 if (change_type != TabChangeType::ALL) | |
764 return; | |
765 auto* data = GetWebContentsData(contents); | |
766 bool old_state = data->IsRecentlyAudible(); | |
767 bool current_state = contents->WasRecentlyAudible(); | |
768 if (old_state != current_state) { | |
769 data->SetRecentlyAudible(current_state); | |
770 data->SetLastAudioChangeTime(NowTicks()); | |
771 } | |
772 } | |
773 | |
774 void TabManager::ActiveTabChanged(content::WebContents* old_contents, | |
775 content::WebContents* new_contents, | |
776 int index, | |
777 int reason) { | |
778 GetWebContentsData(new_contents)->SetDiscardState(false); | |
779 // When ActiveTabChanged, |new_contents| purged state changes to be false. | |
780 GetWebContentsData(new_contents)->set_is_purged(false); | |
781 // If |old_contents| is set, that tab has switched from being active to | |
782 // inactive, so record the time of that transition. | |
783 if (old_contents) { | |
784 GetWebContentsData(old_contents)->SetLastInactiveTime(NowTicks()); | |
785 // Re-setting time-to-purge every time a tab becomes inactive. | |
786 GetWebContentsData(old_contents) | |
787 ->set_time_to_purge(GetTimeToPurge(min_time_to_purge_)); | |
788 } | |
789 } | |
790 | |
791 void TabManager::TabInsertedAt(TabStripModel* tab_strip_model, | |
792 content::WebContents* contents, | |
793 int index, | |
794 bool foreground) { | |
795 // Only interested in background tabs, as foreground tabs get taken care of by | |
796 // ActiveTabChanged. | |
797 if (foreground) | |
798 return; | |
799 | |
800 // A new background tab is similar to having a tab switch from being active to | |
801 // inactive. | |
802 GetWebContentsData(contents)->SetLastInactiveTime(NowTicks()); | |
803 // Re-setting time-to-purge every time a tab becomes inactive. | |
804 GetWebContentsData(contents)->set_time_to_purge( | |
805 GetTimeToPurge(min_time_to_purge_)); | |
806 } | |
807 | |
808 bool TabManager::IsMediaTab(WebContents* contents) const { | |
809 if (contents->WasRecentlyAudible()) | |
810 return true; | |
811 | |
812 scoped_refptr<MediaStreamCaptureIndicator> media_indicator = | |
813 MediaCaptureDevicesDispatcher::GetInstance() | |
814 ->GetMediaStreamCaptureIndicator(); | |
815 if (media_indicator->IsCapturingUserMedia(contents) || | |
816 media_indicator->IsBeingMirrored(contents)) { | |
817 return true; | |
818 } | |
819 | |
820 auto delta = NowTicks() - GetWebContentsData(contents)->LastAudioChangeTime(); | |
821 return delta < TimeDelta::FromSeconds(kAudioProtectionTimeSeconds); | |
822 } | |
823 | |
824 TabManager::WebContentsData* TabManager::GetWebContentsData( | |
825 content::WebContents* contents) const { | |
826 WebContentsData::CreateForWebContents(contents); | |
827 auto* web_contents_data = WebContentsData::FromWebContents(contents); | |
828 web_contents_data->set_test_tick_clock(test_tick_clock_); | |
829 return web_contents_data; | |
830 } | |
831 | |
832 TimeTicks TabManager::NowTicks() const { | |
833 if (!test_tick_clock_) | |
834 return TimeTicks::Now(); | |
835 | |
836 return test_tick_clock_->NowTicks(); | |
837 } | |
838 | |
839 // TODO(jamescook): This should consider tabs with references to other tabs, | |
840 // such as tabs created with JavaScript window.open(). Potentially consider | |
841 // discarding the entire set together, or use that in the priority computation. | |
842 content::WebContents* TabManager::DiscardTabImpl() { | |
843 DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
844 TabStatsList stats = GetTabStats(); | |
845 | |
846 if (stats.empty()) | |
847 return nullptr; | |
848 // Loop until a non-discarded tab to kill is found. | |
849 for (TabStatsList::const_reverse_iterator stats_rit = stats.rbegin(); | |
850 stats_rit != stats.rend(); ++stats_rit) { | |
851 int64_t least_important_tab_id = stats_rit->tab_contents_id; | |
852 if (CanDiscardTab(least_important_tab_id)) { | |
853 WebContents* new_contents = DiscardTabById(least_important_tab_id); | |
854 if (new_contents) | |
855 return new_contents; | |
856 } | |
857 } | |
858 return nullptr; | |
859 } | |
860 | |
861 // Check the variation parameter to see if a tab can be discarded only once or | |
862 // multiple times. | |
863 // Default is to only discard once per tab. | |
864 bool TabManager::CanOnlyDiscardOnce() const { | |
865 #if defined(OS_WIN) || defined(OS_MACOSX) | |
866 // On Windows and MacOS, default to discarding only once unless otherwise | |
867 // specified by the variation parameter. | |
868 // TODO(georgesak): Add Linux when automatic discarding is enabled for that | |
869 // platform. | |
870 std::string allow_multiple_discards = variations::GetVariationParamValue( | |
871 features::kAutomaticTabDiscarding.name, "AllowMultipleDiscards"); | |
872 return (allow_multiple_discards != "true"); | |
873 #else | |
874 return false; | |
875 #endif | |
876 } | |
877 | |
878 } // namespace memory | |
OLD | NEW |