Browsertest based Protected Audience auction simulator
Add a new tool for simulating auctions with custom parameters:
-delays with downloading and running bidding and scoring scripts
-delays with downloading bidding and scoring signals
-the numbers of interest groups, sellers, owners and ads
-the number of auctions & if they should run serially or in parallel.
The tool is useful for profiling hypothetical scenarios locally.
BYPASS_LARGE_CHANGE_WARNING
Bug: 361083904
Change-Id: Ia3eb0189bb08dd3b1e71243c58b47703773ee4d1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5331524
Reviewed-by: Amy Huang <akhuang@google.com>
Reviewed-by: danakj <danakj@chromium.org>
Reviewed-by: Russ Hamilton <behamilton@google.com>
Commit-Queue: Abigail Katcoff <abigailkatcoff@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1363267}
diff --git a/BUILD.gn b/BUILD.gn
index b3a95730..41fc0eca 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -198,6 +198,7 @@
"//components/url_formatter/tools:format_url",
"//components/viz:viz_perftests",
"//components/viz:viz_unittests",
+ "//content/browser/interest_group/tools:adjustable_auction",
"//content/shell:content_shell",
"//content/test:content_browsertests",
"//content/test:content_unittests",
diff --git a/content/browser/interest_group/tools/BUILD.gn b/content/browser/interest_group/tools/BUILD.gn
new file mode 100644
index 0000000..67e4604
--- /dev/null
+++ b/content/browser/interest_group/tools/BUILD.gn
@@ -0,0 +1,18 @@
+# Copyright 2024 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+executable("adjustable_auction") {
+ testonly = true
+ check_includes = false
+
+ sources = [ "adjustable_auction.cc" ]
+ deps = [
+ "//content/test:browsertest_support",
+ "//content/test:content_browsertests",
+ "//content/test:content_test_mojo_bindings",
+ "//content/test:test_support",
+ ]
+
+ defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
+}
diff --git a/content/browser/interest_group/tools/DEPS b/content/browser/interest_group/tools/DEPS
new file mode 100644
index 0000000..80fb0d6
--- /dev/null
+++ b/content/browser/interest_group/tools/DEPS
@@ -0,0 +1,4 @@
+include_rules = [
+ "+content/shell/browser/shell.h",
+ "+content/shell/common/shell_switches.h"
+]
\ No newline at end of file
diff --git a/content/browser/interest_group/tools/adjustable_auction.cc b/content/browser/interest_group/tools/adjustable_auction.cc
new file mode 100644
index 0000000..e6e277f
--- /dev/null
+++ b/content/browser/interest_group/tools/adjustable_auction.cc
@@ -0,0 +1,589 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// Provides a browsertest-based tool for running simulated auctions with
+// custom parameters. The tool is useful for profiling hypothetical scenarios
+// locally.
+
+#include <cstddef>
+#include <fstream>
+#include <string_view>
+
+#include "base/command_line.h"
+#include "base/functional/bind.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/test/gtest_util.h"
+#include "base/test/launcher/test_launcher.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/test_suite.h"
+#include "base/time/time.h"
+#include "base/values.h"
+#include "build/build_config.h"
+#include "content/browser/interest_group/ad_auction_service_impl.h"
+#include "content/browser/interest_group/interest_group_manager_impl.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/common/content_client.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_content_browser_client.h"
+#include "content/public/test/content_browser_test_shell_main_delegate.h"
+#include "content/public/test/content_browser_test_utils.h"
+#include "content/public/test/content_test_suite_base.h"
+#include "content/public/test/test_launcher.h"
+#include "content/public/test/url_loader_interceptor.h"
+#include "content/shell/browser/shell.h"
+#include "content/shell/common/shell_switches.h"
+#include "net/dns/mock_host_resolver.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/interest_group/test_interest_group_builder.h"
+
+namespace content {
+
+namespace {
+
+constexpr char kScoringLogicRelativeURL[] =
+ "/interest_group/generated_decision_logic.js";
+
+constexpr char kBiddingLogicRelativeURL[] =
+ "/interest_group/generated_bidding_logic.js";
+
+constexpr char kBiddingSignalsRelativeURL[] =
+ "/interest_group/generated_trusted_bidding_signals.json";
+
+constexpr char kScoringSignalsRelativeURL[] =
+ "/interest_group/generated_trusted_scoring_signals.json";
+
+constexpr char kFledgeScriptHeaders[] =
+ "HTTP/1.1 200 OK\n"
+ "Ad-Auction-Allowed: true\n";
+
+// This function is a delay mechanism for bidding and scoring scripts. This
+// function is used because Date() is disabled in worklets.
+constexpr char kStallingJavascriptFunction[] = R"(
+ function runNAdditions(script_delay){
+ var i = 0;
+ for (var index = 0; index < script_delay; ++index) {
+ i = 67 + 239;
+ }
+ })";
+
+class AllowAllContentBrowserClient
+ : public ContentBrowserTestContentBrowserClient {
+ public:
+ explicit AllowAllContentBrowserClient() = default;
+
+ AllowAllContentBrowserClient(const AllowAllContentBrowserClient&) = delete;
+ AllowAllContentBrowserClient& operator=(const AllowAllContentBrowserClient&) =
+ delete;
+
+ // ContentBrowserClient overrides:
+ bool IsInterestGroupAPIAllowed(
+ content::RenderFrameHost* render_frame_host,
+ ContentBrowserClient::InterestGroupApiOperation operation,
+ const url::Origin& top_frame_origin,
+ const url::Origin& api_origin) override {
+ return true;
+ }
+
+ bool IsPrivacySandboxReportingDestinationAttested(
+ content::BrowserContext* browser_context,
+ const url::Origin& destination_origin,
+ content::PrivacySandboxInvokingAPI invoking_api) override {
+ return true;
+ }
+};
+
+class NetworkResponder {
+ public:
+ using ResponseHeaders = std::vector<std::pair<std::string, std::string>>;
+
+ NetworkResponder() = default;
+
+ NetworkResponder(const NetworkResponder&) = delete;
+ NetworkResponder& operator=(const NetworkResponder&) = delete;
+
+ void RegisterScoringScript(const std::string& url_path,
+ int script_delay,
+ int network_delay) {
+ std::string script =
+ kStallingJavascriptFunction + base::StringPrintf(R"(
+ function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals,
+ browserSignals) {
+ runNAdditions(%d);
+ return {desirability: bid, allowComponentAuction: true};
+})",
+ script_delay);
+ RegisterNetworkResponse(url_path, kStallingJavascriptFunction + script,
+ "application/javascript", network_delay);
+ }
+
+ void RegisterBidderScript(const std::string& url_path,
+ int script_delay,
+ int network_delay) {
+ std::string script =
+ kStallingJavascriptFunction + base::StringPrintf(R"(
+function generateBid(
+ interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
+ unusedBrowserSignals) {
+ const ad = interestGroup.ads[0];
+ let result = {'ad': ad, 'bid': 1, 'render': ad.renderURL,
+ 'allowComponentAuction': true};
+ runNAdditions(%d);
+ return result;
+})",
+ script_delay);
+ RegisterNetworkResponse(url_path, script, "application/javascript",
+ network_delay);
+ }
+
+ void RegisterTrustedBiddingSignals(const std::string& url_path,
+ int network_delay) {
+ std::string json_response = R"({ "keys": { "key1": "1" } })";
+ RegisterNetworkResponse(url_path, json_response, "application/json",
+ network_delay,
+ "Ad-Auction-Bidding-Signals-Format-Version: 2\n");
+ }
+
+ void RegisterTrustedScoringSignals(const std::string& url_path,
+ int network_delay) {
+ std::string json_response = R"({ "keys": { "key1": "1" } })";
+ RegisterNetworkResponse(url_path, json_response, "application/json",
+ network_delay);
+ }
+
+ private:
+ struct Response {
+ std::string body;
+ std::string extra_headers;
+ std::string content_type;
+ base::TimeDelta network_delay;
+ };
+
+ void RegisterNetworkResponse(const std::string& url_path,
+ const std::string& body,
+ const std::string& content_type,
+ int network_delay,
+ std::string extra_headers = "") {
+ Response response;
+ response.body = body;
+ response.content_type = content_type;
+ response.extra_headers = extra_headers;
+ response.network_delay = base::Milliseconds(network_delay);
+ base::AutoLock auto_lock(response_map_lock_);
+ response_map_[url_path] = response;
+ }
+
+ bool RequestHandler(URLLoaderInterceptor::RequestParams* params) {
+ base::AutoLock auto_lock(response_map_lock_);
+ const auto it = response_map_.find(params->url_request.url.path());
+ if (it == response_map_.end()) {
+ return false;
+ }
+ base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce(&NetworkResponder::DoDelayedResponse, it->second.body,
+ it->second.content_type, it->second.extra_headers,
+ std::move(params->client)),
+ it->second.network_delay);
+ return true;
+ }
+
+ static void DoDelayedResponse(
+ std::string body,
+ std::string content_type,
+ std::string extra_headers,
+ const mojo::Remote<network::mojom::URLLoaderClient> client) {
+ URLLoaderInterceptor::WriteResponse(
+ base::StrCat({kFledgeScriptHeaders, extra_headers,
+ "Content-type: ", content_type, "\n"}),
+ body, client.get());
+ }
+
+ base::Lock response_map_lock_;
+
+ // For each HTTPS request, we see if any path in the map matches the request
+ // path. If so, the server returns the mapped value string as the response.
+ base::flat_map<std::string, Response> response_map_
+ GUARDED_BY(response_map_lock_);
+
+ // Handles all network requests.
+ URLLoaderInterceptor network_interceptor_{
+ base::BindRepeating(&NetworkResponder::RequestHandler,
+ base::Unretained(this))};
+};
+
+class AdjustableAuction : public ContentBrowserTest {
+ public:
+ inline static std::string filename_to_save_histogram = "histograms.txt";
+
+ // Wait between joining interest groups and the first auction, measured in
+ // seconds.
+ inline static size_t kPreauctionLogicDelay = 5;
+
+ inline static size_t kAdsPerInterestGroup = 30;
+ inline static size_t kInterestGroupsPerOwner = 100;
+ inline static size_t kSellers = 1;
+ inline static size_t kOwners = 2;
+
+ // Script delays measured in number of operations.
+ inline static size_t kBiddingLogicDelay = 20000000;
+ inline static size_t kScoringLogicDelay = 10000000;
+
+ // Network delays measured in milliseconds.
+ inline static size_t kBiddingNetworkDelay = 100;
+ inline static size_t kScoringNetworkDelay = 80;
+ inline static size_t kTrustedBiddingSignalsDelay = 200;
+ inline static size_t kTrustedScoringSignalsDelay = 100;
+
+ inline static size_t kAuctions = 1;
+ inline static bool kAuctionsAreParallel = false;
+
+ AdjustableAuction() {
+ feature_list_.InitWithFeatures(
+ /*enabled_features=*/
+ {blink::features::kInterestGroupStorage,
+ blink::features::kAdInterestGroupAPI, blink::features::kFledge,
+ blink::features::kAllowURNsInIframes,
+ blink::features::kFledgeDirectFromSellerSignalsHeaderAdSlot},
+ /*disabled_features=*/
+ {blink::features::kFledgeEnforceKAnonymity,
+ features::kCookieDeprecationFacilitatedTesting});
+ }
+
+ ~AdjustableAuction() override { content_browser_client_.reset(); }
+
+ void SetUpOnMainThread() override {
+ ContentBrowserTest::SetUpOnMainThread();
+ host_resolver()->AddRule("*", "127.0.0.1");
+ ASSERT_TRUE(embedded_test_server()->Start());
+ https_server_ = std::make_unique<net::EmbeddedTestServer>(
+ net::test_server::EmbeddedTestServer::TYPE_HTTPS);
+ https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
+ https_server_->AddDefaultHandlers(GetTestDataFilePath());
+ network_responder_ = std::make_unique<NetworkResponder>();
+ ASSERT_TRUE(https_server_->Start());
+ manager_ = static_cast<InterestGroupManagerImpl*>(
+ shell()
+ ->web_contents()
+ ->GetBrowserContext()
+ ->GetDefaultStoragePartition()
+ ->GetInterestGroupManager());
+ content_browser_client_ = std::make_unique<AllowAllContentBrowserClient>();
+ }
+
+ void TearDownOnMainThread() override {
+ manager_ = nullptr; // don't dangle once StoragePartition cleans it up.
+ network_responder_.reset();
+ ContentBrowserTest::TearDownOnMainThread();
+ }
+
+ content::EvalJsResult RunAuctionsAndWait(
+ const std::string& auction_config_json,
+ int n_auctions) {
+ return EvalJs(shell(), base::StringPrintf(
+ R"(
+(async function() {
+ let auctionConfig = %s;
+ let nAuctions = %d;
+ try {
+ for (var i = 0; i < nAuctions; ++i){
+ await navigator.runAdAuction(auctionConfig);
+ }
+ } catch (e) {
+ return e.toString();
+ }
+})())",
+ auction_config_json.c_str(), n_auctions));
+ }
+
+ content::EvalJsResult RunParallelAuctionsAndWait(
+ const std::string& auction_config_json,
+ int n_auctions) {
+ return EvalJs(shell(), base::StringPrintf(
+ R"(
+(async function() {
+ let auctionConfig = %s;
+ let nAuctions = %d;
+ try {
+ const auction_configs = Array(nAuctions).fill(auctionConfig);
+ return await Promise.allSettled(auction_configs.map(async (t) =>
+ {await navigator.runAdAuction(t)}));
+ } catch (e) {
+ return e.toString();
+ }
+})())",
+ auction_config_json.c_str(), n_auctions));
+ }
+
+ protected:
+ std::unique_ptr<net::EmbeddedTestServer> https_server_;
+ base::test::ScopedFeatureList feature_list_;
+ std::unique_ptr<AllowAllContentBrowserClient> content_browser_client_;
+ raw_ptr<InterestGroupManagerImpl> manager_;
+
+ std::unique_ptr<NetworkResponder> network_responder_;
+};
+
+IN_PROC_BROWSER_TEST_F(AdjustableAuction, RunAdjustableAuction) {
+ GURL test_url = https_server_->GetURL("a.test", "/page_with_iframe.html");
+ ASSERT_TRUE(NavigateToURL(shell(), test_url));
+
+ // Set up network responses.
+ network_responder_->RegisterScoringScript(
+ kScoringLogicRelativeURL, kScoringLogicDelay, kScoringNetworkDelay);
+ network_responder_->RegisterBidderScript(
+ kBiddingLogicRelativeURL, kBiddingLogicDelay, kBiddingNetworkDelay);
+ network_responder_->RegisterTrustedBiddingSignals(
+ kBiddingSignalsRelativeURL, kTrustedBiddingSignalsDelay);
+ network_responder_->RegisterTrustedScoringSignals(
+ kScoringSignalsRelativeURL, kTrustedScoringSignalsDelay);
+
+ // Join interest groups before running any auctions.
+ // Keep the buyers in a Value so that we can put this directly into the
+ // auction config.
+ base::Value::List buyers_for_auction;
+ for (size_t owner_i = 0; owner_i < kOwners; ++owner_i) {
+ std::string owner_str =
+ base::StrCat({"origin", base::NumberToString(owner_i), ".b.test"});
+ auto bidding_url =
+ https_server_->GetURL(owner_str, kBiddingLogicRelativeURL);
+ url::Origin owner_origin = url::Origin::Create(bidding_url);
+
+ buyers_for_auction.Append(owner_origin.GetURL().spec());
+ GURL bidding_signals_url =
+ https_server_->GetURL(owner_str, kBiddingSignalsRelativeURL);
+
+ for (size_t ig_i = 0; ig_i < kInterestGroupsPerOwner; ++ig_i) {
+ std::vector<blink::InterestGroup::Ad> ads;
+ for (size_t ad_i = 0; ad_i < kAdsPerInterestGroup; ++ad_i) {
+ GURL ad_url = https_server_->GetURL(
+ "c.test", base::StrCat({"/test?test_render_url_here_and_some_more_"
+ "words_for_extra_length",
+ base::NumberToString(ad_i)}));
+
+ ads.emplace_back(ad_url, R"({"ad":"metadata","here":[1,2,3,4,5]})");
+ }
+ manager_->JoinInterestGroup(
+ blink::TestInterestGroupBuilder(
+ /*owner=*/owner_origin,
+ /*name=*/base::NumberToString(ig_i))
+ .SetBiddingUrl(bidding_url)
+ .SetTrustedBiddingSignalsUrl(bidding_signals_url)
+ .SetTrustedBiddingSignalsKeys({{"key1"}})
+ .SetAds(ads)
+ .Build(),
+ owner_origin.GetURL());
+ }
+ }
+
+ // Sleeping here helps ensure all interest groups are joined before the
+ // auction starts. If we're profiling these auctions with perf, we can also
+ // set a matching delay there to skip profiling the set up of the test and
+ // auction.
+ base::PlatformThread::Sleep(base::Seconds(kPreauctionLogicDelay));
+
+ // Set up the auction config.
+ auto scoring_url = https_server_->GetURL("a.test", kScoringLogicRelativeURL);
+ auto scoring_signals_url =
+ https_server_->GetURL("a.test", kScoringSignalsRelativeURL);
+ // The resulting config will combine `config_template`, repeated `component`s,
+ // and `config_template_end`.
+ std::string config_template = R"({
+ seller: $1,
+ decisionLogicURL: $2,
+ componentAuctions: [)";
+ std::string component = R"({
+ seller: $1,
+ decisionLogicURL: $2,
+ trustedScoringSignalsURL: $3,
+ interestGroupBuyers: $4,
+ auctionSignals: {x: 1},
+ sellerSignals: {yet: 'more', info: 1},
+ sellerTimeout: 200,
+ perBuyerSignals: {$1: {even: 'more', x: 4.5}},
+ perBuyerTimeouts: {$1: 100, '*': 150}
+ })";
+ std::string config_template_end = R"(]
+ })";
+ for (size_t i = 0; i < kSellers; ++i) {
+ std::string end = i < kSellers - 1 ? "," : config_template_end;
+ config_template = base::StrCat({config_template, component, end});
+ }
+ std::string auction_config =
+ JsReplace(config_template, url::Origin::Create(test_url), scoring_url,
+ scoring_signals_url, std::move(buyers_for_auction));
+
+ // Run the auction(s).
+ base::HistogramTester histogram_tester;
+ if (kAuctionsAreParallel) {
+ RunParallelAuctionsAndWait(auction_config, kAuctions);
+ } else {
+ RunAuctionsAndWait(auction_config, kAuctions);
+ }
+
+ // Verify and save results.
+ histogram_tester.ExpectTotalCount("Ads.InterestGroup.Auction.LoadGroupsTime",
+ kAuctions * (kSellers + 1));
+ histogram_tester.ExpectBucketCount(
+ "Ads.InterestGroup.Auction.NumInterestGroups",
+ kInterestGroupsPerOwner * kOwners * kSellers, kAuctions);
+ histogram_tester.ExpectTotalCount("Ads.InterestGroup.Auction.Result",
+ kAuctions);
+ histogram_tester.ExpectTotalCount(
+ "Ads.InterestGroup.Auction.AuctionWithWinnerTime", kAuctions);
+
+ content::FetchHistogramsFromChildProcesses();
+ std::ofstream histogram_file(filename_to_save_histogram);
+ histogram_file << histogram_tester.GetAllHistogramsRecorded();
+ histogram_file.close();
+ ASSERT_TRUE(histogram_file.good());
+}
+
+} // namespace
+
+// Creating our own ContentTestSuiteBase and TestLauncherDelegate allows us to
+// specify our own main function while still running the test above.
+class AdjustableAuctionTestSuite : public ContentTestSuiteBase {
+ public:
+ AdjustableAuctionTestSuite(int argc, char** argv)
+ : ContentTestSuiteBase(argc, argv) {}
+
+ AdjustableAuctionTestSuite(const AdjustableAuctionTestSuite&) = delete;
+ AdjustableAuctionTestSuite& operator=(const AdjustableAuctionTestSuite&) =
+ delete;
+
+ ~AdjustableAuctionTestSuite() override = default;
+
+ protected:
+ void Initialize() override {
+ // Browser tests are expected not to tear-down various globals.
+ base::TestSuite::DisableCheckForLeakedGlobals();
+
+ ContentTestSuiteBase::Initialize();
+
+#if BUILDFLAG(IS_ANDROID)
+ RegisterInProcessThreads();
+#endif
+ }
+};
+
+class AdjustableAuctionTestLauncherDelegate : public TestLauncherDelegate {
+ public:
+ AdjustableAuctionTestLauncherDelegate() = default;
+
+ AdjustableAuctionTestLauncherDelegate(
+ const AdjustableAuctionTestLauncherDelegate&) = delete;
+ AdjustableAuctionTestLauncherDelegate& operator=(
+ const AdjustableAuctionTestLauncherDelegate&) = delete;
+
+ ~AdjustableAuctionTestLauncherDelegate() override = default;
+
+ int RunTestSuite(int argc, char** argv) override {
+ return AdjustableAuctionTestSuite(argc, argv).Run();
+ }
+
+ std::string GetUserDataDirectoryCommandLineSwitch() override {
+ return switches::kContentShellUserDataDir;
+ ;
+ }
+
+ protected:
+#if !BUILDFLAG(IS_ANDROID)
+ ContentMainDelegate* CreateContentMainDelegate() override {
+ return new ContentBrowserTestShellMainDelegate();
+ }
+#endif
+};
+
+} // namespace content
+
+void SetUpSizeTCommandlineArg(std::string switch_name, size_t* output) {
+ if (base::CommandLine::ForCurrentProcess()->HasSwitch(switch_name)) {
+ std::string param =
+ base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
+ switch_name);
+ base::StringToSizeT(param, output);
+ }
+}
+
+void SetUpCommandLineArgs() {
+ // Filename to save UMA histograms from the auction.
+ if (base::CommandLine::ForCurrentProcess()->HasSwitch("hist-filename")) {
+ content::AdjustableAuction::filename_to_save_histogram =
+ base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
+ "hist-filename");
+ }
+ // Whether to run auctions in parallel (or series) if more than one auction
+ // is specified.
+ if (base::CommandLine::ForCurrentProcess()->HasSwitch("auctions-parallel")) {
+ content::AdjustableAuction::kAuctionsAreParallel = true;
+ }
+
+ // The number of owners in the auction.
+ SetUpSizeTCommandlineArg("owners", &content::AdjustableAuction::kOwners);
+ // The number of ads per interest group.
+ SetUpSizeTCommandlineArg("ads-per-ig",
+ &content::AdjustableAuction::kAdsPerInterestGroup);
+ // The number of interest groups per owner.
+ SetUpSizeTCommandlineArg(
+ "ig-per-owner", &content::AdjustableAuction::kInterestGroupsPerOwner);
+ // The number of sellers participating in the auction.
+ SetUpSizeTCommandlineArg("sellers", &content::AdjustableAuction::kSellers);
+ // The number of top level auctions to run.
+ SetUpSizeTCommandlineArg("n-auctions",
+ &content::AdjustableAuction::kAuctions);
+
+ // The number of seconds to wait between joining interest groups and starting
+ // the auction. This delay is necessary to ensure that the database is not
+ // still busy joining interest groups when the auction starts.
+ SetUpSizeTCommandlineArg("preauction-delay",
+ &content::AdjustableAuction::kPreauctionLogicDelay);
+ // The number of additional operations to run in the bidding script to
+ // simulate various bidding script durations.
+ SetUpSizeTCommandlineArg("bidding-delay",
+ &content::AdjustableAuction::kBiddingLogicDelay);
+ // The number of additional operations to run in the scoring script to
+ // simulate various scoring script durations.
+ SetUpSizeTCommandlineArg("scoring-delay",
+ &content::AdjustableAuction::kScoringLogicDelay);
+ // A simulated "network" delay in downloading a bidding script, measured in
+ // milliseconds.
+ SetUpSizeTCommandlineArg("bidding-network-delay",
+ &content::AdjustableAuction::kBiddingNetworkDelay);
+ // A simulated "network" delay in downloading a scoring script, measured in
+ // milliseconds.
+ SetUpSizeTCommandlineArg("scoring-network-delay",
+ &content::AdjustableAuction::kScoringNetworkDelay);
+ // A simulated "network" delay in downloading trusted bidding signals,
+ // measured in milliseconds.
+ SetUpSizeTCommandlineArg(
+ "trusted-bidding-delay",
+ &content::AdjustableAuction::kTrustedBiddingSignalsDelay);
+ // A simulated "network" delay in downloading trusted scoring signals,
+ // measured in milliseconds.
+ SetUpSizeTCommandlineArg(
+ "trusted-scoring-delay",
+ &content::AdjustableAuction::kTrustedScoringSignalsDelay);
+}
+
+int main(int argc, char* argv[]) {
+ base::CommandLine::Init(argc, argv);
+ SetUpCommandLineArgs();
+
+ size_t parallel_jobs = base::NumParallelJobs(/*cores_per_job=*/2);
+ if (parallel_jobs == 0U) {
+ return 1;
+ }
+
+#if BUILDFLAG(IS_WIN)
+ // Load and pin user32.dll to avoid having to load it once tests start while
+ // on the main thread loop where blocking calls are disallowed.
+ base::win::PinUser32();
+#endif // BUILDFLAG(IS_WIN)
+ content::AdjustableAuctionTestLauncherDelegate launcher_delegate;
+ return LaunchTests(&launcher_delegate, parallel_jobs, argc, argv);
+}
diff --git a/content/browser/interest_group/tools/adjustable_auction.md b/content/browser/interest_group/tools/adjustable_auction.md
new file mode 100644
index 0000000..5a59d8f0
--- /dev/null
+++ b/content/browser/interest_group/tools/adjustable_auction.md
@@ -0,0 +1,28 @@
+[adjustable_auction.cc] is a browsertest-based tool for running simulated
+auctions with custom command line parameters. The tool is useful for
+profiling hypothetical scenarios locally.
+
+To use the tool:
+1. Ensure your gn build configuration has `is_debug = false` so that you
+produce a release build.
+1. Build with `autoninja -C out/{target} adjustable_auction`.
+1. Choose which parameters to use when configuring the auction. Most parameters
+ control aspects of the auctions (e.g. `--n-auctions`,
+`--ads-per-ig`). Others are important to allow us to collect accurate
+latency information. `--hist-filename` is the filename to save UMA
+histograms from the auction. Set a large enough `--preauction-delay` to
+ensure all interest groups have already been joined before starting an
+auction. See `SetUpCommandLineArgs()` in [adjustable_auction.cc] for a full
+list of parameters and their descriptions.
+1. If your auction is likely to take a long time, you may want to increase
+the test timeouts with `--ui-test-action-max-timeout=1000000` and
+`--test-launcher-timeout=1000000`.
+1. This tool is particularly useful when used with the `perf` command line
+tool. A delay can be set with `-D` equal to the delay set with
+`--preauction-delay` to ensure that only the auctions are captured, not any of
+the set up. The following is an example of running perf with the tool:
+```
+perf record -e cpu-clock -F 8000 -D 10000 -o perf_output_file.data -g out/{target}/adjustable_auction ---ozone-platform=headless --ads-per-ig=30 --n-auctions=1 --owners=2 --ig-per-owner=100 --scoring-delay=0 --preauction-delay=10 --bidding-delay=10 --hist-filename=output_histograms.txt
+```
+
+[adjustable_auction.cc]: https://source.chromium.org/chromium/chromium/src/+/main:content/browser/interest_group/tools/adjustable_auction.cc