chromium/third_party/blink/renderer/controller/oom_intervention_impl_test.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/controller/oom_intervention_impl.h"

#include <unistd.h>

#include <utility>

#include "base/files/file_util.h"
#include "base/run_loop.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/oom_intervention/oom_intervention_types.h"
#include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
#include "third_party/blink/renderer/controller/crash_memory_metrics_reporter_impl.h"
#include "third_party/blink/renderer/core/exported/web_view_impl.h"
#include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/platform/testing/task_environment.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/url_test_helpers.h"

namespace blink {

namespace {

const uint64_t kTestBlinkThreshold = 80 * 1024;
const uint64_t kTestPMFThreshold = 160 * 1024;
const uint64_t kTestSwapThreshold = 500 * 1024;
const uint64_t kTestVmSizeThreshold = 1024 * 1024;

class MockOomInterventionHost : public mojom::blink::OomInterventionHost {
 public:
  MockOomInterventionHost(
      mojo::PendingReceiver<mojom::blink::OomInterventionHost> receiver)
      : receiver_(this, std::move(receiver)) {}
  ~MockOomInterventionHost() override = default;

  void OnHighMemoryUsage() override {}

 private:
  mojo::Receiver<mojom::blink::OomInterventionHost> receiver_;
};

// Mock that allows setting mock memory usage.
class MockMemoryUsageMonitor : public MemoryUsageMonitor {
 public:
  MockMemoryUsageMonitor() = default;

  MemoryUsage GetCurrentMemoryUsage() override { return mock_memory_usage_; }

  // MemoryUsageMonitor will report the current memory usage as this value.
  void SetMockMemoryUsage(MemoryUsage usage) { mock_memory_usage_ = usage; }

 private:
  MemoryUsage mock_memory_usage_;
};

// Mock intervention class that uses a mock MemoryUsageMonitor.
class MockOomInterventionImpl : public OomInterventionImpl {
 public:
  MockOomInterventionImpl()
      : OomInterventionImpl(scheduler::GetSingleThreadTaskRunnerForTesting()),
        mock_memory_usage_monitor_(std::make_unique<MockMemoryUsageMonitor>()) {
  }
  ~MockOomInterventionImpl() override {}

  MemoryUsageMonitor& MemoryUsageMonitorInstance() override {
    return *mock_memory_usage_monitor_;
  }

  MockMemoryUsageMonitor* mock_memory_usage_monitor() {
    return mock_memory_usage_monitor_.get();
  }

 private:
  std::unique_ptr<OomInterventionMetrics> metrics_;
  std::unique_ptr<MockMemoryUsageMonitor> mock_memory_usage_monitor_;
};

}  // namespace

class OomInterventionImplTest : public testing::Test {
 public:
  void SetUp() override {
    intervention_ = std::make_unique<MockOomInterventionImpl>();
  }

  Page* DetectOnceOnBlankPage() {
    WebViewImpl* web_view = web_view_helper_.InitializeAndLoad("about:blank");
    Page* page = web_view->MainFrameImpl()->GetFrame()->GetPage();
    EXPECT_FALSE(page->Paused());
    RunDetection(true, false, false);
    return page;
  }

  void RunDetection(bool renderer_pause_enabled,
                    bool navigate_ads_enabled,
                    bool purge_v8_memory_enabled) {
    mojo::PendingRemote<mojom::blink::OomInterventionHost> remote_host;
    MockOomInterventionHost mock_host(
        remote_host.InitWithNewPipeAndPassReceiver());

    mojom::blink::DetectionArgsPtr args(mojom::blink::DetectionArgs::New());
    args->blink_workload_threshold = kTestBlinkThreshold;
    args->private_footprint_threshold = kTestPMFThreshold;
    args->swap_threshold = kTestSwapThreshold;
    args->virtual_memory_thresold = kTestVmSizeThreshold;

    intervention_->StartDetection(std::move(remote_host), std::move(args),
                                  renderer_pause_enabled, navigate_ads_enabled,
                                  purge_v8_memory_enabled);
    test::RunDelayedTasks(base::Seconds(1));
  }

 protected:
  test::TaskEnvironment task_environment_;
  std::unique_ptr<MockOomInterventionImpl> intervention_;
  frame_test_helpers::WebViewHelper web_view_helper_;
  std::unique_ptr<SimRequest> main_resource_;
};

TEST_F(OomInterventionImplTest, NoDetectionOnBelowThreshold) {
  MemoryUsage usage;
  // Set value less than the threshold to not trigger intervention.
  usage.v8_bytes = kTestBlinkThreshold - 1024;
  usage.blink_gc_bytes = 0;
  usage.partition_alloc_bytes = 0;
  usage.private_footprint_bytes = kTestPMFThreshold - 1024;
  usage.swap_bytes = kTestSwapThreshold - 1024;
  usage.vm_size_bytes = kTestVmSizeThreshold - 1024;
  intervention_->mock_memory_usage_monitor()->SetMockMemoryUsage(usage);

  Page* page = DetectOnceOnBlankPage();

  EXPECT_FALSE(page->Paused());
}

TEST_F(OomInterventionImplTest, BlinkThresholdDetection) {
  MemoryUsage usage;
  // Set value more than the threshold to trigger intervention.
  usage.v8_bytes = kTestBlinkThreshold + 1024;
  usage.blink_gc_bytes = 0;
  usage.partition_alloc_bytes = 0;
  usage.private_footprint_bytes = 0;
  usage.swap_bytes = 0;
  usage.vm_size_bytes = 0;
  intervention_->mock_memory_usage_monitor()->SetMockMemoryUsage(usage);

  Page* page = DetectOnceOnBlankPage();

  EXPECT_TRUE(page->Paused());
  intervention_.reset();
  EXPECT_FALSE(page->Paused());
}

TEST_F(OomInterventionImplTest, PmfThresholdDetection) {
  MemoryUsage usage;
  usage.v8_bytes = 0;
  usage.blink_gc_bytes = 0;
  usage.partition_alloc_bytes = 0;
  // Set value more than the threshold to trigger intervention.
  usage.private_footprint_bytes = kTestPMFThreshold + 1024;
  usage.swap_bytes = 0;
  usage.vm_size_bytes = 0;
  intervention_->mock_memory_usage_monitor()->SetMockMemoryUsage(usage);

  Page* page = DetectOnceOnBlankPage();

  EXPECT_TRUE(page->Paused());
  intervention_.reset();
  EXPECT_FALSE(page->Paused());
}

TEST_F(OomInterventionImplTest, SwapThresholdDetection) {
  MemoryUsage usage;
  usage.v8_bytes = 0;
  usage.blink_gc_bytes = 0;
  usage.partition_alloc_bytes = 0;
  usage.private_footprint_bytes = 0;
  // Set value more than the threshold to trigger intervention.
  usage.swap_bytes = kTestSwapThreshold + 1024;
  usage.vm_size_bytes = 0;
  intervention_->mock_memory_usage_monitor()->SetMockMemoryUsage(usage);

  Page* page = DetectOnceOnBlankPage();

  EXPECT_TRUE(page->Paused());
  intervention_.reset();
  EXPECT_FALSE(page->Paused());
}

TEST_F(OomInterventionImplTest, VmSizeThresholdDetection) {
  MemoryUsage usage;
  usage.v8_bytes = 0;
  usage.blink_gc_bytes = 0;
  usage.partition_alloc_bytes = 0;
  usage.private_footprint_bytes = 0;
  usage.swap_bytes = 0;
  // Set value more than the threshold to trigger intervention.
  usage.vm_size_bytes = kTestVmSizeThreshold + 1024;
  intervention_->mock_memory_usage_monitor()->SetMockMemoryUsage(usage);

  Page* page = DetectOnceOnBlankPage();

  EXPECT_TRUE(page->Paused());
  intervention_.reset();
  EXPECT_FALSE(page->Paused());
}

TEST_F(OomInterventionImplTest, StopWatchingAfterDetection) {
  MemoryUsage usage;
  usage.v8_bytes = 0;
  // Set value more than the threshold to trigger intervention.
  usage.blink_gc_bytes = kTestBlinkThreshold + 1024;
  usage.partition_alloc_bytes = 0;
  usage.private_footprint_bytes = 0;
  usage.swap_bytes = 0;
  usage.vm_size_bytes = 0;
  intervention_->mock_memory_usage_monitor()->SetMockMemoryUsage(usage);

  DetectOnceOnBlankPage();

  EXPECT_FALSE(intervention_->mock_memory_usage_monitor()->HasObserver(
      intervention_.get()));
}

TEST_F(OomInterventionImplTest, ContinueWatchingWithoutDetection) {
  MemoryUsage usage;
  // Set value less than the threshold to not trigger intervention.
  usage.v8_bytes = 0;
  usage.blink_gc_bytes = 0;
  usage.partition_alloc_bytes = 0;
  usage.private_footprint_bytes = 0;
  usage.swap_bytes = 0;
  usage.vm_size_bytes = 0;
  intervention_->mock_memory_usage_monitor()->SetMockMemoryUsage(usage);

  DetectOnceOnBlankPage();

  EXPECT_TRUE(intervention_->mock_memory_usage_monitor()->HasObserver(
      intervention_.get()));
}

// TODO(yuzus): Once OOPIF unit test infrastructure is ready, add a test case
// with OOPIF enabled.
TEST_F(OomInterventionImplTest, V1DetectionAdsNavigation) {
  MemoryUsage usage;
  usage.v8_bytes = 0;
  usage.blink_gc_bytes = 0;
  // Set value more than the threshold to trigger intervention.
  usage.partition_alloc_bytes = kTestBlinkThreshold + 1024;
  usage.private_footprint_bytes = 0;
  usage.swap_bytes = 0;
  usage.vm_size_bytes = 0;
  intervention_->mock_memory_usage_monitor()->SetMockMemoryUsage(usage);

  WebViewImpl* web_view = web_view_helper_.InitializeAndLoad("about:blank");
  Page* page = web_view->MainFrameImpl()->GetFrame()->GetPage();

  web_view->MainFrameImpl()->GetFrame()->GetDocument()->body()->setInnerHTML(
      "<iframe name='ad' src='data:text/html,'></iframe><iframe "
      "name='non-ad' src='data:text/html,'>");

  WebFrame* ad_iframe = web_view_helper_.LocalMainFrame()->FindFrameByName(
      WebString::FromUTF8("ad"));
  WebFrame* non_ad_iframe = web_view_helper_.LocalMainFrame()->FindFrameByName(
      WebString::FromUTF8("non-ad"));

  frame_test_helpers::PumpPendingRequestsForFrameToLoad(
      ad_iframe->ToWebLocalFrame());
  frame_test_helpers::PumpPendingRequestsForFrameToLoad(
      non_ad_iframe->ToWebLocalFrame());

  blink::FrameAdEvidence ad_evidence(/*parent_is_ad=*/false);
  ad_evidence.set_created_by_ad_script(
      mojom::FrameCreationStackEvidence::kCreatedByAdScript);
  ad_evidence.set_is_complete();

  auto* local_adframe = To<LocalFrame>(WebFrame::ToCoreFrame(*ad_iframe));
  local_adframe->SetAdEvidence(ad_evidence);
  auto* local_non_adframe =
      To<LocalFrame>(WebFrame::ToCoreFrame(*non_ad_iframe));

  EXPECT_TRUE(local_adframe->IsAdFrame());
  EXPECT_FALSE(local_non_adframe->IsAdFrame());
  EXPECT_EQ(local_adframe->GetDocument()->Url().GetString(), "data:text/html,");
  EXPECT_EQ(local_non_adframe->GetDocument()->Url().GetString(),
            "data:text/html,");

  RunDetection(true, true, false);

  EXPECT_TRUE(page->Paused());
  intervention_.reset();

  // The about:blank navigation won't actually happen until the page unpauses.
  frame_test_helpers::PumpPendingRequestsForFrameToLoad(
      ad_iframe->ToWebLocalFrame());
  EXPECT_EQ(local_adframe->GetDocument()->Url().GetString(), "about:blank");
  EXPECT_NE(local_non_adframe->GetDocument()->Url().GetString(), "about:blank");
}

TEST_F(OomInterventionImplTest, V2DetectionV8PurgeMemory) {
  MemoryUsage usage;
  usage.v8_bytes = 0;
  usage.blink_gc_bytes = 0;
  usage.partition_alloc_bytes = 0;
  usage.private_footprint_bytes = 0;
  usage.swap_bytes = 0;
  // Set value more than the threshold to trigger intervention.
  usage.vm_size_bytes = kTestVmSizeThreshold + 1024;
  intervention_->mock_memory_usage_monitor()->SetMockMemoryUsage(usage);

  WebViewImpl* web_view = web_view_helper_.InitializeAndLoad("about:blank");
  Page* page = web_view->MainFrameImpl()->GetFrame()->GetPage();
  auto* frame = To<LocalFrame>(page->MainFrame());
  EXPECT_FALSE(frame->DomWindow()->IsContextDestroyed());
  RunDetection(true, true, true);
  EXPECT_TRUE(frame->DomWindow()->IsContextDestroyed());
}

}  // namespace blink