910e62b5创建于 1月15日历史提交
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/code_cache/generated_code_cache.h"

#include <optional>

#include "base/feature_list.h"
#include "base/i18n/time_formatting.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "content/browser/code_cache/generated_code_cache_context.h"
#include "content/browser/renderer_host/code_cache_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/renderer.mojom.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/base/features.h"
#include "net/dns/mock_host_resolver.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/common/loader/code_cache_util.h"
#include "third_party/blink/public/common/page/v8_compile_hints_histograms.h"

namespace content {

namespace {

bool SupportsSharedWorker() {
  return base::FeatureList::IsEnabled(blink::features::kSharedWorker);
}

}  // namespace

enum class CodeCacheTestCase {
  kCachePartitioningEnabled,
  kCachePartitioningDisabled,
};

std::string CodeCacheTestCaseToString(CodeCacheTestCase param) {
  switch (param) {
    case CodeCacheTestCase::kCachePartitioningEnabled:
      return "CachePartitioningEnabled";
    case CodeCacheTestCase::kCachePartitioningDisabled:
      return "CachePartitioningDisabled";
  }
}

enum class BackgroundResourceFetchTestCase {
  kBackgroundResourceFetchEnabled,
  kBackgroundResourceFetchDisabled,
};

std::string BackgroundResourceFetchTestCaseToString(
    BackgroundResourceFetchTestCase param) {
  switch (param) {
    case BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled:
      return "BackgroundResourceFetchEnabled";
    case BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled:
      return "BackgroundResourceFetchDisabled";
  }
}

class CodeCacheBrowserTest
    : public ContentBrowserTest,
      public testing::WithParamInterface<
          std::pair<CodeCacheTestCase, BackgroundResourceFetchTestCase>> {
 public:
  CodeCacheBrowserTest() {
    // This test directly inspects and manipulates `GeneratedCodeCache` objects
    // which are not usable under the feature.
    feature_use_persistent_cache_for_code_cache_.InitAndDisableFeature(
        blink::features::kUsePersistentCacheForCodeCache);

    // Enable the split HTTP cache since the GeneratedCodeCache won't consider
    // partitioning by NIK unless the HTTP cache does.
    feature_split_cache_by_network_isolation_key_.InitAndEnableFeature(
        net::features::kSplitCacheByNetworkIsolationKey);

    // Enable third-party storage partitioning so that for the Shared Worker
    // test, different Shared Workers are used in the third-party contexts with
    // differing top-level sites.
    feature_third_party_storage_partitioning_.InitAndEnableFeature(
        net::features::kThirdPartyStoragePartitioning);

    feature_split_code_cache_by_network_isolation_key_.InitWithFeatureState(
        net::features::kSplitCodeCacheByNetworkIsolationKey,
        IsCachePartitioningEnabled());

    if (IsBackgroundResourceFetchEnabled()) {
      feature_background_resource_fetch_.InitAndEnableFeature(
          blink::features::kBackgroundResourceFetch);
    } else {
      feature_background_resource_fetch_.InitAndDisableFeature(
          blink::features::kBackgroundResourceFetch);
    }
  }

  bool IsCachePartitioningEnabled() const {
    return GetParam().first == CodeCacheTestCase::kCachePartitioningEnabled;
  }

  bool IsBackgroundResourceFetchEnabled() const {
    return GetParam().second ==
           BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled;
  }

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
        &CodeCacheBrowserTest::CachedScriptHandler, base::Unretained(this)));
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  std::unique_ptr<net::test_server::HttpResponse> CachedScriptHandler(
      const net::test_server::HttpRequest& request) {
    GURL absolute_url = embedded_test_server()->GetURL(request.relative_url);

    // Worker scripts will fetch this once the cacheable resource has been
    // loaded and the test logic (checking histograms) can continue.
    if (absolute_url.GetPath() == "/done.js") {
      GetUIThreadTaskRunner({})->PostTask(FROM_HERE, std::move(done_callback_));

      auto http_response =
          std::make_unique<net::test_server::BasicHttpResponse>();
      http_response->set_code(net::HTTP_OK);

      http_response->set_content("//done!");
      http_response->set_content_type("application/javascript");
      return http_response;
    }

    // Returns a JavaScript file that should be cacheable by the
    // GeneratedCodeCache (>1024 characters).
    if (absolute_url.GetPath() == "/cacheable.js") {
      if (trigger_validation_requests_ &&
          base::Contains(request.headers, "If-Modified-Since")) {
        auto http_response =
            std::make_unique<net::test_server::BasicHttpResponse>();
        http_response->set_code(net::HTTP_NOT_MODIFIED);
        last_cache_js_response_code_ = net::HTTP_NOT_MODIFIED;
        return http_response;
      }

      auto http_response =
          std::make_unique<net::test_server::BasicHttpResponse>();
      http_response->set_code(net::HTTP_OK);
      last_cache_js_response_code_ = net::HTTP_OK;

      std::string content = "let variable = 'hello!';\n";

      // Make sure the script is long enough to be eligible for caching.
      for (int i = 0; i < 16; i++) {
        content += std::string(64, '/') + '\n';
      }

      http_response->set_content(content);
      http_response->set_content_type("application/javascript");
      if (trigger_validation_requests_) {
        http_response->AddCustomHeader("Age", "3000");
        http_response->AddCustomHeader("Last-Modified",
                                       base::TimeFormatHTTP(base::Time::Now()));
      } else {
        http_response->AddCustomHeader("Cache-Control", "max-age=100000");
      }
      return http_response;
    }

    // Returns an HTML file that will load /cacheable.js.
    if (absolute_url.GetPath() == "/cacheable.html") {
      auto http_response =
          std::make_unique<net::test_server::BasicHttpResponse>();
      http_response->set_code(net::HTTP_OK);

      std::string content =
          "<html><head><title>Title</title></head>"
          "<script src='/cacheable.js'></script></html>";

      http_response->set_content(content);
      return http_response;
    }

    // Returns a JavaScript file that should itself be eligible for caching in
    // the GeneratedCodeCache and that will load /cacheable.js via
    // importScripts.
    if (absolute_url.GetPath() == "/worker.js") {
      auto http_response =
          std::make_unique<net::test_server::BasicHttpResponse>();
      http_response->set_code(net::HTTP_OK);

      // Self-terminate the worker just after loading the scripts so that the
      // parent context doesn't need to wait for the worker's termination when
      // cleaning up the test.
      std::string content = base::StringPrintf(
          "importScripts('cacheable.js', 'done.js');"
          "close();");
      for (int i = 0; i < 16; i++) {
        content += std::string(64, '/') + '\n';
      }

      http_response->set_content(content);
      http_response->set_content_type("application/javascript");
      return http_response;
    }

    // Return a page that will create a Shared Worker that uses /worker.js.
    if (absolute_url.GetPath() == "/shared-worker.html") {
      auto http_response =
          std::make_unique<net::test_server::BasicHttpResponse>();
      http_response->set_code(net::HTTP_OK);

      std::string content =
          "<html><head><title>Title</title></head>"
          "<script>let w = new SharedWorker('worker.js');</script></html>";

      http_response->set_content(content);
      return http_response;
    }

    // Returns a JavaScript module file that should be cacheable by the
    // GeneratedCodeCache (>1024 characters).
    if (absolute_url.GetPath() == "/cacheable_module.js") {
      auto http_response =
          std::make_unique<net::test_server::BasicHttpResponse>();
      http_response->set_code(net::HTTP_OK);

      std::string content =
          "const title = () => { return 'Module Loaded'; }\n"
          "export default title\n";

      // Make sure the script is long enough to be eligible for caching.
      for (int i = 0; i < 16; i++) {
        content += std::string(64, '/') + '\n';
      }

      http_response->set_content(content);
      http_response->set_content_type("application/javascript");
      http_response->AddCustomHeader("Cache-Control", "max-age=100000");
      return http_response;
    }

    // Returns an HTML file that will load /cacheable_module.js.
    if (absolute_url.GetPath() == "/cacheable_module.html") {
      auto http_response =
          std::make_unique<net::test_server::BasicHttpResponse>();
      http_response->set_code(net::HTTP_OK);

      std::string content =
          "<html><head><title>Title</title></head>"
          "<script type='module'>\n"
          "import title from \"./cacheable_module.js\";\n"
          "document.title = title();"
          "</script></html>";

      http_response->set_content(content);
      return http_response;
    }

    return nullptr;
  }

  void LoadIframe(const GURL& url) {
    // The script executed here isn't large enough to be eligible for the
    // Generated Code Cache and therefore shouldn't affect any metrics by being
    // run.
    EXPECT_TRUE(
        ExecJs(shell()->web_contents(),
               JsReplace("const iframe = document.createElement('iframe');"
                         "iframe.src = $1;"
                         "document.body.appendChild(iframe);",
                         url)));
    EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  }

 protected:
  void PurgeResourceCacheFromTheMainFrame() {
    base::RunLoop loop;
    static_cast<WebContentsImpl*>(shell()->web_contents())
        ->GetPrimaryFrameTree()
        .root()
        ->current_frame_host()
        ->GetProcess()
        ->GetRendererInterface()
        ->PurgeResourceCache(loop.QuitClosure());
    loop.Run();
  }
  void PurgeResourceCacheFromTheFirstSubFrame() {
    FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                              ->GetPrimaryFrameTree()
                              .root();
    CHECK(root->child_count() != 0);
    base::RunLoop loop;
    root->child_at(root->child_count() - 1)
        ->current_frame_host()
        ->GetProcess()
        ->GetRendererInterface()
        ->PurgeResourceCache(loop.QuitClosure());
    loop.Run();
  }
  GeneratedCodeCacheContext* GetGeneratedCodeCacheContext() {
    return shell()
        ->web_contents()
        ->GetBrowserContext()
        ->GetDefaultStoragePartition()
        ->GetGeneratedCodeCacheContext();
  }

  base::OnceClosure done_callback_;

  bool trigger_validation_requests_ = false;
  std::optional<net::HttpStatusCode> last_cache_js_response_code_;

 private:
  base::test::ScopedFeatureList feature_use_persistent_cache_for_code_cache_;
  base::test::ScopedFeatureList feature_split_cache_by_network_isolation_key_;
  base::test::ScopedFeatureList feature_third_party_storage_partitioning_;
  base::test::ScopedFeatureList
      feature_split_code_cache_by_network_isolation_key_;
  base::test::ScopedFeatureList feature_background_resource_fetch_;
};

INSTANTIATE_TEST_SUITE_P(
    All,
    CodeCacheBrowserTest,
    testing::Values(
        std::make_pair(
            CodeCacheTestCase::kCachePartitioningEnabled,
            BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled),
        std::make_pair(
            CodeCacheTestCase::kCachePartitioningEnabled,
            BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled),
        std::make_pair(
            CodeCacheTestCase::kCachePartitioningDisabled,
            BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled),
        std::make_pair(
            CodeCacheTestCase::kCachePartitioningDisabled,
            BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)),
    [](const testing::TestParamInfo<
        std::pair<CodeCacheTestCase, BackgroundResourceFetchTestCase>>& info) {
      return CodeCacheTestCaseToString(info.param.first) +
             BackgroundResourceFetchTestCaseToString(info.param.second);
    });

IN_PROC_BROWSER_TEST_P(CodeCacheBrowserTest, CachingFromThirdPartyFrames) {
  GURL a_com_parent_page =
      embedded_test_server()->GetURL("a.com", "/empty.html");
  GURL b_com_parent_page =
      embedded_test_server()->GetURL("b.com", "/empty.html");
  GURL c_com_cacheable_js_page =
      embedded_test_server()->GetURL("c.com", "/cacheable.html");
  {
    // Navigate to the parent page and load an iframe that requests a cacheable
    // javascript resource (/cacheable.js). This should result in one
    // GeneratedCodeCache miss and one create.
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), a_com_parent_page));

    LoadIframe(c_com_cacheable_js_page);

    FetchHistogramsFromChildProcesses();

    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kMiss, 1);
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kCreate, 1);
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kHit, 0);

    PurgeResourceCacheFromTheFirstSubFrame();
  }
  {
    // Navigate to the same test page again and check for a GeneratedCodeCache
    // hit.
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), a_com_parent_page));

    LoadIframe(c_com_cacheable_js_page);

    FetchHistogramsFromChildProcesses();

    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kHit, 1);

    PurgeResourceCacheFromTheFirstSubFrame();
  }

  {
    // Navigate to a page with a different top-level site and verify that:
    //
    //  - When cache partitioning is disabled, /cacheable.js is served from the
    //    GeneratedCodeCache, since resources will be cached per-origin.
    //
    //  - When cache partitioning is enabled, /cacheable.js is not served from
    //    the GeneratedCodeCache, since resources are also partitioned by the
    //    top-level site.
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), b_com_parent_page));

    LoadIframe(c_com_cacheable_js_page);

    FetchHistogramsFromChildProcesses();

    // Note: We don't check the kCreate counts below because for some reason,
    // the previous part of the test causes the /cacheable.js entry to be doomed
    // and then re-created in this part of the test (so the kCreate count is
    // always one more than we'd expect).
    if (IsCachePartitioningEnabled()) {
      histogram_tester.ExpectBucketCount(
          "SiteIsolatedCodeCache.JS.Behaviour",
          GeneratedCodeCache::CacheEntryStatus::kMiss, 1);
      histogram_tester.ExpectBucketCount(
          "SiteIsolatedCodeCache.JS.Behaviour",
          GeneratedCodeCache::CacheEntryStatus::kHit, 0);
    } else {
      histogram_tester.ExpectBucketCount(
          "SiteIsolatedCodeCache.JS.Behaviour",
          GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
      histogram_tester.ExpectBucketCount(
          "SiteIsolatedCodeCache.JS.Behaviour",
          GeneratedCodeCache::CacheEntryStatus::kHit, 1);
    }
  }
}

IN_PROC_BROWSER_TEST_P(CodeCacheBrowserTest, CachingFromIFrame) {
  GURL a_com_parent_page =
      embedded_test_server()->GetURL("a.com", "/empty.html");
  const std::string_view kLoadCacheableJSInIframeScript = R"(
    (async () => {
      await new Promise(resolve => {
        const iframe = document.createElement('iframe');
        document.body.appendChild(iframe);
        const script = iframe.contentWindow.document.createElement('script');
        script.addEventListener('load', resolve);
        script.src = '/cacheable.js';
        iframe.contentWindow.document.body.appendChild(script);
      });
    })();
  )";

  {
    // Navigate to the parent page and load an iframe that requests a cacheable
    // javascript resource (/cacheable.js) in subframe.
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), a_com_parent_page));

    EXPECT_TRUE(ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
                       kLoadCacheableJSInIframeScript));

    FetchHistogramsFromChildProcesses();

    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kMiss, 1);
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kCreate, 1);
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kHit, 0);

    PurgeResourceCacheFromTheFirstSubFrame();
  }
  {
    // Navigate to the same test page again, code cache will be produced.
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), a_com_parent_page));

    EXPECT_TRUE(ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
                       kLoadCacheableJSInIframeScript));

    FetchHistogramsFromChildProcesses();

    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kHit, 1);

    PurgeResourceCacheFromTheFirstSubFrame();
  }
  {
    // Navigate to the same test page again, code cache will be consumed.
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), a_com_parent_page));

    EXPECT_TRUE(ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
                       kLoadCacheableJSInIframeScript));

    FetchHistogramsFromChildProcesses();

    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kHit, 1);

    PurgeResourceCacheFromTheFirstSubFrame();
  }
}

IN_PROC_BROWSER_TEST_P(CodeCacheBrowserTest,
                       CachingFromThirdPartySharedWorkers) {
  if (!SupportsSharedWorker()) {
    return;
  }
  GURL a_com_parent_page =
      embedded_test_server()->GetURL("a.com", "/empty.html");
  GURL b_com_parent_page =
      embedded_test_server()->GetURL("b.com", "/empty.html");
  GURL c_com_worker_js_page =
      embedded_test_server()->GetURL("c.com", "/shared-worker.html");
  {
    // Navigate to a parent page and create a cross-site iframe that creates a
    // SharedWorker which attempts to load a cacheable script via importScripts.

    // Create a callback that will get called when the worker requests /done.js,
    // indicating that /worker.js and /cacheable.js have been loaded and we can
    // proceed with checking histograms.
    base::test::TestFuture<void> worker_done;
    done_callback_ = worker_done.GetCallback();

    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), a_com_parent_page));

    LoadIframe(c_com_worker_js_page);

    ASSERT_TRUE(worker_done.Wait());

    FetchHistogramsFromChildProcesses();

    // TODO(crbug.com/40628019): The kMiss and kCreate counts are zero
    // here because for Dedicated Workers and Shared Workers the Generated Code
    // Cache isn't used yet. Once it is, update these counts (there are two
    // scripts that could be cached, depending on the implementation -
    // worker.js, the script that contains the Shared Worker code, and
    // cacheable.js, which is loaded by the Shared Worker via a call to
    // importScripts).
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kCreate, 0);
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kHit, 0);
  }

  {
    base::test::TestFuture<void> worker_done;
    done_callback_ = worker_done.GetCallback();

    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), a_com_parent_page));

    LoadIframe(c_com_worker_js_page);

    ASSERT_TRUE(worker_done.Wait());

    FetchHistogramsFromChildProcesses();

    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
    // TODO(crbug.com/40628019): Once the Generated Code Cache is used for
    // Shared Workers, check that the scripts loaded from the worker context
    // were re-used from the test section above.
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kHit, 0);
  }

  {
    base::test::TestFuture<void> worker_done;
    done_callback_ = worker_done.GetCallback();

    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), b_com_parent_page));

    LoadIframe(c_com_worker_js_page);

    ASSERT_TRUE(worker_done.Wait());

    FetchHistogramsFromChildProcesses();

    // TODO(crbug.com/40628019): Once the Generated Code Cache is used
    // for Shared Workers, check that the worker scripts re-used in the test
    // section above do not get re-used for this part of the test when
    // `IsCachePartitioningEnabled()` returns true (but are re-used otherwise).
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kMiss, 0);
    histogram_tester.ExpectBucketCount(
        "SiteIsolatedCodeCache.JS.Behaviour",
        GeneratedCodeCache::CacheEntryStatus::kHit, 0);
  }
}

class CodeCacheSizeChecker {
 public:
  CodeCacheSizeChecker(GeneratedCodeCacheContext* cache_context,
                       const GURL& url,
                       const GURL& origin,
                       size_t expected_size)
      : cache_context_(cache_context),
        url_(url),
        origin_(origin),
        expected_size_(expected_size) {}

  size_t Wait() {
    base::test::TestFuture<void> done;
    done_callback_ = done.GetCallback();

    GeneratedCodeCacheContext::GetTaskRunner(cache_context_)
        ->PostTask(FROM_HERE,
                   base::BindOnce(&CodeCacheSizeChecker::GetCodeCache,
                                  base::Unretained(this)));
    CHECK(done.Wait());
    return expected_size_;
  }

 private:
  void GetCodeCache() {
    net::NetworkIsolationKey nik = net::NetworkIsolationKey(
        net::SchemefulSite(origin_), net::SchemefulSite(origin_));
    cache_context_->generated_js_code_cache()->FetchEntry(
        url_, GURL(), nik,
        base::BindOnce(&CodeCacheSizeChecker::FetchCallback,
                       base::Unretained(this)));
  }

  void FetchCallback(const base::Time&, mojo_base::BigBuffer data) {
    if (data.size() >= expected_size_) {
      GetUIThreadTaskRunner({})->PostTask(FROM_HERE, std::move(done_callback_));
    } else {
      // Retries because the CodeCacheHost's IPC may delay.
      GeneratedCodeCacheContext::GetTaskRunner(cache_context_)
          ->PostDelayedTask(FROM_HERE,
                            base::BindOnce(&CodeCacheSizeChecker::GetCodeCache,
                                           base::Unretained(this)),
                            base::Microseconds(100));
    }
  }

  scoped_refptr<GeneratedCodeCacheContext> cache_context_;
  const GURL url_;
  const GURL origin_;
  const size_t expected_size_;
  base::OnceClosure done_callback_;
};

IN_PROC_BROWSER_TEST_P(CodeCacheBrowserTest,
                       GeneratedCodeCacheSizeClassicScript) {
  // With this, we can query the code cache in a unified way in platforms which
  // use origin locks differently.
  CodeCacheHostImpl::SetUseEmptySecondaryKeyForTesting();
  GURL url = embedded_test_server()->GetURL("c.com", "/cacheable.html");
  EXPECT_TRUE(NavigateToURL(shell(), url));

  GeneratedCodeCacheContext* cache_context = GetGeneratedCodeCacheContext();
  // Wait until compile hints were written into the cache.
  const GURL& cacheable_script =
      embedded_test_server()->GetURL("c.com", "/cacheable.js");
  CodeCacheSizeChecker code_cache_size_checker(
      cache_context, cacheable_script,
      embedded_test_server()->GetURL("c.com", "/"),
      blink::kCodeCacheTimestampCachedMetaSize);
  EXPECT_EQ(blink::kCodeCacheTimestampCachedMetaSize,
            code_cache_size_checker.Wait());

  // Clear Blink side cache.
  PurgeResourceCacheFromTheMainFrame();
  // Navigate away.
  EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));

  // Navigate to the same page.
  EXPECT_TRUE(NavigateToURL(shell(), url));
  // We expect that the generated code cache is larger than the timestamp data.
  CodeCacheSizeChecker code_cache_size_checker2(
      cache_context, cacheable_script,
      embedded_test_server()->GetURL("c.com", "/"),
      blink::kCodeCacheTimestampCachedMetaSize + 1);
  code_cache_size_checker2.Wait();
}

// Validation requests are updating response time in the http cache so we need
// to verify that such cases are handled correctly. This test triggers code that
// compares the timestamps between the http cache and code cache, which is used
// to determine whether the code cache is valid or not. If there is a mismatch
// in timestamps the code cache will be dropped.
IN_PROC_BROWSER_TEST_P(CodeCacheBrowserTest, KeepCodeCacheWhenNotModified) {
  // With this, we can query the code cache in a unified way in platforms which
  // use origin locks differently.
  CodeCacheHostImpl::SetUseEmptySecondaryKeyForTesting();
  // Vital part of this test since http 304 responses change the response time
  // even though the content did not change.
  trigger_validation_requests_ = true;
  const GURL url = embedded_test_server()->GetURL("c.com", "/cacheable.html");
  EXPECT_TRUE(NavigateToURL(shell(), url));

  GeneratedCodeCacheContext* cache_context = GetGeneratedCodeCacheContext();
  // Wait until compile hints were written into the cache.
  const GURL cacheable_script =
      embedded_test_server()->GetURL("c.com", "/cacheable.js");
  // This is the size of the meta data header and the stored response time which
  // is the only actual data when there is no generated code in the cache.
  CodeCacheSizeChecker code_cache_size_checker(
      cache_context, cacheable_script,
      embedded_test_server()->GetURL("c.com", "/"),
      blink::kCodeCacheTimestampCachedMetaSize);
  EXPECT_EQ(blink::kCodeCacheTimestampCachedMetaSize,
            code_cache_size_checker.Wait());

  // Clear Blink side cache.
  PurgeResourceCacheFromTheMainFrame();
  // Navigate away.
  EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));

  // Navigate to the same page. This step will put compiled code in the cache.
  EXPECT_TRUE(NavigateToURL(shell(), url));
  // We expect that the generated code cache is larger than the timestamp data.
  // This means that we are storing generated code in the cache in addition to
  // the metadata, thereof the blink::kCodeCacheTimestampCachedMetaSize + 1
  // below.
  CodeCacheSizeChecker code_cache_size_checker2(
      cache_context, cacheable_script,
      embedded_test_server()->GetURL("c.com", "/"),
      blink::kCodeCacheTimestampCachedMetaSize + 1);
  code_cache_size_checker2.Wait();
  ASSERT_TRUE(last_cache_js_response_code_.has_value());
  ASSERT_EQ(net::HTTP_NOT_MODIFIED, last_cache_js_response_code_.value());

  // Clear Blink side cache.
  PurgeResourceCacheFromTheMainFrame();
  // Navigate away.
  EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
  last_cache_js_response_code_.reset();

  // Navigate to the same page a third time. This time the code cache should be
  // used and the data on disk should be kept even if we got a 304 response.
  EXPECT_TRUE(NavigateToURL(shell(), url));
  // We expect that the generated code cache is larger than the timestamp data.
  CodeCacheSizeChecker code_cache_size_checker3(
      cache_context, cacheable_script,
      embedded_test_server()->GetURL("c.com", "/"),
      blink::kCodeCacheTimestampCachedMetaSize + 1);
  code_cache_size_checker3.Wait();
  ASSERT_TRUE(last_cache_js_response_code_.has_value());
  ASSERT_EQ(net::HTTP_NOT_MODIFIED, last_cache_js_response_code_.value());
}

IN_PROC_BROWSER_TEST_P(CodeCacheBrowserTest,
                       GeneratedCodeCacheSizeModuleScript) {
  // With this, we can query the code cache in a unified way in platforms which
  // use origin locks differently.
  CodeCacheHostImpl::SetUseEmptySecondaryKeyForTesting();
  GURL url = embedded_test_server()->GetURL("c.com", "/cacheable_module.html");
  const std::u16string expected_title = u"Module Loaded";
  {
    TitleWatcher watcher(shell()->web_contents(), expected_title);
    EXPECT_TRUE(NavigateToURL(shell(), url));
    EXPECT_EQ(expected_title, watcher.WaitAndGetTitle());
  }

  GeneratedCodeCacheContext* cache_context = GetGeneratedCodeCacheContext();
  // Wait until compile hints were written into the cache.
  const GURL& cacheable_module_script =
      embedded_test_server()->GetURL("c.com", "/cacheable_module.js");
  CodeCacheSizeChecker code_cache_size_checker(
      cache_context, cacheable_module_script,
      embedded_test_server()->GetURL("c.com", "/"),
      blink::kCodeCacheTimestampCachedMetaSize);
  EXPECT_EQ(blink::kCodeCacheTimestampCachedMetaSize,
            code_cache_size_checker.Wait());

  // Clear Blink side cache.
  PurgeResourceCacheFromTheMainFrame();
  // Navigate away.
  EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));

  // Navigate to the same page.
  {
    TitleWatcher watcher(shell()->web_contents(), expected_title);
    EXPECT_TRUE(NavigateToURL(shell(), url));
    EXPECT_EQ(expected_title, watcher.WaitAndGetTitle());
  }
  // We expect that the generated code cache is larger than the timestamp data.
  CodeCacheSizeChecker code_cache_size_checker2(
      cache_context, cacheable_module_script,
      embedded_test_server()->GetURL("c.com", "/"),
      blink::kCodeCacheTimestampCachedMetaSize + 1);
  code_cache_size_checker2.Wait();
}

class CompileHintsBrowserTest : public ContentBrowserTest {
 public:
  CompileHintsBrowserTest() = default;

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
        &CompileHintsBrowserTest::CachedScriptHandler, base::Unretained(this)));
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  std::unique_ptr<net::test_server::HttpResponse> CachedScriptHandler(
      const net::test_server::HttpRequest& request) {
    GURL absolute_url = embedded_test_server()->GetURL(request.relative_url);

    // Returns a JavaScript file that should be cacheable by the
    // GeneratedCodeCache (>1024 characters).
    if (absolute_url.GetPath() == "/cacheable.js") {
      auto http_response =
          std::make_unique<net::test_server::BasicHttpResponse>();
      http_response->set_code(net::HTTP_OK);
      http_response->AddCustomHeader("Cache-Control", "max-age=604800");

      // The script has to include a function so that we can test compile hints
      // with it.
      std::string content = R"(
        let variable = 'hello!';
        function foo() {}
        foo();
        )";

      // Make sure the script is long enough to be eligible for caching.
      for (int i = 0; i < 16; i++) {
        content += std::string(64, '/') + '\n';
      }

      http_response->set_content(content);
      http_response->set_content_type("application/javascript");
      return http_response;
    }

    // Returns an HTML file that will load /cacheable.js.
    if (absolute_url.GetPath() == "/cacheable.html") {
      auto http_response =
          std::make_unique<net::test_server::BasicHttpResponse>();
      http_response->set_code(net::HTTP_OK);

      std::string content =
          "<html><head><title>Title</title></head>"
          "<script src='/cacheable.js'></script></html>";

      http_response->set_content(content);
      return http_response;
    }
    return nullptr;
  }
};

class LocalCompileHintsBrowserTest : public CompileHintsBrowserTest {
 public:
  LocalCompileHintsBrowserTest() {
    local_compile_hints_.InitAndEnableFeature(
        blink::features::kLocalCompileHints);
    interactive_detector_ignore_fcp_.InitAndEnableFeature(
        blink::features::kInteractiveDetectorIgnoreFcp);

    // This test directly inspects and manipulates `GeneratedCodeCache` objects
    // which are not usable under the feature.
    feature_use_persistent_cache_for_code_cache_.InitAndDisableFeature(
        blink::features::kUsePersistentCacheForCodeCache);
  }

 private:
  base::test::ScopedFeatureList local_compile_hints_;
  base::test::ScopedFeatureList interactive_detector_ignore_fcp_;
  base::test::ScopedFeatureList feature_use_persistent_cache_for_code_cache_;
};

class NoLocalCompileHintsBrowserTest : public CompileHintsBrowserTest {
 public:
  NoLocalCompileHintsBrowserTest() {
    // This test directly expects histograms from `GeneratedCodeCache` which are
    // not present under the feature.
    feature_use_persistent_cache_for_code_cache_.InitAndDisableFeature(
        blink::features::kUsePersistentCacheForCodeCache);

    local_compile_hints_.InitAndDisableFeature(
        blink::features::kLocalCompileHints);
  }

 private:
  base::test::ScopedFeatureList feature_use_persistent_cache_for_code_cache_;
  base::test::ScopedFeatureList local_compile_hints_;
};

IN_PROC_BROWSER_TEST_F(NoLocalCompileHintsBrowserTest, NoCompileHints) {
  // TODO(chromium:1495723): Migrate this test to use use counters once we no
  // longer have the histograms.

  // With this, we can query the code cache in a unified way in platforms which
  // use origin locks differently.
  CodeCacheHostImpl::SetUseEmptySecondaryKeyForTesting();

  GURL cacheable_page =
      embedded_test_server()->GetURL("c.com", "/cacheable.html");
  GURL other_page = embedded_test_server()->GetURL("a.com", "/empty.html");

  {
    // Navigate to the page which requests a cacheable
    // javascript resource (/cacheable.js).
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), cacheable_page));

    FetchHistogramsFromChildProcesses();
    EXPECT_EQ(
        1, histogram_tester.GetBucketCount(
               blink::v8_compile_hints::kStatusHistogram,
               blink::v8_compile_hints::Status::
                   kNoCompileHintsClassicNonStreaming) +
               histogram_tester.GetBucketCount(
                   blink::v8_compile_hints::kStatusHistogram,
                   blink::v8_compile_hints::Status::kNoCompileHintsStreaming));
  }

  // Navigate away.
  EXPECT_TRUE(NavigateToURL(shell(), other_page));

  {
    // Navigate to the same test page again and check that local compile hints
    // weren't hit.
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), cacheable_page));

    FetchHistogramsFromChildProcesses();

    EXPECT_EQ(
        1, histogram_tester.GetBucketCount(
               blink::v8_compile_hints::kStatusHistogram,
               blink::v8_compile_hints::Status::
                   kNoCompileHintsClassicNonStreaming) +
               histogram_tester.GetBucketCount(
                   blink::v8_compile_hints::kStatusHistogram,
                   blink::v8_compile_hints::Status::kNoCompileHintsStreaming));
  }

  // Navigate away.
  EXPECT_TRUE(NavigateToURL(shell(), other_page));

  {
    // Navigate to the same test page again and check for a code cache hit.
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), cacheable_page));

    FetchHistogramsFromChildProcesses();

    histogram_tester.ExpectBucketCount(
        blink::v8_compile_hints::kStatusHistogram,
        blink::v8_compile_hints::Status::kConsumeCodeCacheClassicNonStreaming,
        1);
  }
}

IN_PROC_BROWSER_TEST_F(LocalCompileHintsBrowserTest, LocalCompileHints) {
  // TODO(chromium:1495723): Migrate this test to use use counters once we no
  // longer have the histograms.

  // With this, we can query the code cache in a unified way in platforms which
  // use origin locks differently.
  CodeCacheHostImpl::SetUseEmptySecondaryKeyForTesting();

  GURL cacheable_page =
      embedded_test_server()->GetURL("c.com", "/cacheable.html");
  GURL other_page = embedded_test_server()->GetURL("a.com", "/empty.html");

  {
    // Navigate to the page which requests a cacheable
    // javascript resource (/cacheable.js). Once the page is interactive, local
    // compile hints will be generated.
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), cacheable_page));

    GeneratedCodeCacheContext* cache_context =
        shell()
            ->web_contents()
            ->GetBrowserContext()
            ->GetDefaultStoragePartition()
            ->GetGeneratedCodeCacheContext();
    // Wait until compile hints were written into the cache.
    const GURL& cacheable_script =
        embedded_test_server()->GetURL("c.com", "/cacheable.js");
    constexpr int kCompileHintsCacheSize = 28;  // Tag + actual data.
    CodeCacheSizeChecker code_cache_size_checker(
        cache_context, cacheable_script, GURL("http://c.com/"),
        kCompileHintsCacheSize);
    code_cache_size_checker.Wait();

    FetchHistogramsFromChildProcesses();
    EXPECT_EQ(1, histogram_tester.GetBucketCount(
                     blink::v8_compile_hints::kStatusHistogram,
                     blink::v8_compile_hints::Status::
                         kProduceCompileHintsClassicNonStreaming) +
                     histogram_tester.GetBucketCount(
                         blink::v8_compile_hints::kStatusHistogram,
                         blink::v8_compile_hints::Status::
                             kProduceCompileHintsStreaming));
    EXPECT_EQ(1,
              histogram_tester.GetBucketCount(
                  blink::v8_compile_hints::kLocalCompileHintsGeneratedHistogram,
                  blink::v8_compile_hints::LocalCompileHintsGenerated::kFinal));
  }

  // Navigate away.
  EXPECT_TRUE(NavigateToURL(shell(), other_page));

  {
    // Navigate to the same test page again and check for a local compile hints
    // hit.
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), cacheable_page));

    FetchHistogramsFromChildProcesses();

    EXPECT_EQ(1, histogram_tester.GetBucketCount(
                     blink::v8_compile_hints::kStatusHistogram,
                     blink::v8_compile_hints::Status::
                         kConsumeLocalCompileHintsClassicNonStreaming) +
                     histogram_tester.GetBucketCount(
                         blink::v8_compile_hints::kStatusHistogram,
                         blink::v8_compile_hints::Status::
                             kConsumeLocalCompileHintsStreaming));
  }

  // Navigate away.
  EXPECT_TRUE(NavigateToURL(shell(), other_page));

  {
    // Navigate to the same test page again and check for a code cache hit.
    base::HistogramTester histogram_tester;
    EXPECT_TRUE(NavigateToURL(shell(), cacheable_page));

    FetchHistogramsFromChildProcesses();

    histogram_tester.ExpectBucketCount(
        blink::v8_compile_hints::kStatusHistogram,
        blink::v8_compile_hints::Status::kConsumeCodeCacheClassicNonStreaming,
        1);
  }
}

}  // namespace content