| 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 |