blob: f1d167bd9f3fcbb1c511314e37bc96497a518fbe [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/metrics/oom/out_of_memory_reporter.h"
#include <memory>
#include <set>
#include <utility>
#include "base/at_exit.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/files/scoped_temp_dir.h"
#include "base/macros.h"
#include "base/optional.h"
#include "base/path_service.h"
#include "base/process/kill.h"
#include "base/run_loop.h"
#include "base/task/post_task.h"
#include "base/test/simple_test_tick_clock.h"
#include "build/build_config.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/ukm/content/source_url_recorder.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "net/base/net_errors.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_source.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#if defined(OS_ANDROID)
#include "chrome/common/descriptors_android.h"
#include "components/crash/content/browser/child_exit_observer_android.h"
#include "components/crash/content/browser/child_process_crash_observer_android.h"
#endif
#if defined(OS_ANDROID)
// This class listens for notifications that crash dumps have been processed.
// Notifications will come from all crashes, even if an associated crash dump
// was not created.
class CrashDumpWaiter : public crash_reporter::CrashMetricsReporter::Observer {
public:
CrashDumpWaiter() {
crash_reporter::CrashMetricsReporter::GetInstance()->AddObserver(this);
}
~CrashDumpWaiter() {
crash_reporter::CrashMetricsReporter::GetInstance()->RemoveObserver(this);
}
// Waits for the crash dump notification and returns whether the crash was
// considered a foreground oom.
const crash_reporter::CrashMetricsReporter::ReportedCrashTypeSet& Wait() {
waiter_.Run();
return reported_counts_;
}
private:
// CrashDumpManager::Observer:
void OnCrashDumpProcessed(
int rph_id,
const crash_reporter::CrashMetricsReporter::ReportedCrashTypeSet&
reported_counts) override {
reported_counts_ = reported_counts;
waiter_.Quit();
}
base::RunLoop waiter_;
crash_reporter::CrashMetricsReporter::ReportedCrashTypeSet reported_counts_;
DISALLOW_COPY_AND_ASSIGN(CrashDumpWaiter);
};
#endif // defined(OS_ANDROID)
class OutOfMemoryReporterTest : public ChromeRenderViewHostTestHarness,
public OutOfMemoryReporter::Observer {
public:
OutOfMemoryReporterTest() {}
~OutOfMemoryReporterTest() override {}
// ChromeRenderViewHostTestHarness:
void SetUp() override {
#if defined(OS_ANDROID)
crash_reporter::ChildExitObserver::Create();
crash_reporter::ChildExitObserver::GetInstance()->RegisterClient(
std::make_unique<crash_reporter::ChildProcessCrashObserver>());
#endif
ChromeRenderViewHostTestHarness::SetUp();
EXPECT_NE(content::ChildProcessHost::kInvalidUniqueID, process()->GetID());
OutOfMemoryReporter::CreateForWebContents(web_contents());
OutOfMemoryReporter* reporter =
OutOfMemoryReporter::FromWebContents(web_contents());
reporter->AddObserver(this);
auto tick_clock = std::make_unique<base::SimpleTestTickClock>();
test_tick_clock_ = tick_clock.get();
reporter->SetTickClockForTest(std::move(tick_clock));
// Ensure clock is set to something that's not 0 to begin.
test_tick_clock_->Advance(base::TimeDelta::FromSeconds(1));
test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
ukm::InitializeSourceUrlRecorderForWebContents(web_contents());
}
// OutOfMemoryReporter::Observer:
void OnForegroundOOMDetected(const GURL& url,
ukm::SourceId source_id) override {
last_oom_url_ = url;
}
void SimulateRendererCreated() {
content::NotificationService::current()->Notify(
content::NOTIFICATION_RENDERER_PROCESS_CREATED,
content::Source<content::RenderProcessHost>(process()),
content::NotificationService::NoDetails());
}
void SimulateOOM() {
test_tick_clock_->Advance(base::TimeDelta::FromSeconds(3));
SimulateRendererCreated();
#if defined(OS_ANDROID)
process()->SimulateRenderProcessExit(base::TERMINATION_STATUS_OOM_PROTECTED,
0);
#elif defined(OS_CHROMEOS)
process()->SimulateRenderProcessExit(
base::TERMINATION_STATUS_PROCESS_WAS_KILLED_BY_OOM, 0);
#else
process()->SimulateRenderProcessExit(base::TERMINATION_STATUS_OOM, 0);
#endif
}
// Runs a closure which should simulate some sort of crash, and waits until
// the OutOfMemoryReporter *should* have received a notification for it.
void RunCrashClosureAndWait(base::OnceClosure crash_closure,
bool oom_expected) {
#if defined(OS_ANDROID)
CrashDumpWaiter crash_waiter;
std::move(crash_closure).Run();
const auto& reported_counts = crash_waiter.Wait();
EXPECT_EQ(oom_expected ? 1u : 0u,
reported_counts.count(
crash_reporter::CrashMetricsReporter::ProcessedCrashCounts::
kRendererForegroundVisibleOom));
// Since the observer list is not ordered, it isn't guaranteed that the
// OutOfMemoryReporter will be notified at this point. Flush the current
// message loop and task scheduler of tasks.
content::RunAllTasksUntilIdle();
#else
// No need to wait on non-android platforms. The message will be
// synchronous.
std::move(crash_closure).Run();
#endif
}
void SimulateOOMAndWait() {
RunCrashClosureAndWait(base::BindOnce(&OutOfMemoryReporterTest::SimulateOOM,
base::Unretained(this)),
true);
}
void CheckUkmMetricRecorded(const GURL& url, int64_t time_delta) {
const auto& entries = test_ukm_recorder_->GetEntriesByName(
ukm::builders::Tab_RendererOOM::kEntryName);
EXPECT_EQ(1u, entries.size());
for (const auto* entry : entries) {
test_ukm_recorder_->ExpectEntrySourceHasUrl(entry, url);
test_ukm_recorder_->ExpectEntryMetric(
entry, ukm::builders::Tab_RendererOOM::kTimeSinceLastNavigationName,
time_delta);
}
}
protected:
base::ShadowingAtExitManager at_exit_;
base::Optional<GURL> last_oom_url_;
std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_;
private:
base::SimpleTestTickClock* test_tick_clock_;
DISALLOW_COPY_AND_ASSIGN(OutOfMemoryReporterTest);
};
TEST_F(OutOfMemoryReporterTest, SimpleOOM) {
const GURL url("https://example.test/");
NavigateAndCommit(url);
SimulateOOMAndWait();
EXPECT_TRUE(last_oom_url_.has_value());
EXPECT_EQ(url, last_oom_url_.value());
CheckUkmMetricRecorded(url, 3000);
}
TEST_F(OutOfMemoryReporterTest, NormalCrash_NoOOM) {
const GURL url("https://example.test/");
NavigateAndCommit(url);
SimulateRendererCreated();
#if defined(OS_ANDROID)
crash_reporter::ChildExitObserver::GetInstance()->ChildReceivedCrashSignal(
process()->GetProcess().Handle(), SIGSEGV);
#endif
RunCrashClosureAndWait(
base::BindOnce(&content::MockRenderProcessHost::SimulateRenderProcessExit,
base::Unretained(process()),
base::TERMINATION_STATUS_PROCESS_WAS_KILLED, 0),
false);
EXPECT_FALSE(last_oom_url_.has_value());
const auto& entries = test_ukm_recorder_->GetEntriesByName(
ukm::builders::Tab_RendererOOM::kEntryName);
EXPECT_EQ(0u, entries.size());
}
TEST_F(OutOfMemoryReporterTest, SubframeNavigation_IsNotLogged) {
const GURL url("https://example.test/");
NavigateAndCommit(url);
// Navigate a subframe, make sure it isn't the navigation that is logged.
const GURL subframe_url("https://subframe.test/");
auto* subframe =
content::RenderFrameHostTester::For(main_rfh())->AppendChild("subframe");
subframe = content::NavigationSimulator::NavigateAndCommitFromDocument(
subframe_url, subframe);
EXPECT_TRUE(subframe);
SimulateOOMAndWait();
EXPECT_TRUE(last_oom_url_.has_value());
EXPECT_EQ(url, last_oom_url_.value());
CheckUkmMetricRecorded(url, 3000);
}
TEST_F(OutOfMemoryReporterTest, OOMOnPreviousPage) {
const GURL url1("https://example.test1/");
const GURL url2("https://example.test2/");
const GURL url3("https://example.test2/");
NavigateAndCommit(url1);
NavigateAndCommit(url2);
// Should not commit.
content::NavigationSimulator::NavigateAndFailFromBrowser(web_contents(), url3,
net::ERR_ABORTED);
SimulateOOMAndWait();
EXPECT_TRUE(last_oom_url_.has_value());
EXPECT_EQ(url2, last_oom_url_.value());
CheckUkmMetricRecorded(url2, 3000);
last_oom_url_.reset();
NavigateAndCommit(url1);
// Should navigate to an error page.
content::NavigationSimulator::NavigateAndFailFromBrowser(
web_contents(), url3, net::ERR_CONNECTION_RESET);
// Don't report OOMs on error pages.
SimulateOOMAndWait();
EXPECT_FALSE(last_oom_url_.has_value());
// Only the first OOM is recorded.
CheckUkmMetricRecorded(url2, 3000);
}