diff --git a/DEPS b/DEPS
index 4469de0..c89fecb 100644
--- a/DEPS
+++ b/DEPS
@@ -300,7 +300,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'src_internal_revision': 'f964ce98d3a6b672509d3e584959338d0c8c9827',
+  'src_internal_revision': 'caa0380f0b26c677fedd3a3829236c0d8414c8ae',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
@@ -308,11 +308,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'v8_revision': '208e64a3826d88df2c2c21a0ccbd501e4c39408c',
+  'v8_revision': '5df25f061a57285d5ba77884b65b1b4ba0835759',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
-  'angle_revision': 'f1e1987261215891dd310f886544c32e1d5dbbcf',
+  'angle_revision': '1572f609c18e12eef702983c58b563f58acad1c3',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -391,7 +391,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling devtools-frontend
   # and whatever else without interference from each other.
-  'devtools_frontend_revision': 'e653e7f03c2faa8eebf2cfdd4c5c82e1730612c2',
+  'devtools_frontend_revision': 'cf6706a3cb404a78a7bdec9900bb106ef7faf731',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libprotobuf-mutator
   # and whatever else without interference from each other.
@@ -431,7 +431,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'dawn_revision': '261e510cd3298f69b23c65a0d2288d4ba03647bf',
+  'dawn_revision': 'bfe346b8723e37a23de1f4b199bfaeff156ae70a',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -1249,7 +1249,7 @@
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
 
   'src/third_party/devtools-frontend-internal': {
-      'url': Var('chrome_git') + '/devtools/devtools-internal.git' + '@' + 'd81bfc61bbfd36996528309d51baa321448ea964',
+      'url': Var('chrome_git') + '/devtools/devtools-internal.git' + '@' + '4dd947a96a4953f3fc2c2da2ca0b4181676e48c8',
     'condition': 'checkout_src_internal',
   },
 
@@ -1705,7 +1705,7 @@
     Var('chromium_git') + '/external/github.com/cisco/openh264' + '@' + '09a4f3ec842a8932341b195c5b01e141c8a16eb7',
 
   'src/third_party/openscreen/src':
-    Var('chromium_git') + '/openscreen' + '@' + '15348b85c11a6ca20a6c93ac5bad4018e2ebb1ca',
+    Var('chromium_git') + '/openscreen' + '@' + '03ab64e47e14f7b4c8ac50e651b2ef1cbb1f9b92',
 
   'src/third_party/openxr/src': {
     'url': Var('chromium_git') + '/external/github.com/KhronosGroup/OpenXR-SDK' + '@' + '58a00cf85c39ad5ec4dc43a769624e420c06179a',
@@ -1716,7 +1716,7 @@
     Var('pdfium_git') + '/pdfium.git' + '@' +  Var('pdfium_revision'),
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' + 'a124832ae9ce7a717e68dc569ee99dc151046258',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + '67cf047d357b44d870ac51a74a578ce462bac047',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + '6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3',
@@ -2015,7 +2015,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/eche_app/app',
-        'version': 'k7xonW_WNZwSrxN7tuRU321LUBL--Y0vy6PIthiYv88C',
+        'version': 'AGaYoE5HgGxUdavPZXZ_WmzKlTj-huAqcrS67fSFdYwC',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
diff --git a/ash/accessibility/accessibility_controller_impl.cc b/ash/accessibility/accessibility_controller_impl.cc
index 7865cf7..6d83d3f 100644
--- a/ash/accessibility/accessibility_controller_impl.cc
+++ b/ash/accessibility/accessibility_controller_impl.cc
@@ -66,7 +66,6 @@
 #include "components/vector_icons/vector_icons.h"
 #include "media/base/media_switches.h"
 #include "ui/accessibility/accessibility_features.h"
-#include "ui/accessibility/accessibility_switches.h"
 #include "ui/accessibility/aura/aura_window_properties.h"
 #include "ui/aura/window.h"
 #include "ui/base/cursor/cursor_size.h"
diff --git a/ash/capture_mode/camera_video_frame_renderer.cc b/ash/capture_mode/camera_video_frame_renderer.cc
index 7c094934..8f8a63aea 100644
--- a/ash/capture_mode/camera_video_frame_renderer.cc
+++ b/ash/capture_mode/camera_video_frame_renderer.cc
@@ -43,8 +43,7 @@
                            GetContextFactory(),
                            std::move(camera_video_source),
                            capture_format),
-      context_provider_(GetContextFactory()->SharedMainThreadContextProvider()),
-      raster_context_provider_(
+      context_provider_(
           GetContextFactory()->SharedMainThreadRasterContextProvider()),
       should_flip_frames_horizontally_(should_flip_frames_horizontally) {
   host_window_.set_owned_by_parent(false);
@@ -69,10 +68,10 @@
   layer_tree_frame_sink_->BindToClient(this);
 
   const int max_texture_size =
-      raster_context_provider_->ContextCapabilities().max_texture_size;
+      context_provider_->ContextCapabilities().max_texture_size;
   video_resource_updater_ = std::make_unique<media::VideoResourceUpdater>(
-      context_provider_.get(), raster_context_provider_.get(),
-      layer_tree_frame_sink_.get(), &client_resource_provider_,
+      context_provider_.get(), layer_tree_frame_sink_.get(),
+      &client_resource_provider_,
       /*use_stream_video_draw_quad=*/false,
       /*use_gpu_memory_buffer_resources=*/false,
       /*use_r16_texture=*/false, max_texture_size);
@@ -288,7 +287,7 @@
 
   std::vector<viz::TransferableResource> resource_list;
   client_resource_provider_.PrepareSendToParent(resource_ids, &resource_list,
-                                                raster_context_provider_.get());
+                                                context_provider_.get());
   compositor_frame.resource_list = std::move(resource_list);
 
   return compositor_frame;
diff --git a/ash/capture_mode/camera_video_frame_renderer.h b/ash/capture_mode/camera_video_frame_renderer.h
index 47a2de5b..a4a4743 100644
--- a/ash/capture_mode/camera_video_frame_renderer.h
+++ b/ash/capture_mode/camera_video_frame_renderer.h
@@ -26,7 +26,6 @@
 }  // namespace media
 
 namespace viz {
-class ContextProvider;
 class RasterContextProvider;
 }  // namespace viz
 
@@ -114,8 +113,7 @@
   gfx::Size last_compositor_frame_size_pixels_;
   float last_compositor_frame_dsf_ = 1.0f;
 
-  scoped_refptr<viz::ContextProvider> context_provider_;
-  scoped_refptr<viz::RasterContextProvider> raster_context_provider_;
+  scoped_refptr<viz::RasterContextProvider> context_provider_;
 
   // The layer tree frame sink created from `host_window_`, which is used to
   // submit compositor frames for the camera video frames.
diff --git a/ash/constants/ash_features.cc b/ash/constants/ash_features.cc
index 53c77b42..8a104a04 100644
--- a/ash/constants/ash_features.cc
+++ b/ash/constants/ash_features.cc
@@ -245,6 +245,11 @@
              "CrosBatterySaver",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+// Make Battery Saver on all the time, even when charged or charging.
+BASE_FEATURE(kBatterySaverAlwaysOn,
+             "CrosBatterySaverAlwaysOn",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 // Enables or disables the usage of fixed Bluetooth A2DP packet size to improve
 // audio performance in noisy environment.
 BASE_FEATURE(kBluetoothFixA2dpPacketSize,
@@ -417,7 +422,7 @@
 // Enables or disables Crostini IME support.
 BASE_FEATURE(kCrostiniImeSupport,
              "CrostiniImeSupport",
-             base::FEATURE_DISABLED_BY_DEFAULT);
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 // Enables or disables Crostini Qt application IME support.
 BASE_FEATURE(kCrostiniQtImeSupport,
@@ -2672,6 +2677,10 @@
   return base::FeatureList::IsEnabled(kBatterySaver);
 }
 
+bool IsBatterySaverAlwaysOn() {
+  return base::FeatureList::IsEnabled(kBatterySaverAlwaysOn);
+}
+
 bool IsBluetoothQualityReportEnabled() {
   return base::FeatureList::IsEnabled(kBluetoothQualityReport);
 }
diff --git a/ash/constants/ash_features.h b/ash/constants/ash_features.h
index 74cec21..35bf3cb 100644
--- a/ash/constants/ash_features.h
+++ b/ash/constants/ash_features.h
@@ -74,6 +74,7 @@
 BASE_DECLARE_FEATURE(kAutozoomNudgeSessionReset);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kAvatarsCloudMigration);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kBatterySaver);
+COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kBatterySaverAlwaysOn);
 COMPONENT_EXPORT(ASH_CONSTANTS)
 BASE_DECLARE_FEATURE(kBluetoothFixA2dpPacketSize);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kBluetoothQualityReport);
@@ -744,6 +745,7 @@
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsAudioHFPNbsWarningEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBackgroundBlurEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBatterySaverAvailable();
+COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBatterySaverAlwaysOn();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBluetoothQualityReportEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsCalendarJellyEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsCaptivePortalErrorPageEnabled();
diff --git a/ash/events/event_rewriter_controller_impl.cc b/ash/events/event_rewriter_controller_impl.cc
index c9188c9..a8f8db2 100644
--- a/ash/events/event_rewriter_controller_impl.cc
+++ b/ash/events/event_rewriter_controller_impl.cc
@@ -15,7 +15,6 @@
 #include "ash/public/cpp/accessibility_event_rewriter_delegate.h"
 #include "ash/shell.h"
 #include "base/command_line.h"
-#include "ui/accessibility/accessibility_switches.h"
 #include "ui/aura/env.h"
 #include "ui/aura/window_tree_host.h"
 #include "ui/events/event_sink.h"
diff --git a/ash/shelf/login_shelf_view_unittest.cc b/ash/shelf/login_shelf_view_unittest.cc
index b31e804..f774c5f4 100644
--- a/ash/shelf/login_shelf_view_unittest.cc
+++ b/ash/shelf/login_shelf_view_unittest.cc
@@ -1035,15 +1035,6 @@
 const char kShelfShutdownConfirmationActionHistogramName[] =
     "Ash.Shelf.ShutdownConfirmationBubble.Action";
 
-const char kCancelActionDurationHistogramName[] =
-    "Ash.Shelf.ShutdownConfirmationBubble.ActionDuration.Cancel";
-
-const char kConfirmActionDurationHistogramName[] =
-    "Ash.Shelf.ShutdownConfirmationBubble.ActionDuration.Confirm";
-
-const char kDismissActionDurationHistogramName[] =
-    "Ash.Shelf.ShutdownConfirmationBubble.ActionDuration.Dismiss";
-
 }  // namespace
 
 class LoginShelfViewWithShutdownConfirmationTest : public LoginShelfViewTest {
@@ -1136,9 +1127,6 @@
       ShelfShutdownConfirmationBubble::BubbleAction::kOpened, 1);
   histograms().ExpectTotalCount(kShelfShutdownConfirmationActionHistogramName,
                                 1);
-  histograms().ExpectTotalCount(kCancelActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kDismissActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kConfirmActionDurationHistogramName, 0);
 }
 
 // Checks that shutdown confirmation bubble appears after pressing the
@@ -1161,9 +1149,6 @@
       ShelfShutdownConfirmationBubble::BubbleAction::kOpened, 1);
   histograms().ExpectTotalCount(kShelfShutdownConfirmationActionHistogramName,
                                 1);
-  histograms().ExpectTotalCount(kCancelActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kDismissActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kConfirmActionDurationHistogramName, 0);
 }
 
 // Checks that shutdown confirmation bubble disappears after pressing the
@@ -1194,9 +1179,6 @@
       ShelfShutdownConfirmationBubble::BubbleAction::kCancelled, 1);
   histograms().ExpectTotalCount(kShelfShutdownConfirmationActionHistogramName,
                                 2);
-  histograms().ExpectTotalCount(kCancelActionDurationHistogramName, 1);
-  histograms().ExpectTotalCount(kDismissActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kConfirmActionDurationHistogramName, 0);
 
   // Shutdown confirmation could be shown again.
   Click(LoginShelfView::kShutdown);
@@ -1207,9 +1189,6 @@
       ShelfShutdownConfirmationBubble::BubbleAction::kOpened, 2);
   histograms().ExpectTotalCount(kShelfShutdownConfirmationActionHistogramName,
                                 3);
-  histograms().ExpectTotalCount(kCancelActionDurationHistogramName, 1);
-  histograms().ExpectTotalCount(kDismissActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kConfirmActionDurationHistogramName, 0);
 }
 
 // Checks that shutdown confirmation bubble disappears after pressing the
@@ -1241,9 +1220,6 @@
       ShelfShutdownConfirmationBubble::BubbleAction::kConfirmed, 1);
   histograms().ExpectTotalCount(kShelfShutdownConfirmationActionHistogramName,
                                 2);
-  histograms().ExpectTotalCount(kCancelActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kDismissActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kConfirmActionDurationHistogramName, 1);
 }
 
 // Checks that shutdown confirmation bubble disappears after inactive.
@@ -1273,9 +1249,6 @@
       ShelfShutdownConfirmationBubble::BubbleAction::kDismissed, 1);
   histograms().ExpectTotalCount(kShelfShutdownConfirmationActionHistogramName,
                                 2);
-  histograms().ExpectTotalCount(kCancelActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kDismissActionDurationHistogramName, 1);
-  histograms().ExpectTotalCount(kConfirmActionDurationHistogramName, 0);
 }
 
 // Checks that shutdown confirmation was first cancelled, then confirmed
@@ -1305,9 +1278,6 @@
       ShelfShutdownConfirmationBubble::BubbleAction::kCancelled, 1);
   histograms().ExpectTotalCount(kShelfShutdownConfirmationActionHistogramName,
                                 2);
-  histograms().ExpectTotalCount(kCancelActionDurationHistogramName, 1);
-  histograms().ExpectTotalCount(kDismissActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kConfirmActionDurationHistogramName, 0);
 
   // Shutdown confirmation could be shown again.
   Click(LoginShelfView::kShutdown);
@@ -1318,9 +1288,6 @@
       ShelfShutdownConfirmationBubble::BubbleAction::kOpened, 2);
   histograms().ExpectTotalCount(kShelfShutdownConfirmationActionHistogramName,
                                 3);
-  histograms().ExpectTotalCount(kCancelActionDurationHistogramName, 1);
-  histograms().ExpectTotalCount(kDismissActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kConfirmActionDurationHistogramName, 0);
 
   // Shutdown confirmation is confirmed and disappeared.
   ConfirmShutdown();
@@ -1331,9 +1298,6 @@
       ShelfShutdownConfirmationBubble::BubbleAction::kConfirmed, 1);
   histograms().ExpectTotalCount(kShelfShutdownConfirmationActionHistogramName,
                                 4);
-  histograms().ExpectTotalCount(kCancelActionDurationHistogramName, 1);
-  histograms().ExpectTotalCount(kDismissActionDurationHistogramName, 0);
-  histograms().ExpectTotalCount(kConfirmActionDurationHistogramName, 1);
 }
 
 // When display is on Shutdown button clicks should not be blocked.
diff --git a/ash/shelf/shelf_shutdown_confirmation_bubble.cc b/ash/shelf/shelf_shutdown_confirmation_bubble.cc
index f8530e17..30a358e 100644
--- a/ash/shelf/shelf_shutdown_confirmation_bubble.cc
+++ b/ash/shelf/shelf_shutdown_confirmation_bubble.cc
@@ -15,9 +15,6 @@
 #include "base/functional/callback_forward.h"
 #include "base/functional/callback_helpers.h"
 #include "base/metrics/histogram_functions.h"
-#include "base/notreached.h"
-#include "base/strings/strcat.h"
-#include "base/time/time.h"
 #include "chromeos/constants/chromeos_features.h"
 #include "components/strings/grit/components_strings.h"
 #include "components/vector_icons/vector_icons.h"
@@ -54,29 +51,6 @@
 constexpr char kActionHistogramName[] =
     "Ash.Shelf.ShutdownConfirmationBubble.Action";
 
-// Histogram for tracking the time delta between bubble opened and actions taken
-// on the shelf shutdown confirmation bubble.
-constexpr char kActionDurationHistogramPrefix[] =
-    "Ash.Shelf.ShutdownConfirmationBubble.ActionDuration.";
-
-// Suffix for shutdown confirmation action. Should match suffixes of the
-// Ash.Shelf.ShutdownConfirmationBubble.ActionDuration.* metrics in
-// metadata/ash/histograms.xml
-std::string BubbleActionSuffix(
-    ShelfShutdownConfirmationBubble::BubbleAction action) {
-  switch (action) {
-    case ShelfShutdownConfirmationBubble::BubbleAction::kCancelled:
-      return "Cancel";
-    case ShelfShutdownConfirmationBubble::BubbleAction::kConfirmed:
-      return "Confirm";
-    case ShelfShutdownConfirmationBubble::BubbleAction::kDismissed:
-      return "Dismiss";
-    case ShelfShutdownConfirmationBubble::BubbleAction::kOpened:
-      NOTREACHED();
-      return "";
-  }
-}
-
 }  // namespace
 
 ShelfShutdownConfirmationBubble::ShelfShutdownConfirmationBubble(
@@ -84,8 +58,7 @@
     ShelfAlignment alignment,
     base::OnceClosure on_confirm_callback,
     base::OnceClosure on_cancel_callback)
-    : ShelfBubble(anchor, alignment),
-      bubble_opened_timestamp_(base::TimeTicks::Now()) {
+    : ShelfBubble(anchor, alignment) {
   DCHECK(on_confirm_callback);
   DCHECK(on_cancel_callback);
   confirm_callback_ = std::move(on_confirm_callback);
@@ -258,12 +231,6 @@
 void ShelfShutdownConfirmationBubble::ReportBubbleAction(
     ShelfShutdownConfirmationBubble::BubbleAction action) {
   base::UmaHistogramEnumeration(kActionHistogramName, action);
-
-  const std::string action_suffix = BubbleActionSuffix(action);
-  auto elapsed_time = base::TimeTicks::Now() - bubble_opened_timestamp_;
-  base::UmaHistogramMediumTimes(
-      base::StrCat({kActionDurationHistogramPrefix, action_suffix}),
-      elapsed_time);
 }
 
 }  // namespace ash
diff --git a/ash/shelf/shelf_shutdown_confirmation_bubble.h b/ash/shelf/shelf_shutdown_confirmation_bubble.h
index 6387c297..9477e08e 100644
--- a/ash/shelf/shelf_shutdown_confirmation_bubble.h
+++ b/ash/shelf/shelf_shutdown_confirmation_bubble.h
@@ -10,7 +10,6 @@
 #include "ash/shelf/shelf_bubble.h"
 #include "base/functional/callback_forward.h"
 #include "base/memory/raw_ptr.h"
-#include "base/time/time.h"
 #include "ui/gfx/geometry/size.h"
 #include "ui/views/controls/button/label_button.h"
 #include "ui/views/controls/image_view.h"
@@ -84,9 +83,6 @@
 
   // A simple state machine to keep track of the dialog result.
   DialogResult dialog_result_{DialogResult::kNone};
-
-  // Track time delta between bubble opened to an action taken
-  base::TimeTicks bubble_opened_timestamp_;
 };
 
 }  // namespace ash
diff --git a/ash/shell.cc b/ash/shell.cc
index 3d130c6..6434cbdc 100644
--- a/ash/shell.cc
+++ b/ash/shell.cc
@@ -860,7 +860,7 @@
 
   // As clients of `capture_mode_controller_`, `projector_controller_` and
   // `game_dashboard_controller_` need to be destroyed before
-  // `capture_mode_controller_`
+  // `capture_mode_controller_`.
   projector_controller_.reset();
   game_dashboard_controller_.reset();
 
@@ -1310,10 +1310,6 @@
   env_filter_ = std::make_unique<::wm::CompoundEventFilter>();
   AddPreTargetHandler(env_filter_.get());
 
-  if (features::IsSnapGroupEnabled()) {
-    snap_group_controller_ = std::make_unique<SnapGroupController>();
-  }
-
   // FocusController takes ownership of AshFocusRules.
   focus_rules_ = new AshFocusRules();
   focus_controller_ = std::make_unique<::wm::FocusController>(focus_rules_);
@@ -1321,6 +1317,12 @@
 
   overview_controller_ = std::make_unique<OverviewController>();
 
+  // `SnapGroupController` has dependencies on `OverviweController` and
+  // `TabletModeController`.
+  if (features::IsSnapGroupEnabled()) {
+    snap_group_controller_ = std::make_unique<SnapGroupController>();
+  }
+
   screen_position_controller_ = std::make_unique<ScreenPositionController>();
 
   frame_throttling_controller_ = std::make_unique<FrameThrottlingController>(
diff --git a/ash/system/power/battery_saver_controller.cc b/ash/system/power/battery_saver_controller.cc
index 990e4bd..226436b 100644
--- a/ash/system/power/battery_saver_controller.cc
+++ b/ash/system/power/battery_saver_controller.cc
@@ -4,6 +4,7 @@
 
 #include "ash/system/power/battery_saver_controller.h"
 
+#include "ash/constants/ash_features.h"
 #include "ash/constants/ash_pref_names.h"
 #include "base/logging.h"
 #include "chromeos/dbus/power/power_manager_client.h"
@@ -15,7 +16,8 @@
 const double BatterySaverController::kActivationChargePercent = 20.0;
 
 BatterySaverController::BatterySaverController(PrefService* local_state)
-    : local_state_(local_state) {
+    : local_state_(local_state),
+      always_on_(features::IsBatterySaverAlwaysOn()) {
   power_status_observation_.Observe(PowerStatus::Get());
 
   pref_change_registrar_.Init(local_state);
@@ -36,6 +38,11 @@
 }
 
 void BatterySaverController::OnPowerStatusChanged() {
+  if (always_on_) {
+    SetBatterySaverState(true);
+    return;
+  }
+
   auto* power_status = PowerStatus::Get();
   double battery_percent = power_status->GetBatteryPercent();
   bool active = power_status->IsBatterySaverActive();
@@ -52,6 +59,11 @@
 }
 
 void BatterySaverController::OnSettingsPrefChanged() {
+  if (always_on_) {
+    SetBatterySaverState(true);
+    return;
+  }
+
   // OS Settings has changed the pref, tell Power Manager.
   SetBatterySaverState(local_state_->GetBoolean(prefs::kPowerBatterySaver));
 }
diff --git a/ash/system/power/battery_saver_controller.h b/ash/system/power/battery_saver_controller.h
index 393fcbe3..a498c9fe 100644
--- a/ash/system/power/battery_saver_controller.h
+++ b/ash/system/power/battery_saver_controller.h
@@ -52,6 +52,8 @@
 
   PrefChangeRegistrar pref_change_registrar_;
 
+  bool always_on_;
+
   base::WeakPtrFactory<BatterySaverController> weak_ptr_factory_{this};
 };
 
diff --git a/ash/system/privacy_hub/geolocation_privacy_switch_controller.cc b/ash/system/privacy_hub/geolocation_privacy_switch_controller.cc
index 99b3f51..9501adfc 100644
--- a/ash/system/privacy_hub/geolocation_privacy_switch_controller.cc
+++ b/ash/system/privacy_hub/geolocation_privacy_switch_controller.cc
@@ -48,15 +48,15 @@
   // TODO(zauri): Set 0-state
 }
 
-void GeolocationPrivacySwitchController::OnAppStartsUsingGeolocation(
-    const std::u16string& app_name) {
+void GeolocationPrivacySwitchController::TrackGeolocationAttempted(
+    const std::string& app_name) {
   ++usage_per_app_[app_name];
   ++usage_cnt_;
   UpdateNotification();
 }
 
-void GeolocationPrivacySwitchController::OnAppStopsUsingGeolocation(
-    const std::u16string& app_name) {
+void GeolocationPrivacySwitchController::TrackGeolocationRelinquished(
+    const std::string& app_name) {
   --usage_per_app_[app_name];
   --usage_cnt_;
   if (usage_per_app_[app_name] < 0 || usage_cnt_ < 0) {
@@ -74,13 +74,12 @@
   std::vector<std::u16string> apps;
   for (const auto& [name, cnt] : usage_per_app_) {
     if (cnt > 0) {
-      apps.push_back(name);
+      apps.push_back(base::UTF8ToUTF16(name));
       if (apps.size() == max_count) {
         break;
       }
     }
   }
-
   return apps;
 }
 
diff --git a/ash/system/privacy_hub/geolocation_privacy_switch_controller.h b/ash/system/privacy_hub/geolocation_privacy_switch_controller.h
index e5c3cf9..3cd3d740 100644
--- a/ash/system/privacy_hub/geolocation_privacy_switch_controller.h
+++ b/ash/system/privacy_hub/geolocation_privacy_switch_controller.h
@@ -38,8 +38,8 @@
   // using the following methods. They are used to decide whether a notification
   // that an app wants to use geolocation should be used. System usages like
   // time-zones should not use this mechanism as they are permanently active.
-  void OnAppStartsUsingGeolocation(const std::u16string& app_name);
-  void OnAppStopsUsingGeolocation(const std::u16string& app_name);
+  void TrackGeolocationAttempted(const std::string& app_name);
+  void TrackGeolocationRelinquished(const std::string& app_name);
 
   // Returns the names of the apps that want to actively use geolocation (if
   // there is more than `max_count` of such apps, first max_count names are
@@ -55,7 +55,7 @@
 
   std::unique_ptr<PrefChangeRegistrar> pref_change_registrar_;
   int usage_cnt_{};
-  std::map<std::u16string, int> usage_per_app_;
+  std::map<std::string, int> usage_per_app_;
 };
 
 }  // namespace ash
diff --git a/ash/system/privacy_hub/geolocation_privacy_switch_controller_unittest.cc b/ash/system/privacy_hub/geolocation_privacy_switch_controller_unittest.cc
index b150b14..f64a8657 100644
--- a/ash/system/privacy_hub/geolocation_privacy_switch_controller_unittest.cc
+++ b/ash/system/privacy_hub/geolocation_privacy_switch_controller_unittest.cc
@@ -77,47 +77,48 @@
 };
 
 TEST_F(PrivacyHubGeolocationControllerTest, GetActiveAppsTest) {
-  const std::vector<std::u16string> app_names{u"App1", u"App2", u"App3"};
+  const std::vector<std::string> app_names{"App1", "App2", "App3"};
+  const std::vector<std::u16string> app_names_u16{u"App1", u"App2", u"App3"};
   EXPECT_EQ(controller_->GetActiveApps(3), (std::vector<std::u16string>{}));
-  controller_->OnAppStartsUsingGeolocation(app_names[0]);
+  controller_->TrackGeolocationAttempted(app_names[0]);
   EXPECT_EQ(controller_->GetActiveApps(3),
-            (std::vector<std::u16string>{app_names[0]}));
-  controller_->OnAppStartsUsingGeolocation(app_names[1]);
+            (std::vector<std::u16string>{app_names_u16[0]}));
+  controller_->TrackGeolocationAttempted(app_names[1]);
   EXPECT_EQ(controller_->GetActiveApps(3),
-            (std::vector<std::u16string>{app_names[0], app_names[1]}));
-  controller_->OnAppStartsUsingGeolocation(app_names[1]);
+            (std::vector<std::u16string>{app_names_u16[0], app_names_u16[1]}));
+  controller_->TrackGeolocationAttempted(app_names[1]);
   EXPECT_EQ(controller_->GetActiveApps(3),
-            (std::vector<std::u16string>{app_names[0], app_names[1]}));
-  controller_->OnAppStartsUsingGeolocation(app_names[2]);
-  EXPECT_EQ(controller_->GetActiveApps(3), app_names);
-  controller_->OnAppStopsUsingGeolocation(app_names[2]);
+            (std::vector<std::u16string>{app_names_u16[0], app_names_u16[1]}));
+  controller_->TrackGeolocationAttempted(app_names[2]);
+  EXPECT_EQ(controller_->GetActiveApps(3), app_names_u16);
+  controller_->TrackGeolocationRelinquished(app_names[2]);
   EXPECT_EQ(controller_->GetActiveApps(3),
-            (std::vector<std::u16string>{app_names[0], app_names[1]}));
-  controller_->OnAppStopsUsingGeolocation(app_names[1]);
+            (std::vector<std::u16string>{app_names_u16[0], app_names_u16[1]}));
+  controller_->TrackGeolocationRelinquished(app_names[1]);
   EXPECT_EQ(controller_->GetActiveApps(3),
-            (std::vector<std::u16string>{app_names[0], app_names[1]}));
-  controller_->OnAppStopsUsingGeolocation(app_names[1]);
+            (std::vector<std::u16string>{app_names_u16[0], app_names_u16[1]}));
+  controller_->TrackGeolocationRelinquished(app_names[1]);
   EXPECT_EQ(controller_->GetActiveApps(3),
-            (std::vector<std::u16string>{app_names[0]}));
-  controller_->OnAppStopsUsingGeolocation(app_names[0]);
+            (std::vector<std::u16string>{app_names_u16[0]}));
+  controller_->TrackGeolocationRelinquished(app_names[0]);
   EXPECT_EQ(controller_->GetActiveApps(3), (std::vector<std::u16string>{}));
 }
 
 TEST_F(PrivacyHubGeolocationControllerTest, NotificationOnActivityChangeTest) {
-  const std::u16string app_name = u"app";
+  const std::string app_name = "app";
   SetUserPref(false);
   EXPECT_FALSE(FindNotification());
-  controller_->OnAppStartsUsingGeolocation(app_name);
+  controller_->TrackGeolocationAttempted(app_name);
   EXPECT_TRUE(FindNotification());
-  controller_->OnAppStopsUsingGeolocation(app_name);
+  controller_->TrackGeolocationRelinquished(app_name);
   EXPECT_FALSE(FindNotification());
 }
 
 TEST_F(PrivacyHubGeolocationControllerTest,
        NotificationOnPreferenceChangeTest) {
-  const std::u16string app_name = u"app";
+  const std::string app_name = "app";
   SetUserPref(true);
-  controller_->OnAppStartsUsingGeolocation(app_name);
+  controller_->TrackGeolocationAttempted(app_name);
   EXPECT_FALSE(FindNotification());
   SetUserPref(false);
   EXPECT_TRUE(FindNotification());
@@ -126,9 +127,9 @@
 }
 
 TEST_F(PrivacyHubGeolocationControllerTest, ClickOnNotificationTest) {
-  const std::u16string app_name = u"app";
+  const std::string app_name = "app";
   SetUserPref(false);
-  controller_->OnAppStartsUsingGeolocation(app_name);
+  controller_->TrackGeolocationAttempted(app_name);
   // We didn't log any notification clicks so far.
   EXPECT_EQ(histogram_tester_.GetBucketCount(
                 privacy_hub_metrics::
diff --git a/ash/webui/camera_app_ui/resources/js/models/barcode.ts b/ash/webui/camera_app_ui/resources/js/models/barcode.ts
index 33dd3d10..2827519 100644
--- a/ash/webui/camera_app_ui/resources/js/models/barcode.ts
+++ b/ash/webui/camera_app_ui/resources/js/models/barcode.ts
@@ -52,9 +52,6 @@
     }, SCAN_INTERVAL);
   }
 
-  /**
-   * Stops scanning barcodes.
-   */
   stop(): void {
     if (this.scanRunner === null) {
       return;
diff --git a/ash/webui/camera_app_ui/resources/js/models/file_namer.ts b/ash/webui/camera_app_ui/resources/js/models/file_namer.ts
index c7815917..afae0cf 100644
--- a/ash/webui/camera_app_ui/resources/js/models/file_namer.ts
+++ b/ash/webui/camera_app_ui/resources/js/models/file_namer.ts
@@ -8,14 +8,8 @@
   VideoType,
 } from '../type.js';
 
-/**
- * The prefix of image files.
- */
 export const IMAGE_PREFIX = 'IMG_';
 
-/**
- * The prefix of video files.
- */
 export const VIDEO_PREFIX = 'VID_';
 
 /**
@@ -34,10 +28,7 @@
 const BURST_COVER_SUFFIX = '_COVER';
 
 /**
- * Transforms from capture timestamp to datetime name.
- *
- * @param timestamp Timestamp to be transformed.
- * @return Transformed datetime name.
+ * Transforms from capture timestamp to datetime name as YYYYMMDD_HHMMSS.
  */
 function timestampToDatetimeName(timestamp: number): string {
   function pad(n: number) {
@@ -85,7 +76,8 @@
   private burstCount = 0;
 
   /**
-   * @param timestamp Timestamp of camera session.
+   * @param timestamp Optional timestamp of camera session. If |timestamp| is
+   *     not given, it will use current time.
    */
   constructor(timestamp?: number) {
     this.timestamp = timestamp ?? Date.now();
@@ -95,7 +87,6 @@
    * Creates new filename for burst image.
    *
    * @param isCover If the image is set as cover of the burst.
-   * @return New filename.
    */
   newBurstName(isCover: boolean): string {
     function prependZeros(n: number, width: number) {
@@ -108,8 +99,6 @@
 
   /**
    * Creates new filename for video.
-   *
-   * @return New filename.
    */
   newVideoName(videoType: VideoType): string {
     return VIDEO_PREFIX + timestampToDatetimeName(this.timestamp) + '.' +
@@ -118,17 +107,13 @@
 
   /**
    * Creates new filename for image.
-   *
-   * @return New filename.
    */
   newImageName(): string {
     return IMAGE_PREFIX + timestampToDatetimeName(this.timestamp) + '.jpg';
   }
 
   /**
-   * Creates new filename for pdf.
-   *
-   * @return New filename.
+   * Creates new filename for document.
    */
   newDocumentName(mimeType: MimeType): string {
     const ext = (() => {
@@ -146,20 +131,14 @@
   }
 
   /**
-   * Get the metadata name from image name.
-   *
-   * @param imageName Name of image to derive the metadata name.
-   * @return Metadata name of the image.
+   * Gets the metadata name from given |imageName|.
    */
   static getMetadataName(imageName: string): string {
     return imageName.replace(/\.[^/.]+$/, '.json');
   }
 
   /**
-   * Returns true if the file name matches the format that CCA generates.
-   *
-   * @param fileName Name of the file.
-   * @return True if it matches CCA file naming format.
+   * Returns true if given |fileName| matches the format that CCA generates.
    */
   static isCCAFileFormat(fileName: string): boolean {
     return FILE_NAME_PATTERN.test(fileName);
diff --git a/ash/webui/camera_app_ui/resources/js/models/file_system.ts b/ash/webui/camera_app_ui/resources/js/models/file_system.ts
index ec92870..e7e8224 100644
--- a/ash/webui/camera_app_ui/resources/js/models/file_system.ts
+++ b/ash/webui/camera_app_ui/resources/js/models/file_system.ts
@@ -22,9 +22,6 @@
 
 /**
  * Checks if the entry's name has the video prefix.
- *
- * @param entry File entry.
- * @return Has the video prefix or not.
  */
 export function hasVideoPrefix(entry: FileAccessEntry): boolean {
   return entry.name.startsWith(VIDEO_PREFIX);
@@ -32,9 +29,6 @@
 
 /**
  * Checks if the entry's name has the image prefix.
- *
- * @param entry File entry.
- * @return Has the image prefix or not.
  */
 function hasImagePrefix(entry: FileAccessEntry): boolean {
   return entry.name.startsWith(IMAGE_PREFIX);
@@ -42,9 +36,6 @@
 
 /**
  * Checks if the entry's name has the document prefix.
- *
- * @param entry File entry.
- * @return Has the document prefix or not.
  */
 function hasDocumentPrefix(entry: FileAccessEntry): boolean {
   return entry.name.startsWith(DOCUMENT_PREFIX);
@@ -126,11 +117,11 @@
 }
 
 /**
- * Saves photo blob or metadata blob into predefined default location.
+ * Saves photo blob or metadata blob into predefined default location and
+ * returns the file.
  *
  * @param blob Data of the photo to be saved.
  * @param name Filename of the photo to be saved.
- * @return Promise for the result.
  */
 export async function saveBlob(
     blob: Blob, name: string): Promise<FileAccessEntry> {
@@ -145,7 +136,6 @@
 const PRIVATE_TEMPFILE_NAME = 'video-tmp.mp4';
 
 /**
- * @return Newly created temporary file.
  * @throws If failed to create video temp file.
  */
 export async function createPrivateTempVideoFile(name = PRIVATE_TEMPFILE_NAME):
@@ -165,8 +155,6 @@
 
 /**
  * Gets the picture entries.
- *
- * @return Promise for the picture entries.
  */
 export async function getEntries(): Promise<FileAccessEntry[]> {
   assert(cameraDir !== null);
@@ -182,9 +170,6 @@
 
 /**
  * Returns an URL for a picture given by the file |entry|.
- *
- * @param entry The file entry of the picture.
- * @return Promise for the result.
  */
 export async function pictureURL(entry: FileAccessEntry): Promise<string> {
   const file = await entry.file();
diff --git a/ash/webui/camera_app_ui/resources/js/models/file_system_access_entry.ts b/ash/webui/camera_app_ui/resources/js/models/file_system_access_entry.ts
index d9c87c0..b623c29 100644
--- a/ash/webui/camera_app_ui/resources/js/models/file_system_access_entry.ts
+++ b/ash/webui/camera_app_ui/resources/js/models/file_system_access_entry.ts
@@ -26,7 +26,7 @@
   /**
    * Writes |blob| data into the file.
    *
-   * @return The returned promise is resolved once the write operation is
+   * @return The promise is resolved once the write operation is
    *     completed.
    */
   async write(blob: Blob): Promise<void> {
@@ -35,9 +35,6 @@
     await writer.close();
   }
 
-  /**
-   * Gets a writer to write data into the file.
-   */
   async getWriter(): Promise<AsyncWriter> {
     const writer = await this.handle.createWritable();
     // TODO(crbug.com/980846): We should write files in-place so that even the
@@ -61,8 +58,6 @@
   }
 
   /**
-   * Deletes the file.
-   *
    * @throws Thrown when trying to delete file with no parent directory.
    */
   async remove(): Promise<void> {
@@ -94,32 +89,14 @@
  * The abstract interface for the directory entry.
  */
 export interface DirectoryAccessEntry {
-  /**
-   * Gets the name of the directory.
-   */
   readonly name: string;
 
-  /**
-   * Gets the handle of the directory.
-   */
   getHandle(): Promise<FileSystemDirectoryHandle>;
 
-  /**
-   * Gets files in this directory.
-   */
   getFiles(): Promise<FileAccessEntry[]>;
 
-  /**
-   * Gets directories in this directory.
-   */
   getDirectories(): Promise<DirectoryAccessEntry[]>;
 
-  /**
-   * Gets the file given by its |name|.
-   *
-   * @param name The name of the file.
-   * @return The entry of the found file.
-   */
   getFile(name: string): Promise<FileAccessEntry|null>;
 
   /**
@@ -131,9 +108,6 @@
    * Create the file given by its |name|. If there is already a file with same
    * name, it will try to use a name with index as suffix.
    * (e.g. IMG.png => IMG (1).png).
-   *
-   * @param name The name of the file.
-   * @return The entry of the created file.
    */
   createFile(name: string): Promise<FileAccessEntry>;
 
@@ -142,8 +116,6 @@
    * create one if |createIfNotExist| is true.
    * TODO(crbug.com/1127587): Split this method to getDirectory() and
    * createDirectory().
-   *
-   * @return The entry of the found/created directory.
    */
   getDirectory({name, createIfNotExist}:
                    {name: string, createIfNotExist: boolean}):
@@ -151,8 +123,6 @@
 
   /**
    * Removes file by given |name| from the directory.
-   *
-   * @param name The name of the file.
    */
   removeEntry(name: string): Promise<void>;
 }
diff --git a/ash/webui/camera_app_ui/resources/js/models/idb.ts b/ash/webui/camera_app_ui/resources/js/models/idb.ts
index 4f4f88f..070bf858 100644
--- a/ash/webui/camera_app_ui/resources/js/models/idb.ts
+++ b/ash/webui/camera_app_ui/resources/js/models/idb.ts
@@ -28,7 +28,6 @@
  * Retrieves serializable object from idb.
  *
  * @param key The key of the object.
- * @return The promise of the retrieved object.
  */
 export async function get<T>(key: string): Promise<T|null> {
   const transaction = (await idb).transaction(DB_STORE, 'readonly');
diff --git a/ash/webui/camera_app_ui/resources/js/models/lazy_directory_entry.ts b/ash/webui/camera_app_ui/resources/js/models/lazy_directory_entry.ts
index 036c084..bcba50fc 100644
--- a/ash/webui/camera_app_ui/resources/js/models/lazy_directory_entry.ts
+++ b/ash/webui/camera_app_ui/resources/js/models/lazy_directory_entry.ts
@@ -13,9 +13,6 @@
  * Gets directory entry by given |name| under |parentDir| directory. If the
  * directory does not exist, returns a lazy directory which will only be created
  * once there is any file written in it.
- *
- * @param parentDir Parent directory.
- * @param name Name of the target directory.
  */
 export async function getMaybeLazyDirectory(
     parentDir: DirectoryAccessEntry,
@@ -33,10 +30,6 @@
 class LazyDirectoryEntry implements DirectoryAccessEntry {
   private directory: DirectoryAccessEntry|null = null;
 
-  /**
-   * @param parent The parent of the directory that will be lazily created.
-   * @param name The name of the directory that will be lazily created.
-   */
   constructor(
       private readonly parent: DirectoryAccessEntry, readonly name: string) {}
 
diff --git a/ash/webui/camera_app_ui/resources/js/models/video_saver.ts b/ash/webui/camera_app_ui/resources/js/models/video_saver.ts
index 3dd4f6e..a52cd7b3 100644
--- a/ash/webui/camera_app_ui/resources/js/models/video_saver.ts
+++ b/ash/webui/camera_app_ui/resources/js/models/video_saver.ts
@@ -101,8 +101,6 @@
 
   /**
    * Finishes the write of video data parts and returns result video file.
-   *
-   * @return Result video file.
    */
   async endWrite(): Promise<FileAccessEntry> {
     await this.processor.close();
diff --git a/ash/wm/overview/overview_constants.h b/ash/wm/overview/overview_constants.h
index 9a60d929..600ff51 100644
--- a/ash/wm/overview/overview_constants.h
+++ b/ash/wm/overview/overview_constants.h
@@ -19,9 +19,12 @@
 constexpr base::TimeDelta kWindowRestoreDurationCrOSNext =
     base::Milliseconds(350);
 
-// In the conceptual overview table, the space between two adjacent items
-// horizontally and vertically.
-constexpr int kSpaceBetweenItemsDp = 10;
+// In the conceptual overview table, the horizontal space between two adjacent
+// items.
+constexpr int kHorizontalSpaceBetweenItemsDp = 10;
+
+// The vertical space between two adjacent items.
+constexpr int kVerticalSpaceBetweenItemsDp = 15;
 
 // The amount we want to enlarge the dragged overview window.
 constexpr int kDraggingEnlargeDp = 10;
diff --git a/ash/wm/overview/overview_grid.cc b/ash/wm/overview/overview_grid.cc
index f6259796..dae5b3a 100644
--- a/ash/wm/overview/overview_grid.cc
+++ b/ash/wm/overview/overview_grid.cc
@@ -2276,7 +2276,7 @@
   // |high_height|. Once this optimal height is known, |height_fixed| is set to
   // true and the rows are balanced by repeatedly squeezing the widest row to
   // cause windows to overflow to the subsequent rows.
-  int low_height = kSpaceBetweenItemsDp;
+  int low_height = kVerticalSpaceBetweenItemsDp;
   int high_height = std::max(low_height, total_bounds.height() + 1);
   int height = 0.5 * (low_height + high_height);
   bool height_fixed = false;
@@ -2396,7 +2396,7 @@
   // |window_position| remains where the item was as to then reposition the
   // other window's bounds in place of that item.
   const int height = (total_bounds.height() -
-                      ((kTabletLayoutRow - 1) * kSpaceBetweenItemsDp)) /
+                      ((kTabletLayoutRow - 1) * kVerticalSpaceBetweenItemsDp)) /
                      kTabletLayoutRow;
   int window_position = 0;
   std::vector<gfx::RectF> rects;
@@ -2409,16 +2409,16 @@
 
     // Calculate the width and y position of the item.
     const int width = CalculateWidthAndMaybeSetUnclippedBounds(item, height);
-    const int y =
-        (height + kSpaceBetweenItemsDp) * (window_position % kTabletLayoutRow) +
-        total_bounds.y();
+    const int y = (height + kVerticalSpaceBetweenItemsDp) *
+                      (window_position % kTabletLayoutRow) +
+                  total_bounds.y();
 
     // Use the right bounds of the item next to in the row as the x position, if
     // that item exists.
     const int x = right_edge_map.contains(y)
                       ? right_edge_map[y]
                       : total_bounds.x() + scroll_offset_;
-    right_edge_map[y] = x + width + kSpaceBetweenItemsDp;
+    right_edge_map[y] = x + width + kHorizontalSpaceBetweenItemsDp;
     DCHECK_LE(static_cast<int>(right_edge_map.size()), kTabletLayoutRow);
 
     const gfx::RectF bounds(x, y, width, height);
@@ -2461,18 +2461,19 @@
     int width =
         CalculateWidthAndMaybeSetUnclippedBounds(window_list_[i].get(), height);
 
-    if ((left + width + kSpaceBetweenItemsDp) > bounds.right()) {
+    if ((left + width + kHorizontalSpaceBetweenItemsDp) > bounds.right()) {
       // Move to the next row if possible.
       if (*out_min_right > left)
         *out_min_right = left;
       if (*out_max_right < left)
         *out_max_right = left;
-      top += (height + kSpaceBetweenItemsDp);
+      top += (height + kVerticalSpaceBetweenItemsDp);
 
       // Check if the new row reaches the bottom or if the first item in the new
       // row does not fit within the available width.
-      if ((top + height + kSpaceBetweenItemsDp) > bounds.bottom() ||
-          bounds.x() + width + kSpaceBetweenItemsDp > bounds.right()) {
+      if ((top + height + kVerticalSpaceBetweenItemsDp) > bounds.bottom() ||
+          bounds.x() + width + kHorizontalSpaceBetweenItemsDp >
+              bounds.right()) {
         return false;
       }
       left = bounds.x();
@@ -2482,7 +2483,7 @@
     (*out_rects)[i] = gfx::RectF(left, top, width, height);
 
     // Increment horizontal position using sanitized positive `width`.
-    left += (width + kSpaceBetweenItemsDp);
+    left += (width + kHorizontalSpaceBetweenItemsDp);
 
     *out_max_bottom = top + height;
   }
diff --git a/base/allocator/partition_allocator/partition_alloc.gni b/base/allocator/partition_allocator/partition_alloc.gni
index a344a4fa..e0c141b 100644
--- a/base/allocator/partition_allocator/partition_alloc.gni
+++ b/base/allocator/partition_allocator/partition_alloc.gni
@@ -213,15 +213,16 @@
 # enable_backup_ref_ptr_support is true.
 assert(
     enable_backup_ref_ptr_support || !put_ref_count_in_previous_slot,
-    "Can't put ref count in the previous slot if BackupRefPtr isn't enabled at all")
+    "Can't put ref count in the previous slot if BackupRefPtr isn't enabled " +
+        "at all")
 
-# enable_backup_ref_ptr_slow_checks can only be used if enable_backup_ref_ptr_support
-# is true.
+# enable_backup_ref_ptr_slow_checks can only be used if
+# enable_backup_ref_ptr_support is true.
 assert(enable_backup_ref_ptr_support || !enable_backup_ref_ptr_slow_checks,
        "Can't enable additional BackupRefPtr checks if it isn't enabled at all")
 
-# enable_dangling_raw_ptr_checks can only be used if enable_backup_ref_ptr_support
-# is true.
+# enable_dangling_raw_ptr_checks can only be used if
+# enable_backup_ref_ptr_support is true.
 assert(
     enable_backup_ref_ptr_support || !enable_dangling_raw_ptr_checks,
     "Can't enable dangling raw_ptr checks if BackupRefPtr isn't enabled at all")
@@ -232,18 +233,21 @@
     enable_dangling_raw_ptr_checks || !enable_dangling_raw_ptr_perf_experiment,
     "Missing dangling pointer checks feature for its performance experiment")
 
-# To poison OOB pointers for BackupRefPtr, the underlying feature must
-# be enabled, too.
+# To poison OOB pointers for BackupRefPtr, the underlying feature must be
+# enabled, too.
 assert(
     enable_backup_ref_ptr_support || !backup_ref_ptr_poison_oob_ptr,
-    "Can't enable poisoning for OOB pointers if BackupRefPtr isn't enabled at all")
+    "Can't enable poisoning for OOB pointers if BackupRefPtr isn't enabled " +
+        "at all")
 assert(has_64_bit_pointers || !backup_ref_ptr_poison_oob_ptr,
        "Can't enable poisoning for OOB pointers if pointers are only 32-bit")
 
-# AsanBackupRefPtr and AsanUnownedPtr are mutually exclusive variants of raw_ptr.
+# AsanBackupRefPtr and AsanUnownedPtr are mutually exclusive variants of
+# raw_ptr.
 assert(
     !use_asan_unowned_ptr || !use_asan_backup_ref_ptr,
-    "Both AsanUnownedPtr and AsanBackupRefPtr can't be enabled at the same time")
+    "Both AsanUnownedPtr and AsanBackupRefPtr can't be enabled at the same " +
+        "time")
 
 # BackupRefPtr and AsanBackupRefPtr are mutually exclusive variants of raw_ptr.
 assert(
@@ -254,15 +258,19 @@
 assert(!enable_backup_ref_ptr_support || !use_asan_unowned_ptr,
        "Both BackupRefPtr and AsanUnownedPtr can't be enabled at the same time")
 
-# RawPtrHookableImpl and BackupRefPtr are mutually exclusive variants of raw_ptr.
+# RawPtrHookableImpl and BackupRefPtr are mutually exclusive variants of
+# raw_ptr.
 assert(
     !use_hookable_raw_ptr || !enable_backup_ref_ptr_support,
-    "Both RawPtrHookableImpl and BackupRefPtr can't be enabled at the same time")
+    "Both RawPtrHookableImpl and BackupRefPtr can't be enabled at the same " +
+        "time")
 
-# RawPtrHookableImpl and AsanUnownedPtr are mutually exclusive variants of raw_ptr.
+# RawPtrHookableImpl and AsanUnownedPtr are mutually exclusive variants of
+# raw_ptr.
 assert(
     !use_hookable_raw_ptr || !use_asan_unowned_ptr,
-    "Both RawPtrHookableImpl and AsanUnownedPtr can't be enabled at the same time")
+    "Both RawPtrHookableImpl and AsanUnownedPtr can't be enabled at the same " +
+        "time")
 
 assert(!use_asan_backup_ref_ptr || is_asan,
        "AsanBackupRefPtr requires AddressSanitizer")
@@ -279,8 +287,8 @@
 }
 
 # AsanBackupRefPtr is not supported outside Chromium. The implementation is
-# entangled with `//base`. The code is only physically located with the
-# rest of `raw_ptr` to keep it together.
+# entangled with `//base`. The code is only physically located with the rest of
+# `raw_ptr` to keep it together.
 assert(build_with_chromium || !use_asan_backup_ref_ptr,
        "AsanBackupRefPtr is not supported outside Chromium")
 
@@ -288,9 +296,9 @@
        "AsanBackupRefPtr requires RawPtrHookableImpl")
 
 declare_args() {
-  # pkeys support is explicitly disabled in all Cronet builds, as some test dependencies that
-  # use partition_allocator are compiled in AOSP against a version of glibc that does not
-  # include pkeys syscall numbers.
+  # pkeys support is explicitly disabled in all Cronet builds, as some test
+  # dependencies that use partition_allocator are compiled in AOSP against a
+  # version of glibc that does not include pkeys syscall numbers.
   enable_pkeys = is_linux && target_cpu == "x64" && !is_cronet_build
 }
 assert(!enable_pkeys || (is_linux && target_cpu == "x64"),
diff --git a/build/config/clang/BUILD.gn b/build/config/clang/BUILD.gn
index 6a69428..bdf4303 100644
--- a/build/config/clang/BUILD.gn
+++ b/build/config/clang/BUILD.gn
@@ -83,6 +83,12 @@
         "-plugin-arg-find-bad-constructs",
         "-Xclang",
         "raw-ptr-exclude-path=ui/views/controls/native/native_view_host_mac_unittest.mm",
+
+        # TODO(mikt): Remove this once crbug.com/1449812 is resolved.
+        "-Xclang",
+        "-plugin-arg-find-bad-constructs",
+        "-Xclang",
+        "raw-ptr-exclude-path=um/winnt.h",
       ]
     }
   }
diff --git a/build/config/clang/clang.gni b/build/config/clang/clang.gni
index 474fde4..727a2ec 100644
--- a/build/config/clang/clang.gni
+++ b/build/config/clang/clang.gni
@@ -16,7 +16,8 @@
 
   enable_check_raw_ptr_fields =
       build_with_chromium && !is_official_build &&
-      ((is_linux && !is_castos) || (is_android && !is_cast_android) || is_mac)
+      ((is_linux && !is_castos) || (is_android && !is_cast_android) || is_mac ||
+       is_win)
 
   clang_base_path = default_clang_base_path
 
diff --git a/build/config/siso/configure_siso.py b/build/config/siso/configure_siso.py
index 2770f6e7..bcf79ca 100755
--- a/build/config/siso/configure_siso.py
+++ b/build/config/siso/configure_siso.py
@@ -12,25 +12,26 @@
 
 
 def main():
-  parser = argparse.ArgumentParser(description='configure siso')
-  parser.add_argument('--rbe_instance', help='RBE instance to use for Siso')
+  parser = argparse.ArgumentParser(description="configure siso")
+  parser.add_argument("--rbe_instance", help="RBE instance to use for Siso")
   args = parser.parse_args()
 
   project = None
-  if not args.rbe_instance:
-    return 0
   rbe_instance = args.rbe_instance
-  elems = rbe_instance.split('/')
-  if len(elems) == 4 and elems[0] == 'projects':
-    project = elems[1]
-    rbe_instance = elems[-1]
-  siso_env_path = os.path.join(THIS_DIR, '.sisoenv')
-  with open(siso_env_path, 'w') as f:
+  if rbe_instance:
+    elems = rbe_instance.split("/")
+    if len(elems) == 4 and elems[0] == "projects":
+      project = elems[1]
+      rbe_instance = elems[-1]
+
+  siso_env_path = os.path.join(THIS_DIR, ".sisoenv")
+  with open(siso_env_path, "w") as f:
     if project:
-      f.write('SISO_PROJECT=%s\n' % project)
-    f.write('SISO_REAPI_INSTANCE=%s\n' % rbe_instance)
+      f.write("SISO_PROJECT=%s\n" % project)
+    if rbe_instance:
+      f.write("SISO_REAPI_INSTANCE=%s\n" % rbe_instance)
   return 0
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
   sys.exit(main())
diff --git a/build/config/siso/rewrapper_to_reproxy.star b/build/config/siso/rewrapper_to_reproxy.star
index aec12d1..020ac73 100644
--- a/build/config/siso/rewrapper_to_reproxy.star
+++ b/build/config/siso/rewrapper_to_reproxy.star
@@ -52,7 +52,7 @@
             reproxy_config["exec_timeout"] = line.removeprefix("exec_timeout=")
 
         if line.startswith("inputs="):
-            reproxy_config["inputs"] = line.removeprefix("inputs").split(",")
+            reproxy_config["inputs"] = line.removeprefix("inputs=").split(",")
 
         if line.startswith("labels="):
             if "labels" not in reproxy_config:
diff --git a/build/fuchsia/linux_internal.sdk.sha1 b/build/fuchsia/linux_internal.sdk.sha1
index a532479..a81c0a90 100644
--- a/build/fuchsia/linux_internal.sdk.sha1
+++ b/build/fuchsia/linux_internal.sdk.sha1
@@ -1 +1 @@
-12.20230612.2.1
+13.20230613.0.1
diff --git a/cc/layers/video_layer_impl.cc b/cc/layers/video_layer_impl.cc
index 9fabe7c..2ef7a02 100644
--- a/cc/layers/video_layer_impl.cc
+++ b/cc/layers/video_layer_impl.cc
@@ -110,8 +110,7 @@
   if (!updater_) {
     const LayerTreeSettings& settings = layer_tree_impl()->settings();
     updater_ = std::make_unique<media::VideoResourceUpdater>(
-        /*context_provider=*/nullptr,
-        /*raster_context_provider=*/layer_tree_impl()->context_provider(),
+        layer_tree_impl()->context_provider(),
         layer_tree_impl()->layer_tree_frame_sink(),
         layer_tree_impl()->resource_provider(),
         settings.use_stream_video_draw_quad,
diff --git a/chrome/VERSION b/chrome/VERSION
index 8b7371f..8d7641a 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=116
 MINOR=0
-BUILD=5829
+BUILD=5830
 PATCH=0
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 880f45e..59fc9b5 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -10063,6 +10063,11 @@
          kServiceWorkerBypassFetchHandlerForMainResourceDescription,
      kOsAll, FEATURE_VALUE_TYPE(features::kServiceWorkerBypassFetchHandler)},
 
+    {"service-worker-static-router",
+     flag_descriptions::kServiceWorkerStaticRouterName,
+     flag_descriptions::kServiceWorkerStaticRouterDescription, kOsAll,
+     FEATURE_VALUE_TYPE(features::kServiceWorkerStaticRouter)},
+
     {"autofill-remove-card-expiration-and-type-titles",
      flag_descriptions::kAutofillRemoveCardExpirationAndTypeTitlesName,
      flag_descriptions::kAutofillRemoveCardExpirationAndTypeTitlesDescription,
@@ -10425,6 +10430,13 @@
      kOsDesktop | kOsAndroid,
      FEATURE_VALUE_TYPE(features::kProcessPerSiteUpToMainFrameThreshold)},
 
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+    {"cros-battery-saver-always-on",
+     flag_descriptions::kCrosBatterySaverAlwaysOnName,
+     flag_descriptions::kCrosBatterySaverAlwaysOnDescription, kOsCrOS,
+     FEATURE_VALUE_TYPE(ash::features::kBatterySaverAlwaysOn)},
+#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
+
     // NOTE: Adding a new flag requires adding a corresponding entry to enum
     // "LoginCustomFlags" in tools/metrics/histograms/enums.xml. See "Flag
     // Histograms" in tools/metrics/histograms/README.md (run the
diff --git a/chrome/browser/accessibility/accessibility_extension_api_chromeos.cc b/chrome/browser/accessibility/accessibility_extension_api_chromeos.cc
index 3ff7c70..492c24e6 100644
--- a/chrome/browser/accessibility/accessibility_extension_api_chromeos.cc
+++ b/chrome/browser/accessibility/accessibility_extension_api_chromeos.cc
@@ -46,7 +46,6 @@
 #include "extensions/common/error_utils.h"
 #include "extensions/common/manifest_handlers/background_info.h"
 #include "ui/accessibility/accessibility_features.h"
-#include "ui/accessibility/accessibility_switches.h"
 #include "ui/aura/client/cursor_client.h"
 #include "ui/aura/window_tree_host.h"
 #include "ui/base/ui_base_features.h"
diff --git a/chrome/browser/ash/BUILD.gn b/chrome/browser/ash/BUILD.gn
index 896e541..6c84862aa 100644
--- a/chrome/browser/ash/BUILD.gn
+++ b/chrome/browser/ash/BUILD.gn
@@ -4795,6 +4795,7 @@
     "app_list/app_list_test_util.cc",
     "app_list/app_list_test_util.h",
     "app_list/app_service/app_service_app_model_builder_unittest.cc",
+    "app_list/app_service/app_service_promise_app_model_builder_unittest.cc",
     "app_list/arc/arc_app_metrics_data_unittest.cc",
     "app_list/arc/arc_app_metrics_util_unittest.cc",
     "app_list/arc/arc_app_sync_metrics_helper_unittest.cc",
@@ -4965,6 +4966,7 @@
     "arc/input_overlay/touch_injector_unittest.cc",
     "arc/input_overlay/ui/action_view_unittest.cc",
     "arc/input_overlay/ui/edit_label_unittest.cc",
+    "arc/input_overlay/ui/editing_list_unittest.cc",
     "arc/input_overlay/ui/menu_entry_view_unittest.cc",
     "arc/instance_throttle/arc_active_window_throttle_observer_unittest.cc",
     "arc/instance_throttle/arc_app_launch_throttle_observer_unittest.cc",
diff --git a/chrome/browser/ash/accessibility/select_to_speak_browsertest.cc b/chrome/browser/ash/accessibility/select_to_speak_browsertest.cc
index 98dfc48b..7bc54a4 100644
--- a/chrome/browser/ash/accessibility/select_to_speak_browsertest.cc
+++ b/chrome/browser/ash/accessibility/select_to_speak_browsertest.cc
@@ -47,7 +47,6 @@
 #include "mojo/public/cpp/bindings/remote.h"
 #include "third_party/skia/include/core/SkColor.h"
 #include "ui/accessibility/accessibility_features.h"
-#include "ui/accessibility/accessibility_switches.h"
 #include "ui/compositor/layer.h"
 #include "ui/display/manager/display_manager.h"
 #include "ui/display/screen.h"
diff --git a/chrome/browser/ash/android_sms/android_sms_app_setup_controller_impl.cc b/chrome/browser/ash/android_sms/android_sms_app_setup_controller_impl.cc
index acacbc1..f7bafea 100644
--- a/chrome/browser/ash/android_sms/android_sms_app_setup_controller_impl.cc
+++ b/chrome/browser/ash/android_sms/android_sms_app_setup_controller_impl.cc
@@ -83,8 +83,7 @@
       webapps::WebappUninstallSource::kInternalPreinstalled,
       base::BindOnce(
           [](SuccessCallback callback, webapps::UninstallResultCode code) {
-            std::move(callback).Run(code ==
-                                    webapps::UninstallResultCode::kSuccess);
+            std::move(callback).Run(UninstallSucceeded(code));
           },
           std::move(callback)));
 }
diff --git a/chrome/browser/ash/app_list/app_list_syncable_service.cc b/chrome/browser/ash/app_list/app_list_syncable_service.cc
index a0b2e3d0..d0ccbad 100644
--- a/chrome/browser/ash/app_list/app_list_syncable_service.cc
+++ b/chrome/browser/ash/app_list/app_list_syncable_service.cc
@@ -28,6 +28,7 @@
 #include "chrome/browser/ash/app_list/app_list_model_updater.h"
 #include "chrome/browser/ash/app_list/app_list_sync_model_sanitizer.h"
 #include "chrome/browser/ash/app_list/app_service/app_service_app_model_builder.h"
+#include "chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder.h"
 #include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
 #include "chrome/browser/ash/app_list/arc/arc_app_utils.h"
 #include "chrome/browser/ash/app_list/chrome_app_list_item.h"
@@ -503,11 +504,19 @@
 
   app_service_apps_builder_ =
       std::make_unique<AppServiceAppModelBuilder>(controller);
+  if (ash::features::ArePromiseIconsEnabled()) {
+    app_service_promise_apps_builder_ =
+        std::make_unique<AppServicePromiseAppModelBuilder>(controller);
+  }
 
   DCHECK(profile_);
   SyncStarted();
 
   app_service_apps_builder_->Initialize(this, profile_, model_updater_.get());
+  if (ash::features::ArePromiseIconsEnabled()) {
+    app_service_promise_apps_builder_->Initialize(this, profile_,
+                                                  model_updater_.get());
+  }
 
   HandleUpdateFinished(false /* clean_up_after_init_sync */);
 
@@ -1182,6 +1191,9 @@
 
 void AppListSyncableService::Shutdown() {
   app_service_apps_builder_.reset();
+  if (ash::features::ArePromiseIconsEnabled()) {
+    app_service_promise_apps_builder_.reset();
+  }
 }
 
 void AppListSyncableService::SetAppListPreferredOrder(
diff --git a/chrome/browser/ash/app_list/app_list_syncable_service.h b/chrome/browser/ash/app_list/app_list_syncable_service.h
index 171e7fc..6c4d4a91 100644
--- a/chrome/browser/ash/app_list/app_list_syncable_service.h
+++ b/chrome/browser/ash/app_list/app_list_syncable_service.h
@@ -32,6 +32,7 @@
 
 class AppListModelUpdater;
 class AppServiceAppModelBuilder;
+class AppServicePromiseAppModelBuilder;
 class ChromeAppListItem;
 class Profile;
 
@@ -407,6 +408,8 @@
   std::unique_ptr<AppListSyncModelSanitizer> sync_model_sanitizer_;
 
   std::unique_ptr<AppServiceAppModelBuilder> app_service_apps_builder_;
+  std::unique_ptr<AppServicePromiseAppModelBuilder>
+      app_service_promise_apps_builder_;
   std::unique_ptr<syncer::SyncChangeProcessor> sync_processor_;
   SyncItemMap sync_items_;
   // Map that keeps pending request to transfer attributes from one app to
diff --git a/chrome/browser/ash/app_list/app_service/app_service_promise_app_item.cc b/chrome/browser/ash/app_list/app_service/app_service_promise_app_item.cc
new file mode 100644
index 0000000..3e784e3
--- /dev/null
+++ b/chrome/browser/ash/app_list/app_service/app_service_promise_app_item.cc
@@ -0,0 +1,77 @@
+// 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 "chrome/browser/ash/app_list/app_service/app_service_promise_app_item.h"
+
+#include "base/check.h"
+#include "chrome/browser/apps/app_service/promise_apps/promise_app_update.h"
+#include "chrome/browser/ash/app_list/app_list_model_updater.h"
+#include "chrome/browser/ash/app_list/chrome_app_list_item.h"
+
+// static
+const char AppServicePromiseAppItem::kItemType[] = "AppServicePromiseAppItem";
+
+AppServicePromiseAppItem::AppServicePromiseAppItem(
+    Profile* profile,
+    AppListModelUpdater* model_updater,
+    const apps::PromiseAppUpdate& update)
+    : ChromeAppListItem(profile, update.PackageId().ToString()) {
+  InitializeItem(update);
+
+  // Promise icons should not be synced as they are transient and only present
+  // during app installations.
+  SetIsEphemeral(true);
+
+  SetPosition(CalculateDefaultPositionIfApplicable());
+
+  // Set model updater last to avoid being called during construction.
+  set_model_updater(model_updater);
+}
+
+void AppServicePromiseAppItem::Activate(int event_flags) {
+  base::DoNothing();
+}
+
+const char* AppServicePromiseAppItem::GetItemType() const {
+  return AppServicePromiseAppItem::kItemType;
+}
+
+AppServicePromiseAppItem::~AppServicePromiseAppItem() = default;
+
+void AppServicePromiseAppItem::OnPromiseAppUpdate(
+    const apps::PromiseAppUpdate& update) {
+  if (update.NameChanged() && update.Name().has_value()) {
+    SetName(update.Name().value());
+  }
+  if (update.ProgressChanged() && update.Progress().has_value()) {
+    progress_ = update.Progress();
+  }
+}
+
+void AppServicePromiseAppItem::LoadIcon() {
+  // TODO(b/261907495): Retrieve icon from Promise App Icon Cache.
+}
+
+void AppServicePromiseAppItem::InitializeItem(
+    const apps::PromiseAppUpdate& update) {
+  CHECK(update.Name().has_value());
+  CHECK(update.ShouldShow());
+  SetName(update.Name().value());
+  if (update.Progress().has_value()) {
+    progress_ = update.Progress();
+  }
+  // TODO(b/261907495): Consider adding new AppStatus values specific to promise
+  // apps and update them in OnPromiseAppUpdate.
+  SetAppStatus(ash::AppStatus::kReady);
+}
+
+void AppServicePromiseAppItem::GetContextMenuModel(
+    ash::AppListItemContext item_context,
+    GetMenuModelCallback callback) {
+  // TODO(b/261907495): Create Promise App Context Menu.
+}
+
+app_list::AppContextMenu* AppServicePromiseAppItem::GetAppContextMenu() {
+  return nullptr;
+}
diff --git a/chrome/browser/ash/app_list/app_service/app_service_promise_app_item.h b/chrome/browser/ash/app_list/app_service/app_service_promise_app_item.h
new file mode 100644
index 0000000..348d9f6
--- /dev/null
+++ b/chrome/browser/ash/app_list/app_service/app_service_promise_app_item.h
@@ -0,0 +1,48 @@
+// 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.
+
+#ifndef CHROME_BROWSER_ASH_APP_LIST_APP_SERVICE_APP_SERVICE_PROMISE_APP_ITEM_H_
+#define CHROME_BROWSER_ASH_APP_LIST_APP_SERVICE_APP_SERVICE_PROMISE_APP_ITEM_H_
+
+#include "chrome/browser/apps/app_service/promise_apps/promise_app_update.h"
+#include "chrome/browser/ash/app_list/app_context_menu_delegate.h"
+#include "chrome/browser/ash/app_list/chrome_app_list_item.h"
+#include "components/services/app_service/public/cpp/app_launch_util.h"
+#include "components/services/app_service/public/cpp/app_types.h"
+#include "components/services/app_service/public/cpp/app_update.h"
+#include "components/services/app_service/public/cpp/icon_types.h"
+
+// A promise app list item provided by the App Service.
+class AppServicePromiseAppItem : public ChromeAppListItem {
+ public:
+  static const char kItemType[];
+
+  AppServicePromiseAppItem(Profile* profile,
+                           AppListModelUpdater* model_updater,
+                           const apps::PromiseAppUpdate& app_update);
+  AppServicePromiseAppItem(const AppServicePromiseAppItem&) = delete;
+  AppServicePromiseAppItem& operator=(const AppServicePromiseAppItem&) = delete;
+  ~AppServicePromiseAppItem() override;
+
+  // Update the promise app item with the new promise app info from the Promise
+  // App Registry Cache.
+  void OnPromiseAppUpdate(const apps::PromiseAppUpdate& update);
+
+ private:
+  void InitializeItem(const apps::PromiseAppUpdate& update);
+
+  // ChromeAppListItem overrides:
+  void LoadIcon() override;
+  void Activate(int event_flags) override;
+  const char* GetItemType() const override;
+  void GetContextMenuModel(ash::AppListItemContext item_context,
+                           GetMenuModelCallback callback) override;
+  app_list::AppContextMenu* GetAppContextMenu() override;
+
+  // Used to indicate the installation progress in the promise icon progress
+  // bar.
+  absl::optional<float> progress_;
+};
+
+#endif  // CHROME_BROWSER_ASH_APP_LIST_APP_SERVICE_APP_SERVICE_PROMISE_APP_ITEM_H_
diff --git a/chrome/browser/ash/app_list/app_service/app_service_promise_app_item_browsertest.cc b/chrome/browser/ash/app_list/app_service/app_service_promise_app_item_browsertest.cc
new file mode 100644
index 0000000..e935611
--- /dev/null
+++ b/chrome/browser/ash/app_list/app_service/app_service_promise_app_item_browsertest.cc
@@ -0,0 +1,112 @@
+// 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 "chrome/browser/ash/app_list/app_service/app_service_promise_app_item.h"
+
+#include "ash/app_list/app_list_model_provider.h"
+#include "ash/app_list/model/app_list_item.h"
+#include "ash/constants/ash_features.h"
+#include "chrome/browser/apps/app_service/app_service_proxy.h"
+#include "chrome/browser/apps/app_service/app_service_proxy_ash.h"
+#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
+#include "chrome/browser/apps/app_service/package_id.h"
+#include "chrome/browser/apps/app_service/promise_apps/promise_app.h"
+#include "chrome/browser/apps/app_service/promise_apps/promise_app_registry_cache.h"
+#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
+#include "chrome/browser/ash/app_list/app_list_client_impl.h"
+#include "chrome/browser/ash/app_list/app_list_syncable_service.h"
+#include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/common/pref_names.h"
+#include "components/services/app_service/public/cpp/app_types.h"
+#include "components/sync/protocol/entity_specifics.pb.h"
+#include "components/sync/test/fake_sync_change_processor.h"
+#include "components/sync/test/sync_change_processor_wrapper_for_test.h"
+#include "content/public/test/browser_test.h"
+
+namespace apps {
+
+const apps::PackageId kTestPackageId =
+    apps::PackageId(apps::AppType::kArc, "com.test.package");
+
+ash::AppListItem* GetAppListItem(const std::string& id) {
+  return ash::AppListModelProvider::Get()->model()->FindItem(id);
+}
+
+class AppServicePromiseAppItemBrowserTest
+    : public extensions::PlatformAppBrowserTest {
+ public:
+  AppServicePromiseAppItemBrowserTest() {
+    scoped_feature_list_.InitAndEnableFeature(ash::features::kPromiseIcons);
+  }
+  ~AppServicePromiseAppItemBrowserTest() override = default;
+
+  // extensions::PlatformAppBrowserTest:
+  void SetUpOnMainThread() override {
+    extensions::PlatformAppBrowserTest::SetUpOnMainThread();
+    AppListClientImpl* client = AppListClientImpl::GetInstance();
+    ASSERT_TRUE(client);
+    client->UpdateProfile();
+    cache_ = apps::AppServiceProxyFactory::GetForProfile(profile())
+                 ->PromiseAppRegistryCache();
+  }
+
+  apps::PromiseAppRegistryCache* cache() { return cache_; }
+
+ private:
+  apps::PromiseAppRegistryCache* cache_;
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
+                       ShouldShowUpdateCreatesItem) {
+  // Sync setup.
+  std::unique_ptr<syncer::FakeSyncChangeProcessor> sync_processor =
+      std::make_unique<syncer::FakeSyncChangeProcessor>();
+  app_list::AppListSyncableService* app_list_syncable_service_ =
+      app_list::AppListSyncableServiceFactory::GetForProfile(profile());
+  app_list_syncable_service_->MergeDataAndStartSyncing(
+      syncer::APP_LIST, {},
+      std::make_unique<syncer::SyncChangeProcessorWrapperForTest>(
+          sync_processor.get()));
+  content::RunAllTasksUntilIdle();
+
+  // Register a promise app in the promise app registry cache.
+  apps::PromiseAppPtr promise_app =
+      std::make_unique<PromiseApp>(kTestPackageId);
+  cache()->OnPromiseApp(std::move(promise_app));
+
+  // Promise app registration in the cache should not result in a promise app
+  // launcher item if should_show is false (which it is by default).
+  ash::AppListItem* item = GetAppListItem(kTestPackageId.ToString());
+  ASSERT_FALSE(item);
+
+  // Update the promise app to allow showing in the Launcher.
+  apps::PromiseAppPtr promise_app_update =
+      std::make_unique<PromiseApp>(kTestPackageId);
+  promise_app_update->name = "Test";
+  promise_app_update->should_show = true;
+  cache()->OnPromiseApp(std::move(promise_app_update));
+
+  // Promise app item should now exist in the model.
+  item = GetAppListItem(kTestPackageId.ToString());
+  ASSERT_TRUE(item);
+  ASSERT_EQ(item->name(), "Test");
+
+  // Verify that the promise app item is not added to local storage.
+  const base::Value::Dict& local_items =
+      profile()->GetPrefs()->GetDict(prefs::kAppListLocalState);
+  const base::Value::Dict* dict_item =
+      local_items.FindDict(kTestPackageId.ToString());
+  EXPECT_FALSE(dict_item);
+
+  // Verify that promise app item is not uploaded to sync data.
+  for (auto sync_change : sync_processor->changes()) {
+    const std::string item_id =
+        sync_change.sync_data().GetSpecifics().app_list().item_id();
+    EXPECT_NE(item_id, kTestPackageId.ToString());
+  }
+}
+
+}  // namespace apps
diff --git a/chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder.cc b/chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder.cc
new file mode 100644
index 0000000..0570a43
--- /dev/null
+++ b/chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder.cc
@@ -0,0 +1,60 @@
+// 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 "chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder.h"
+#include <ostream>
+
+#include "chrome/browser/apps/app_service/app_service_proxy.h"
+#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
+#include "chrome/browser/apps/app_service/promise_apps/promise_app_registry_cache.h"
+#include "chrome/browser/ash/app_list/app_list_controller_delegate.h"
+#include "chrome/browser/ash/app_list/app_list_model_builder.h"
+#include "chrome/browser/ash/app_list/app_service/app_service_promise_app_item.h"
+
+AppServicePromiseAppModelBuilder::AppServicePromiseAppModelBuilder(
+    AppListControllerDelegate* controller)
+    : AppListModelBuilder(controller, AppServicePromiseAppItem::kItemType) {}
+
+AppServicePromiseAppModelBuilder::~AppServicePromiseAppModelBuilder() = default;
+
+void AppServicePromiseAppModelBuilder::BuildModel() {
+  CHECK(!promise_app_registry_cache_observation_.IsObserving());
+  promise_app_registry_cache_observation_.Observe(
+      apps::AppServiceProxyFactory::GetForProfile(profile())
+          ->PromiseAppRegistryCache());
+
+  // No need to iterate through the registry cache and insert existing promise
+  // apps into the model since the registry cache will be empty on start up.
+  // Promise apps in the cache only get registered/ created when we start new
+  // app installations, at which point the AppServicePromiseAppModelBuilder
+  // should already exist. This will change in the future when we support ARC
+  // default promise apps.
+  // TODO(b/286981938): Insert existing promise app entries from the registry
+  // cache.
+}
+
+// Update the App Service Promise App Item for the appropriate promise app if
+// one already exists. Otherwise, create a new one.
+void AppServicePromiseAppModelBuilder::OnPromiseAppUpdate(
+    const apps::PromiseAppUpdate& update) {
+  ChromeAppListItem* item = GetAppItem(update.PackageId().ToString());
+  bool show = update.ShouldShow();
+  if (item) {
+    if (show) {
+      CHECK(item->GetItemType() == AppServicePromiseAppItem::kItemType);
+      static_cast<AppServicePromiseAppItem*>(item)->OnPromiseAppUpdate(update);
+    } else {
+      RemoveApp(update.PackageId().ToString(), false);
+    }
+  } else if (show) {
+    auto promise_app_item = std::make_unique<AppServicePromiseAppItem>(
+        profile(), model_updater(), update);
+    InsertApp(std::move(promise_app_item));
+  }
+}
+
+void AppServicePromiseAppModelBuilder::OnPromiseAppRegistryCacheWillBeDestroyed(
+    apps::PromiseAppRegistryCache* cache) {
+  promise_app_registry_cache_observation_.Reset();
+}
diff --git a/chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder.h b/chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder.h
new file mode 100644
index 0000000..8b160e1
--- /dev/null
+++ b/chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder.h
@@ -0,0 +1,44 @@
+// 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.
+
+#ifndef CHROME_BROWSER_ASH_APP_LIST_APP_SERVICE_APP_SERVICE_PROMISE_APP_MODEL_BUILDER_H_
+#define CHROME_BROWSER_ASH_APP_LIST_APP_SERVICE_APP_SERVICE_PROMISE_APP_MODEL_BUILDER_H_
+
+#include "base/scoped_observation.h"
+#include "chrome/browser/apps/app_service/promise_apps/promise_app_registry_cache.h"
+#include "chrome/browser/ash/app_list/app_list_model_builder.h"
+
+class AppListControllerDelegate;
+
+// Model builder that creates and manages App Service Promise App Items to track
+// entries in the profile's Promise App Registry Cache.
+class AppServicePromiseAppModelBuilder
+    : public AppListModelBuilder,
+      public apps::PromiseAppRegistryCache::Observer {
+ public:
+  explicit AppServicePromiseAppModelBuilder(
+      AppListControllerDelegate* controller);
+
+  AppServicePromiseAppModelBuilder(const AppServicePromiseAppModelBuilder&) =
+      delete;
+  AppServicePromiseAppModelBuilder& operator=(
+      const AppServicePromiseAppModelBuilder&) = delete;
+
+  ~AppServicePromiseAppModelBuilder() override;
+
+ private:
+  // AppListModelBuilder overrides:
+  void BuildModel() override;
+
+  // apps::PromiseAppRegistryCache::Observer overrides:
+  void OnPromiseAppUpdate(const apps::PromiseAppUpdate& update) override;
+  void OnPromiseAppRegistryCacheWillBeDestroyed(
+      apps::PromiseAppRegistryCache* cache) override;
+
+  base::ScopedObservation<apps::PromiseAppRegistryCache,
+                          apps::PromiseAppRegistryCache::Observer>
+      promise_app_registry_cache_observation_{this};
+};
+
+#endif  // CHROME_BROWSER_ASH_APP_LIST_APP_SERVICE_APP_SERVICE_PROMISE_APP_MODEL_BUILDER_H_
diff --git a/chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder_unittest.cc b/chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder_unittest.cc
new file mode 100644
index 0000000..7618ab9
--- /dev/null
+++ b/chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder_unittest.cc
@@ -0,0 +1,135 @@
+// 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 "chrome/browser/ash/app_list/app_service/app_service_promise_app_model_builder.h"
+
+#include "ash/constants/ash_features.h"
+#include "ash/public/cpp/app_list/app_list_types.h"
+#include "chrome/browser/apps/app_service/app_service_proxy.h"
+#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
+#include "chrome/browser/apps/app_service/app_service_test.h"
+#include "chrome/browser/apps/app_service/package_id.h"
+#include "chrome/browser/apps/app_service/promise_apps/promise_app.h"
+#include "chrome/browser/apps/app_service/promise_apps/promise_app_registry_cache.h"
+#include "chrome/browser/ash/app_list/app_list_test_util.h"
+#include "chrome/browser/ash/app_list/chrome_app_list_item.h"
+#include "chrome/browser/ash/app_list/internal_app/internal_app_metadata.h"
+#include "chrome/browser/ash/app_list/test/fake_app_list_model_updater.h"
+#include "chrome/browser/ash/app_list/test/test_app_list_controller_delegate.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/services/app_service/public/cpp/app_types.h"
+#include "content/public/test/test_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/display/test/test_screen.h"
+
+class AppServicePromiseAppModelBuilderTest : public app_list::AppListTestBase {
+ public:
+  AppServicePromiseAppModelBuilderTest() = default;
+  AppServicePromiseAppModelBuilderTest(
+      const AppServicePromiseAppModelBuilderTest&) = delete;
+  AppServicePromiseAppModelBuilderTest& operator=(
+      const AppServicePromiseAppModelBuilderTest&) = delete;
+
+  void TearDown() override {
+    ResetBuilder();
+    AppListTestBase::TearDown();
+  }
+
+ protected:
+  void ResetBuilder() {
+    scoped_callback_.reset();
+    builder_.reset();
+    controller_.reset();
+    model_updater_.reset();
+  }
+
+  // Creates a new builder, destroying any existing one.
+  void CreateBuilder(bool guest_mode) {
+    ResetBuilder();  // Destroy any existing builder in the correct order.
+    scoped_feature_list_.InitAndEnableFeature(ash::features::kPromiseIcons);
+    testing_profile()->SetGuestSession(guest_mode);
+    app_service_test_.SetUp(profile());
+    model_updater_ = std::make_unique<FakeAppListModelUpdater>(
+        /*profile=*/nullptr, /*reorder_delegate=*/nullptr);
+    controller_ = std::make_unique<test::TestAppListControllerDelegate>();
+    builder_ =
+        std::make_unique<AppServicePromiseAppModelBuilder>(controller_.get());
+    scoped_callback_ = std::make_unique<
+        AppServicePromiseAppModelBuilder::ScopedAppPositionInitCallbackForTest>(
+        builder_.get(),
+        base::BindRepeating(
+            &AppServicePromiseAppModelBuilderTest::InitAppPosition,
+            weak_ptr_factory_.GetWeakPtr()));
+    builder_->Initialize(nullptr, profile(), model_updater_.get());
+    cache_ = apps::AppServiceProxyFactory::GetForProfile(profile())
+                 ->PromiseAppRegistryCache();
+  }
+
+  void InitAppPosition(ChromeAppListItem* item) {
+    if (!last_position_.IsValid()) {
+      last_position_ = syncer::StringOrdinal::CreateInitialOrdinal();
+    } else {
+      last_position_ = last_position_.CreateAfter();
+    }
+    item->SetChromePosition(last_position_);
+  }
+
+  void RegisterTestApps() {
+    // Register two promise apps in the promise app registry cache.
+    apps::PromiseAppPtr promise_app_1 = std::make_unique<apps::PromiseApp>(
+        apps::PackageId(apps::AppType::kArc, "test1"));
+    promise_app_1->name = "Test 1";
+    promise_app_1->should_show = true;
+    cache()->OnPromiseApp(std::move(promise_app_1));
+
+    apps::PromiseAppPtr promise_app_2 = std::make_unique<apps::PromiseApp>(
+        apps::PackageId(apps::AppType::kArc, "test2"));
+    promise_app_2->name = "Test 2";
+    promise_app_2->should_show = true;
+    cache()->OnPromiseApp(std::move(promise_app_2));
+  }
+
+  void BuildModelTest(bool guest_mode) {
+    CreateBuilder(guest_mode);
+    EXPECT_EQ(model_updater()->ItemCount(), 0u);
+
+    RegisterTestApps();
+
+    // Confirm there are 2 launcher promise app items.
+    EXPECT_EQ(model_updater()->ItemCount(), 2u);
+    EXPECT_EQ(model_updater()->ItemAtForTest(0)->id(), "android:test1");
+    EXPECT_EQ(model_updater()->ItemAtForTest(0)->name(), "Test 1");
+    EXPECT_EQ(model_updater()->ItemAtForTest(1)->id(), "android:test2");
+    EXPECT_EQ(model_updater()->ItemAtForTest(1)->name(), "Test 2");
+  }
+
+  AppListModelUpdater* model_updater() { return model_updater_.get(); }
+
+  apps::PromiseAppRegistryCache* cache() { return cache_; }
+
+ private:
+  apps::AppServiceTest app_service_test_;
+  std::unique_ptr<
+      AppServicePromiseAppModelBuilder::ScopedAppPositionInitCallbackForTest>
+      scoped_callback_;
+  std::unique_ptr<AppServicePromiseAppModelBuilder> builder_;
+  std::unique_ptr<test::TestAppListControllerDelegate> controller_;
+  std::unique_ptr<FakeAppListModelUpdater> model_updater_;
+  display::test::TestScreen test_screen_;
+  std::unique_ptr<Profile> profile_;
+  base::test::ScopedFeatureList scoped_feature_list_;
+  apps::PromiseAppRegistryCache* cache_;
+  syncer::StringOrdinal last_position_;
+  base::WeakPtrFactory<AppServicePromiseAppModelBuilderTest> weak_ptr_factory_{
+      this};
+};
+
+TEST_F(AppServicePromiseAppModelBuilderTest, BuildModel) {
+  BuildModelTest(/*guest_mode=*/true);
+}
+
+TEST_F(AppServicePromiseAppModelBuilderTest, BuildModelGuestMode) {
+  BuildModelTest(/*guest_mode=*/false);
+}
diff --git a/chrome/browser/ash/apps/apk_web_app_service_lacros_browsertest.cc b/chrome/browser/ash/apps/apk_web_app_service_lacros_browsertest.cc
index 6cc50b2..8ffda13 100644
--- a/chrome/browser/ash/apps/apk_web_app_service_lacros_browsertest.cc
+++ b/chrome/browser/ash/apps/apk_web_app_service_lacros_browsertest.cc
@@ -65,7 +65,9 @@
  public:
   ApkWebAppServiceLacrosBrowserTest() {
     scoped_feature_list_.InitWithFeatures(
-        {features::kLacrosSupport, features::kLacrosPrimary}, {});
+        {features::kLacrosSupport, features::kLacrosPrimary,
+         features::kLacrosOnly, features::kLacrosProfileMigrationForceOff},
+        {});
     dependency_manager_subscription_ =
         BrowserContextDependencyManager::GetInstance()
             ->RegisterCreateServicesCallbackForTesting(base::BindRepeating(
diff --git a/chrome/browser/ash/arc/input_overlay/actions/action.cc b/chrome/browser/ash/arc/input_overlay/actions/action.cc
index 5a0f0a4..3d838e1 100644
--- a/chrome/browser/ash/arc/input_overlay/actions/action.cc
+++ b/chrome/browser/ash/arc/input_overlay/actions/action.cc
@@ -448,12 +448,6 @@
   return id_ <= kMaxDefaultActionID;
 }
 
-bool Action::IsOnLeftSide() {
-  auto* parent = action_view_->parent();
-  DCHECK(parent);
-  return action_view_->GetTouchCenterInWindow().x() < parent->width() / 2;
-}
-
 bool Action::CreateTouchPressedEvent(const base::TimeTicks& time_stamp,
                                      std::list<ui::TouchEvent>& touch_events) {
   if (touch_id_) {
@@ -579,17 +573,21 @@
     const auto root_point = point.ToString();
     float scale = touch_injector_->window()->GetHost()->device_scale_factor();
     point.Scale(scale);
-    const auto root_point_pixel = point.ToString();
-    if (touch_injector_->rotation_transform()) {
-      point = touch_injector_->rotation_transform()->MapPoint(point);
-    }
-    touch_down_positions_.emplace_back(point);
 
     VLOG(1) << "Calculate touch position for location at index " << i
             << ": local position {" << calculated_point << "}, root location {"
-            << root_point << "}, root location in pixels {" << root_point_pixel
+            << root_point << "}, root location in pixels {" << point.ToString()
             << "}";
+
+    if (touch_injector_->rotation_transform()) {
+      point = touch_injector_->rotation_transform()->MapPoint(point);
+    }
+    touch_down_positions_.emplace_back(std::move(point));
   }
+
+  on_left_or_middle_side_ =
+      touch_down_positions_[0].x() <= content_bounds.width() / 2 ? true : false;
+
   DCHECK_EQ(touch_down_positions_.size(), original_positions_.size());
 }
 
diff --git a/chrome/browser/ash/arc/input_overlay/actions/action.h b/chrome/browser/ash/arc/input_overlay/actions/action.h
index b9073c0b..f34e904 100644
--- a/chrome/browser/ash/arc/input_overlay/actions/action.h
+++ b/chrome/browser/ash/arc/input_overlay/actions/action.h
@@ -150,8 +150,6 @@
   ActionView* action_view() const { return action_view_; }
   void set_action_view(ActionView* action_view) { action_view_ = action_view; }
 
-  bool IsOnLeftSide();
-
  protected:
   // |touch_injector| must be non-NULL and own this Action.
   explicit Action(TouchInjector* touch_injector);
diff --git a/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc b/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc
index c0a1e87d..a64d0f17 100644
--- a/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc
+++ b/chrome/browser/ash/arc/input_overlay/display_overlay_controller.cc
@@ -541,6 +541,10 @@
   return type;
 }
 
+void DisplayOverlayController::AddNewAction(ActionType action_type) {
+  touch_injector_->AddNewAction(action_type);
+}
+
 int DisplayOverlayController::GetTouchInjectorActionsSize() {
   return touch_injector_->actions().size();
 }
diff --git a/chrome/browser/ash/arc/input_overlay/display_overlay_controller.h b/chrome/browser/ash/arc/input_overlay/display_overlay_controller.h
index dc28fe0..02f4760 100644
--- a/chrome/browser/ash/arc/input_overlay/display_overlay_controller.h
+++ b/chrome/browser/ash/arc/input_overlay/display_overlay_controller.h
@@ -72,6 +72,9 @@
   // Get window state type.
   InputOverlayWindowStateType GetWindowStateType() const;
 
+  // For editor.
+  void AddNewAction(ActionType action_type = ActionType::TAP);
+
   int GetTouchInjectorActionsSize();
 
   // For menu entry hover state:
diff --git a/chrome/browser/ash/arc/input_overlay/touch_injector.cc b/chrome/browser/ash/arc/input_overlay/touch_injector.cc
index 9e0c633..ecd3e59 100644
--- a/chrome/browser/ash/arc/input_overlay/touch_injector.cc
+++ b/chrome/browser/ash/arc/input_overlay/touch_injector.cc
@@ -858,7 +858,7 @@
   return action;
 }
 
-void TouchInjector::NotifyActionAdded(const Action& action) {
+void TouchInjector::NotifyActionAdded(Action& action) {
   for (auto& observer : observers_) {
     observer.OnActionAdded(action);
   }
@@ -888,12 +888,15 @@
 }
 
 void TouchInjector::AddNewAction(ActionType action_type) {
+  DCHECK(IsBeta());
   auto action = CreateRawAction(action_type);
   if (!action) {
     return;
   }
-
   action->InitFromEditor();
+
+  // Apply the change right away for beta.
+  NotifyActionAdded(*actions_.emplace_back(std::move(action)));
 }
 
 void TouchInjector::RemoveAction(Action* action) {
diff --git a/chrome/browser/ash/arc/input_overlay/touch_injector.h b/chrome/browser/ash/arc/input_overlay/touch_injector.h
index d5192eb..294265d 100644
--- a/chrome/browser/ash/arc/input_overlay/touch_injector.h
+++ b/chrome/browser/ash/arc/input_overlay/touch_injector.h
@@ -239,7 +239,7 @@
   std::unique_ptr<Action> CreateRawAction(ActionType action_type);
 
   // For observers.
-  void NotifyActionAdded(const Action& action);
+  void NotifyActionAdded(Action& action);
   void NotifyActionRemoved(const Action& action);
   void NotifyActionTypeChanged(const Action& action, const Action& new_action);
   void NotifyActionUpdated(const Action& action);
diff --git a/chrome/browser/ash/arc/input_overlay/touch_injector_observer.h b/chrome/browser/ash/arc/input_overlay/touch_injector_observer.h
index d608cb3e..eed352d 100644
--- a/chrome/browser/ash/arc/input_overlay/touch_injector_observer.h
+++ b/chrome/browser/ash/arc/input_overlay/touch_injector_observer.h
@@ -17,7 +17,7 @@
  public:
   TouchInjectorObserver();
 
-  virtual void OnActionAdded(const Action& action) {}
+  virtual void OnActionAdded(Action& action) {}
   virtual void OnActionRemoved(const Action& action) {}
   // Once action type is changed, the original action is removed and
   // |new_action| with new type is added.
diff --git a/chrome/browser/ash/arc/input_overlay/ui/button_options_menu.cc b/chrome/browser/ash/arc/input_overlay/ui/button_options_menu.cc
index d507ac3..5602bf56 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/button_options_menu.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/button_options_menu.cc
@@ -154,7 +154,7 @@
   SetLayoutManager(std::make_unique<views::BoxLayout>(
       views::BoxLayout::Orientation::kVertical));
   SetBorder(views::CreateEmptyBorder(
-      action_->IsOnLeftSide()
+      action_->on_left_or_middle_side()
           ? gfx::Insets::TLBR(16, 16 + kTriangleHeight, 16, 16)
           : gfx::Insets::TLBR(16, 16, 16, 16 + kTriangleHeight)));
 
@@ -343,7 +343,7 @@
   int y = action_->GetUICenterPosition().y();
   auto parent_size = controller_->GetOverlayWidgetContentsView()->size();
 
-  if (action_->IsOnLeftSide()) {
+  if (action_->on_left_or_middle_side()) {
     x += action_view->width() + kMenuActionGap;
   } else {
     x -= width() + kMenuActionGap;
@@ -393,7 +393,7 @@
   ui::ColorProvider* color_provider = GetColorProvider();
   flags.setColor(color_provider->GetColor(cros_tokens::kCrosSysBaseElevated));
   int height = GetHeightForWidth(kMenuWidth);
-  bool draw_triangle_on_left = action_->IsOnLeftSide();
+  bool draw_triangle_on_left = action_->on_left_or_middle_side();
   int action_offset = CalculateActionOffset(height);
   canvas->DrawPath(BackgroundPath(height, draw_triangle_on_left, action_offset),
                    flags);
diff --git a/chrome/browser/ash/arc/input_overlay/ui/editing_list.cc b/chrome/browser/ash/arc/input_overlay/ui/editing_list.cc
index 2bc1228b..4b6023b 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/editing_list.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/editing_list.cc
@@ -70,15 +70,23 @@
 
   AddHeader(main_container);
 
+  scroll_content_ =
+      main_container->AddChildView(std::make_unique<views::View>());
+  scroll_content_
+      ->SetLayoutManager(std::make_unique<views::BoxLayout>(
+          views::BoxLayout::Orientation::kVertical,
+          /*inside_border_insets=*/gfx::Insets(),
+          /*between_child_spacing=*/8))
+      ->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);
+
   // Add contents.
   if (HasControls()) {
-    AddControlListContent(main_container);
+    AddControlListContent();
   } else {
-    AddZeroStateContent(main_container);
+    AddZeroStateContent();
   }
 
   SizeToPreferredSize();
-  InvalidateLayout();
 }
 
 bool EditingList::HasControls() const {
@@ -119,9 +127,11 @@
       IDS_APP_LIST_FOLDER_NAME_PLACEHOLDER));
 }
 
-void EditingList::AddZeroStateContent(views::View* container) {
+void EditingList::AddZeroStateContent() {
+  DCHECK(scroll_content_);
+
   auto* content_container =
-      container->AddChildView(std::make_unique<ash::RoundedContainer>());
+      scroll_content_->AddChildView(std::make_unique<ash::RoundedContainer>());
   content_container->SetBackground(
       views::CreateThemedSolidBackground(cros_tokens::kCrosSysSystemOnBase));
   content_container->SetBorderInsets(gfx::Insets::VH(48, 32));
@@ -147,7 +157,7 @@
       u"Your button will show up here.", cros_tokens::kCrosSysSecondary));
 }
 
-void EditingList::AddControlListContent(views::View* container) {
+void EditingList::AddControlListContent() {
   // Add list content as:
   // --------------------------
   // | ---------------------- |
@@ -159,14 +169,8 @@
   // | ......                 |
   // --------------------------
   // TODO(b/270969479): Wrap |scroll_content| in a scroll view.
-  scroll_content_ = container->AddChildView(std::make_unique<views::View>());
-  scroll_content_
-      ->SetLayoutManager(std::make_unique<views::BoxLayout>(
-          views::BoxLayout::Orientation::kVertical,
-          /*inside_border_insets=*/gfx::Insets(),
-          /*between_child_spacing=*/8))
-      ->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);
   DCHECK(controller_);
+  DCHECK(scroll_content_);
   for (const auto& action : controller_->touch_injector()->actions()) {
     scroll_content_->AddChildView(
         std::make_unique<ActionViewListItem>(controller_, action.get()));
@@ -174,8 +178,7 @@
 }
 
 void EditingList::OnAddButtonPressed() {
-  // TODO(b/270969479): Implement the function for the button.
-  NOTIMPLEMENTED();
+  controller_->AddNewAction();
 }
 
 void EditingList::OnDoneButtonPressed() {
@@ -188,9 +191,18 @@
   return gfx::Size(kMainContainerWidth, GetHeightForWidth(kMainContainerWidth));
 }
 
-void EditingList::OnActionAdded(const Action& action) {
-  NOTIMPLEMENTED();
+void EditingList::OnActionAdded(Action& action) {
+  DCHECK(scroll_content_);
+  if (controller_->GetTouchInjectorActionsSize() == 1u) {
+    // Clear the zero-state.
+    scroll_content_->RemoveAllChildViews();
+  }
+  scroll_content_->AddChildView(
+      std::make_unique<ActionViewListItem>(controller_, &action));
+
+  SizeToPreferredSize();
 }
+
 void EditingList::OnActionRemoved(const Action& action) {
   NOTIMPLEMENTED();
 }
diff --git a/chrome/browser/ash/arc/input_overlay/ui/editing_list.h b/chrome/browser/ash/arc/input_overlay/ui/editing_list.h
index bce8012..62e6336 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/editing_list.h
+++ b/chrome/browser/ash/arc/input_overlay/ui/editing_list.h
@@ -33,6 +33,7 @@
   ~EditingList() override;
 
  private:
+  friend class EditingListTest;
   friend class EditLabelTest;
 
   void Init();
@@ -40,8 +41,8 @@
 
   // Add UI components to |container| as children.
   void AddHeader(views::View* container);
-  void AddZeroStateContent(views::View* container);
-  void AddControlListContent(views::View* container);
+  void AddZeroStateContent();
+  void AddControlListContent();
 
   // Functions related to buttons.
   void OnAddButtonPressed();
@@ -51,7 +52,7 @@
   gfx::Size CalculatePreferredSize() const override;
 
   // TouchInjectorObserver:
-  void OnActionAdded(const Action& action) override;
+  void OnActionAdded(Action& action) override;
   void OnActionRemoved(const Action& action) override;
   void OnActionTypeChanged(const Action& action,
                            const Action& new_action) override;
diff --git a/chrome/browser/ash/arc/input_overlay/ui/editing_list_unittest.cc b/chrome/browser/ash/arc/input_overlay/ui/editing_list_unittest.cc
new file mode 100644
index 0000000..8dee6b2b1
--- /dev/null
+++ b/chrome/browser/ash/arc/input_overlay/ui/editing_list_unittest.cc
@@ -0,0 +1,75 @@
+// 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 "chrome/browser/ash/arc/input_overlay/ui/editing_list.h"
+
+#include <memory>
+
+#include "ash/constants/ash_features.h"
+#include "chrome/browser/ash/arc/input_overlay/test/view_test_base.h"
+#include "chrome/browser/ash/arc/input_overlay/touch_injector.h"
+#include "chrome/browser/ash/arc/input_overlay/ui/input_mapping_view.h"
+
+namespace arc::input_overlay {
+
+class EditingListTest : public ViewTestBase {
+ public:
+  EditingListTest() = default;
+  ~EditingListTest() override = default;
+
+  size_t GetActionListItemsSize() {
+    DCHECK(editing_list_->scroll_content_);
+    DCHECK(editing_list_);
+    if (editing_list_->HasControls()) {
+      return editing_list_->scroll_content_->children().size();
+    }
+    return 0;
+  }
+
+  size_t GetActionViewSize() {
+    DCHECK(input_mapping_view_);
+    return input_mapping_view_->children().size();
+  }
+
+  size_t GetTouchInjectorActionSize() {
+    DCHECK(touch_injector_);
+    return touch_injector_->actions().size();
+  }
+
+  void PressAddButton() {
+    DCHECK(editing_list_);
+    editing_list_->OnAddButtonPressed();
+  }
+
+  std::unique_ptr<EditingList> editing_list_;
+
+ private:
+  void SetUp() override {
+    ViewTestBase::SetUp();
+    InitWithFeature(ash::features::kArcInputOverlayBeta);
+    SetDisplayMode(DisplayMode::kEdit);
+
+    editing_list_ =
+        std::make_unique<EditingList>(display_overlay_controller_.get());
+    editing_list_->Init();
+    DCHECK(editing_list_->scroll_content_);
+  }
+
+  void TearDown() override {
+    editing_list_.reset();
+    ViewTestBase::TearDown();
+  }
+};
+
+TEST_F(EditingListTest, TestEditingListAddNewAction) {
+  EXPECT_EQ(2u, GetActionListItemsSize());
+  EXPECT_EQ(2u, GetActionViewSize());
+  EXPECT_EQ(2u, GetTouchInjectorActionSize());
+  PressAddButton();
+  EXPECT_EQ(3u, GetActionListItemsSize());
+  EXPECT_EQ(3u, GetActionViewSize());
+  EXPECT_EQ(3u, GetTouchInjectorActionSize());
+}
+
+}  // namespace arc::input_overlay
diff --git a/chrome/browser/ash/arc/input_overlay/ui/input_mapping_view.cc b/chrome/browser/ash/arc/input_overlay/ui/input_mapping_view.cc
index 1c5b27a..9e4cb72 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/input_mapping_view.cc
+++ b/chrome/browser/ash/arc/input_overlay/ui/input_mapping_view.cc
@@ -136,10 +136,14 @@
   }
 }
 
-void InputMappingView::OnActionAdded(const Action& action) {
+void InputMappingView::OnActionAdded(Action& action) {
   // No add function for pre-beta version.
   DCHECK(IsBeta());
-  NOTIMPLEMENTED();
+
+  auto view = action.CreateView(controller_);
+  if (view) {
+    AddChildView(std::move(view))->SetDisplayMode(current_display_mode_);
+  }
 }
 
 void InputMappingView::OnActionRemoved(const Action& action) {
diff --git a/chrome/browser/ash/arc/input_overlay/ui/input_mapping_view.h b/chrome/browser/ash/arc/input_overlay/ui/input_mapping_view.h
index 35edad0..ec48173 100644
--- a/chrome/browser/ash/arc/input_overlay/ui/input_mapping_view.h
+++ b/chrome/browser/ash/arc/input_overlay/ui/input_mapping_view.h
@@ -42,7 +42,7 @@
   void OnGestureEvent(ui::GestureEvent* event) override;
 
   // TouchInjectorObserver:
-  void OnActionAdded(const Action& action) override;
+  void OnActionAdded(Action& action) override;
   void OnActionRemoved(const Action& action) override;
   void OnActionTypeChanged(const Action& action,
                            const Action& new_action) override;
diff --git a/chrome/browser/ash/drive/drive_integration_service.cc b/chrome/browser/ash/drive/drive_integration_service.cc
index de44327..e8efb6f 100644
--- a/chrome/browser/ash/drive/drive_integration_service.cc
+++ b/chrome/browser/ash/drive/drive_integration_service.cc
@@ -22,6 +22,7 @@
 #include "base/hash/md5.h"
 #include "base/logging.h"
 #include "base/memory/raw_ptr.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/strings/stringprintf.h"
 #include "base/system/sys_info.h"
@@ -92,12 +93,16 @@
 namespace drive {
 namespace {
 
+using base::Seconds;
+using base::SequencedTaskRunner;
+using base::TimeDelta;
 using content::BrowserContext;
 using content::BrowserThread;
 using drivefs::mojom::DriveFs;
 using drivefs::pinning::PinManager;
 using network::NetworkConnectionTracker;
 using network::mojom::ConnectionType;
+using prefs::kDriveFsBulkPinningEnabled;
 
 // Name of the directory used to store metadata.
 const base::FilePath::CharType kMetadataDirectory[] = FILE_PATH_LITERAL("meta");
@@ -287,7 +292,7 @@
   // list of files to pin to the UI thread without waiting for the remaining
   // data to be cleared.
   metadata_storage.reset();
-  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+  SequencedTaskRunner::GetCurrentDefault()->PostTask(
       FROM_HERE,
       base::BindOnce(&CleanupGCacheV1, std::move(cache_directory),
                      std::move(downloads_directory), std::move(dirty_files)));
@@ -396,7 +401,7 @@
 
     if (util::IsDriveFsBulkPinningEnabled(profile_)) {
       pref_change_registrar_.Add(
-          prefs::kDriveFsBulkPinningEnabled,
+          kDriveFsBulkPinningEnabled,
           base::BindRepeating(&PreferenceWatcher::ToggleBulkPinning,
                               weak_ptr_factory_.GetWeakPtr()));
     }
@@ -602,7 +607,7 @@
   }
 
   void OnMountFailed(MountFailure failure,
-                     absl::optional<base::TimeDelta> remount_delay) override {
+                     absl::optional<TimeDelta> remount_delay) override {
     mount_observer_->OnMountFailed(failure, std::move(remount_delay));
   }
 
@@ -610,7 +615,7 @@
     mount_observer_->OnMounted(path);
   }
 
-  void OnUnmounted(absl::optional<base::TimeDelta> remount_delay) override {
+  void OnUnmounted(absl::optional<TimeDelta> remount_delay) override {
     mount_observer_->OnUnmounted(std::move(remount_delay));
   }
 
@@ -733,9 +738,8 @@
 
   void OnProgress(const Progress& progress) override {
     if (progress.IsError()) {
-      VLOG(1) << "Disabling bulk pinning preference";
-      pref_service_->SetBoolean(drive::prefs::kDriveFsBulkPinningEnabled,
-                                false);
+      pref_service_->SetBoolean(kDriveFsBulkPinningEnabled, false);
+      VLOG(1) << "Disabled bulk-pinning because of error " << progress.stage;
     }
   }
 
@@ -797,6 +801,7 @@
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
 
   weak_ptr_factory_.InvalidateWeakPtrs();
+  bulk_pinning_pref_sampling_ = false;
 
   RemoveDriveMountPoint();
 
@@ -917,14 +922,14 @@
   }
   in_clear_cache_ = true;
 
-  base::TimeDelta delay;
+  TimeDelta delay;
   if (IsMounted()) {
     RemoveDriveMountPoint();
     // TODO(crbug/1069328): We wait 2 seconds here so that DriveFS can unmount
     // completely. Ideally we'd wait for an unmount complete callback.
-    delay = base::Seconds(2);
+    delay = Seconds(2);
   }
-  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
+  SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
       FROM_HERE,
       base::BindOnce(
           &DriveIntegrationService::ClearCacheAndRemountFileSystemAfterDelay,
@@ -1001,6 +1006,7 @@
   DCHECK(enabled_);
 
   weak_ptr_factory_.InvalidateWeakPtrs();
+  bulk_pinning_pref_sampling_ = false;
 
   if (GetDriveFsHost()->IsMounted()) {
     AddDriveMountPointAfterMounted();
@@ -1049,7 +1055,7 @@
           NotificationHandler::Type::TRANSIENT, *notification, nullptr);
 
       // Disable bulk-pinning.
-      GetPrefs()->SetBoolean(prefs::kDriveFsBulkPinningEnabled, false);
+      GetPrefs()->SetBoolean(kDriveFsBulkPinningEnabled, false);
     }
   }
 
@@ -1118,7 +1124,7 @@
 }
 
 void DriveIntegrationService::MaybeRemountFileSystem(
-    absl::optional<base::TimeDelta> remount_delay,
+    absl::optional<TimeDelta> remount_delay,
     bool failed_to_mount) {
   DCHECK_EQ(INITIALIZED, state_);
 
@@ -1155,12 +1161,12 @@
       return;
     }
     remount_delay =
-        base::Seconds(5 * (1 << (drivefs_consecutive_failures_count_ - 1)));
+        Seconds(5 * (1 << (drivefs_consecutive_failures_count_ - 1)));
     logger_->Log(logging::LOG_WARNING, "DriveFs died, retry in %d seconds",
                  static_cast<int>(remount_delay.value().InSeconds()));
   }
 
-  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
+  SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
       FROM_HERE,
       base::BindOnce(&DriveIntegrationService::AddDriveMountPoint,
                      weak_ptr_factory_.GetWeakPtr()),
@@ -1209,11 +1215,31 @@
     }
 
     ToggleBulkPinning();
+
+    if (!bulk_pinning_pref_sampling_) {
+      bulk_pinning_pref_sampling_ = true;
+      SampleBulkPinningPref();
+    }
   }
 }
 
+void DriveIntegrationService::SampleBulkPinningPref() {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  DCHECK(bulk_pinning_pref_sampling_);
+  const bool enabled = GetPrefs()->GetBoolean(kDriveFsBulkPinningEnabled);
+  VLOG(1) << "Bulk-pinning is currently " << (enabled ? "en" : "dis")
+          << "abled";
+  base::UmaHistogramBoolean("FileBrowser.GoogleDrive.BulkPinning.Enabled",
+                            enabled);
+  SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
+      FROM_HERE,
+      base::BindOnce(&DriveIntegrationService::SampleBulkPinningPref,
+                     weak_ptr_factory_.GetWeakPtr()),
+      base::Hours(1));
+}
+
 void DriveIntegrationService::OnUnmounted(
-    absl::optional<base::TimeDelta> remount_delay) {
+    absl::optional<TimeDelta> remount_delay) {
   UmaEmitUnmountOutcome(remount_delay ? DriveMountStatus::kTemporaryUnavailable
                                       : DriveMountStatus::kUnknownFailure);
   MaybeRemountFileSystem(remount_delay, false);
@@ -1221,7 +1247,7 @@
 
 void DriveIntegrationService::OnMountFailed(
     MountFailure failure,
-    absl::optional<base::TimeDelta> remount_delay) {
+    absl::optional<TimeDelta> remount_delay) {
   PrefService* prefs = GetPrefs();
   DriveMountStatus status = ConvertMountFailure(failure);
   UmaEmitMountStatus(status);
@@ -1329,7 +1355,7 @@
     return;
   }
 
-  if (GetPrefs()->GetBoolean(prefs::kDriveFsBulkPinningEnabled)) {
+  if (GetPrefs()->GetBoolean(kDriveFsBulkPinningEnabled)) {
     pin_manager_->ShouldPin(true);
     pin_manager_->Start();
   } else {
@@ -1532,7 +1558,7 @@
 }
 
 void DriveIntegrationService::RestartDrive() {
-  MaybeRemountFileSystem(base::TimeDelta(), false);
+  MaybeRemountFileSystem(TimeDelta(), false);
 }
 
 void DriveIntegrationService::SetStartupArguments(
diff --git a/chrome/browser/ash/drive/drive_integration_service.h b/chrome/browser/ash/drive/drive_integration_service.h
index 372ac2e..e4d003e 100644
--- a/chrome/browser/ash/drive/drive_integration_service.h
+++ b/chrome/browser/ash/drive/drive_integration_service.h
@@ -408,6 +408,10 @@
   // Enable or disable DriveFS bulk pinning.
   void ToggleBulkPinning();
 
+  // Regularly samples the bulk-pinning preference and stores the result in a
+  // UMA histogram.
+  void SampleBulkPinningPref();
+
   void OnGetOfflineItemsPage(
       int64_t total_size,
       mojo::Remote<drivefs::mojom::SearchQuery> search_query,
@@ -446,6 +450,10 @@
   bool enabled_;
   bool mount_failed_ = false;
   bool in_clear_cache_ = false;
+
+  // Is the bulk-pinning preference sampling task currently scheduled?
+  bool bulk_pinning_pref_sampling_ = false;
+
   // Custom mount point name that can be injected for testing in constructor.
   std::string mount_point_name_;
 
diff --git a/chrome/browser/ash/extensions/autotest_private/autotest_private_apitest.cc b/chrome/browser/ash/extensions/autotest_private/autotest_private_apitest.cc
index 406c1dd..bf2adc08 100644
--- a/chrome/browser/ash/extensions/autotest_private/autotest_private_apitest.cc
+++ b/chrome/browser/ash/extensions/autotest_private/autotest_private_apitest.cc
@@ -42,6 +42,7 @@
 #include "chrome/browser/ash/settings/scoped_testing_cros_settings.h"
 #include "chrome/browser/ash/system_web_apps/test_support/test_system_web_app_installation.h"
 #include "chrome/browser/extensions/extension_apitest.h"
+#include "chrome/browser/profiles/profile_manager.h"
 #include "chrome/browser/ui/ash/shelf/chrome_shelf_prefs.h"
 #include "chrome/browser/ui/browser_window.h"
 #include "components/feature_engagement/public/feature_constants.h"
@@ -505,13 +506,29 @@
 
  protected:
   AutotestPrivateLacrosTest() {
-    feature_list_.InitAndEnableFeature(ash::features::kLacrosSupport);
+    feature_list_.InitWithFeatures(
+        {
+            ash::features::kLacrosSupport,
+            ash::features::kLacrosPrimary,
+            ash::features::kLacrosOnly,
+            ash::features::kLacrosProfileMigrationForceOff,
+        },
+        {});
     crosapi::BrowserManager::DisableForTesting();
   }
   ~AutotestPrivateLacrosTest() override {
     crosapi::BrowserManager::EnableForTesting();
   }
 
+  void SetUpOnMainThread() override {
+    // For testing APIs, we need web browser instance as JS runtime.
+    Browser::CreateParams params(ProfileManager::GetLastUsedProfile(), false);
+    Browser::Create(params);
+    SelectFirstBrowser();
+
+    AutotestPrivateApiTest::SetUpOnMainThread();
+  }
+
  private:
   base::test::ScopedFeatureList feature_list_;
 };
diff --git a/chrome/browser/ash/extensions/file_manager/private_api_util.cc b/chrome/browser/ash/extensions/file_manager/private_api_util.cc
index 25cbf9f..789a1fae 100644
--- a/chrome/browser/ash/extensions/file_manager/private_api_util.cc
+++ b/chrome/browser/ash/extensions/file_manager/private_api_util.cc
@@ -248,8 +248,12 @@
   switch (stage) {
     case drivefs::pinning::Stage::kStopped:
       return extensions::api::file_manager_private::BULK_PIN_STAGE_STOPPED;
-    case drivefs::pinning::Stage::kPaused:
-      return extensions::api::file_manager_private::BULK_PIN_STAGE_PAUSED;
+    case drivefs::pinning::Stage::kPausedOffline:
+      return extensions::api::file_manager_private::
+          BULK_PIN_STAGE_PAUSED_OFFLINE;
+    case drivefs::pinning::Stage::kPausedBatterySaver:
+      return extensions::api::file_manager_private::
+          BULK_PIN_STAGE_PAUSED_BATTERY_SAVER;
     case drivefs::pinning::Stage::kGettingFreeSpace:
       return extensions::api::file_manager_private::
           BULK_PIN_STAGE_GETTING_FREE_SPACE;
diff --git a/chrome/browser/ash/file_manager/file_manager_string_util.cc b/chrome/browser/ash/file_manager/file_manager_string_util.cc
index b01a0b5..9884375 100644
--- a/chrome/browser/ash/file_manager/file_manager_string_util.cc
+++ b/chrome/browser/ash/file_manager/file_manager_string_util.cc
@@ -216,6 +216,8 @@
              IDS_FILE_BROWSER_GOOGLE_DRIVE_SINGLE_FILE_SYNCING_LABEL);
   SET_STRING("DRIVE_ALL_FILES_SYNCED",
              IDS_FILE_BROWSER_GOOGLE_DRIVE_ALL_FILES_SYNCED_LABEL);
+  SET_STRING("DRIVE_BULK_PINNING_BATTERY_SAVER",
+             IDS_FILE_BROWSER_BULK_PINNING_BATTERY_SAVER_LABEL);
   SET_STRING("DRIVE_BULK_PINNING_OFFLINE",
              IDS_FILE_BROWSER_BULK_PINNING_OFFLINE_LABEL);
   SET_STRING("DRIVE_BULK_PINNING_NOT_ENOUGH_SPACE",
@@ -355,7 +357,8 @@
   SET_STRING("ARCHIVE_MOUNT_MESSAGE", IDS_FILE_BROWSER_ARCHIVE_MOUNT_MESSAGE);
   SET_STRING("ARCHIVE_MOUNT_INVALID_PATH",
              IDS_FILE_BROWSER_ARCHIVE_MOUNT_INVALID_PATH);
-  SET_STRING("BULK_PINNING_TURN_ON", IDS_FILE_BROWSER_BULK_PINNING_TURN_ON);
+  SET_STRING("BULK_PINNING_BATTERY_SAVER",
+             IDS_FILE_BROWSER_BULK_PINNING_BATTERY_SAVER);
   SET_STRING("BULK_PINNING_ERROR", IDS_FILE_BROWSER_BULK_PINNING_ERROR);
   SET_STRING("BULK_PINNING_EXPLANATION",
              IDS_FILE_BROWSER_BULK_PINNING_EXPLANATION);
@@ -368,6 +371,7 @@
   SET_STRING("BULK_PINNING_POINT_1", IDS_FILE_BROWSER_BULK_PINNING_POINT_1);
   SET_STRING("BULK_PINNING_SPACE", IDS_FILE_BROWSER_BULK_PINNING_SPACE);
   SET_STRING("BULK_PINNING_TITLE", IDS_FILE_BROWSER_BULK_PINNING_TITLE);
+  SET_STRING("BULK_PINNING_TURN_ON", IDS_FILE_BROWSER_BULK_PINNING_TURN_ON);
   SET_STRING("BULK_PINNING_VIEW_STORAGE",
              IDS_FILE_BROWSER_BULK_PINNING_VIEW_STORAGE);
   SET_STRING("CALCULATING_SIZE", IDS_FILE_BROWSER_CALCULATING_SIZE);
diff --git a/chrome/browser/ash/geolocation/system_geolocation_source.cc b/chrome/browser/ash/geolocation/system_geolocation_source.cc
index a8f60a0..6d14041 100644
--- a/chrome/browser/ash/geolocation/system_geolocation_source.cc
+++ b/chrome/browser/ash/geolocation/system_geolocation_source.cc
@@ -50,17 +50,29 @@
   }
 }
 
-void SystemGeolocationSource::AppAttemptsToUseGeolocation() {
+void SystemGeolocationSource::TrackGeolocationAttempted(
+    const std::string& app_name) {
   if (auto* controller = GeolocationPrivacySwitchController::Get()) {
-    controller->OnAppStartsUsingGeolocation(
-        l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME));
+    if (!app_name.empty()) {
+      controller->TrackGeolocationAttempted(app_name);
+    } else {
+      // Use the default name for this app.
+      controller->TrackGeolocationAttempted(
+          l10n_util::GetStringUTF8(IDS_SHORT_PRODUCT_NAME));
+    }
   }
 }
 
-void SystemGeolocationSource::AppCeasesToUseGeolocation() {
+void SystemGeolocationSource::TrackGeolocationRelinquished(
+    const std::string& app_name) {
   if (auto* controller = GeolocationPrivacySwitchController::Get()) {
-    controller->OnAppStopsUsingGeolocation(
-        l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME));
+    if (!app_name.empty()) {
+      controller->TrackGeolocationRelinquished(app_name);
+    } else {
+      // Use the default id for this app.
+      controller->TrackGeolocationAttempted(
+          l10n_util::GetStringUTF8(IDS_SHORT_PRODUCT_NAME));
+    }
   }
 }
 
diff --git a/chrome/browser/ash/geolocation/system_geolocation_source.h b/chrome/browser/ash/geolocation/system_geolocation_source.h
index 78d84b1..46ebd7d 100644
--- a/chrome/browser/ash/geolocation/system_geolocation_source.h
+++ b/chrome/browser/ash/geolocation/system_geolocation_source.h
@@ -38,8 +38,8 @@
   // device::SystemGeolocationSource:
   void RegisterPermissionUpdateCallback(
       PermissionUpdateCallback callback) override;
-  void AppAttemptsToUseGeolocation() override;
-  void AppCeasesToUseGeolocation() override;
+  void TrackGeolocationAttempted(const std::string& app_name) override;
+  void TrackGeolocationRelinquished(const std::string& app_name) override;
 
  private:
   // SessionObserver:
diff --git a/chrome/browser/ash/login/oobe_quick_start/second_device_auth_broker.cc b/chrome/browser/ash/login/oobe_quick_start/second_device_auth_broker.cc
index ada7401..5f10d9d461 100644
--- a/chrome/browser/ash/login/oobe_quick_start/second_device_auth_broker.cc
+++ b/chrome/browser/ash/login/oobe_quick_start/second_device_auth_broker.cc
@@ -80,6 +80,10 @@
 constexpr char kHttpMethod[] = "POST";
 constexpr char kHttpContentType[] = "application/json";
 
+constexpr char kGetChallengeDataRequest[] = R"({
+      "target_device_type": "CHROME_OS"
+    })";
+
 constexpr auto kRejectionReasonErrorMap = base::MakeFixedFlatMap<
     base::StringPiece,
     SecondDeviceAuthBroker::RefreshTokenRejectionResponse::Reason>({
@@ -118,8 +122,8 @@
             "Google's authentication server"
           trigger: "When the user starts the Quick Start flow from OOBE"
           data:
-            "Nothing. Authentication to this API is done through Chrome's API "
-            "key"
+            "A JSON dict that identifies the device type as ChromeOS. "
+            "Authentication to this API is done through Chrome's API key"
           destination: GOOGLE_OWNED_SERVICE
         }
         policy {
@@ -596,7 +600,7 @@
       /*http_method=*/kHttpMethod,
       /*content_type=*/kHttpContentType,
       /*timeout_ms=*/kGetChallengeDataTimeoutInSeconds * 1000,
-      /*post_data=*/std::string(),
+      /*post_data=*/kGetChallengeDataRequest,
       /*headers=*/std::vector<std::string>(),
       /*annotation_tag=*/kChallengeDataAnnotation,
       /*is_stable_channel=*/chrome::GetChannel() ==
diff --git a/chrome/browser/ash/login/oobe_quick_start/second_device_auth_broker_unittest.cc b/chrome/browser/ash/login/oobe_quick_start/second_device_auth_broker_unittest.cc
index 7654acd..18dace9 100644
--- a/chrome/browser/ash/login/oobe_quick_start/second_device_auth_broker_unittest.cc
+++ b/chrome/browser/ash/login/oobe_quick_start/second_device_auth_broker_unittest.cc
@@ -8,11 +8,14 @@
 #include <string>
 #include <utility>
 
+#include "base/json/json_reader.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/test/bind.h"
 #include "base/test/gtest_util.h"
 #include "base/test/task_environment.h"
 #include "base/test/test_future.h"
 #include "base/types/expected.h"
+#include "base/values.h"
 #include "chrome/browser/ash/login/oobe_quick_start/connectivity/fido_assertion_info.h"
 #include "chromeos/ash/components/attestation/attestation_flow.h"
 #include "chromeos/ash/components/attestation/mock_attestation_flow.h"
@@ -22,12 +25,16 @@
 #include "google_apis/gaia/google_service_auth_error.h"
 #include "net/http/http_status_code.h"
 #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
+#include "services/network/public/cpp/data_element.h"
+#include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/cpp/shared_url_loader_factory.h"
 #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
 #include "services/network/test/test_url_loader_factory.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/abseil-cpp/absl/types/variant.h"
+#include "url/gurl.h"
 
 namespace ash::quick_start {
 
@@ -84,6 +91,8 @@
 constexpr char kFidoCredentialId[] = "fido_credential_id";
 constexpr char kCertificate[] = "fake_certificate";
 constexpr char kFakeDeviceId[] = "fake_device_id";
+constexpr char kTargetDeviceType[] = "target_device_type";
+constexpr char kChromeOS[] = "CHROME_OS";
 
 MATCHER_P(ProtoBufContentBindingEq, expected, "") {
   return arg.content_binding() == expected;
@@ -216,6 +225,18 @@
     test_factory_.AddResponse(url, response, status);
   }
 
+  // Sets an `interceptor`. Overwrites any other interceptor that may have been
+  // previously set.
+  void SetInterceptor(
+      const network::TestURLLoaderFactory::Interceptor& interceptor) {
+    test_factory_.SetInterceptor(interceptor);
+  }
+
+  void SimulateBadRequest(const std::string& url) {
+    test_factory_.AddResponse(url, /*content=*/std::string(),
+                              net::HTTP_BAD_REQUEST);
+  }
+
   void SimulateAuthError(const std::string& url) {
     test_factory_.AddResponse(url, /*content=*/std::string(),
                               net::HTTP_UNAUTHORIZED);
@@ -317,7 +338,41 @@
 }
 
 TEST_F(SecondDeviceAuthBrokerTest, GetChallengeBytesReturnsChallengeBytes) {
-  AddFakeResponse(kGetChallengeDataUrl, kFakeChallengeDataResponse);
+  // Set an interceptor that checks the validity of the incoming request for
+  // challenge bytes.
+  SetInterceptor(
+      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
+        if (request.url != GURL(kGetChallengeDataUrl)) {
+          return;
+        }
+
+        if (!request.request_body || !request.request_body->elements() ||
+            request.request_body->elements()->empty()) {
+          SimulateBadRequest(kGetChallengeDataUrl);
+          return;
+        }
+
+        absl::optional<base::Value> request_body =
+            base::JSONReader::Read(request.request_body->elements()
+                                       ->at(0)
+                                       .As<network::DataElementBytes>()
+                                       .AsStringPiece());
+        if (!request_body || !request_body->is_dict()) {
+          SimulateBadRequest(kGetChallengeDataUrl);
+          return;
+        }
+
+        const base::Value::Dict& request_dict = request_body->GetDict();
+        const std::string* target_device_type =
+            request_dict.FindString(kTargetDeviceType);
+        if (!target_device_type || *target_device_type != kChromeOS) {
+          SimulateBadRequest(kGetChallengeDataUrl);
+          return;
+        }
+
+        AddFakeResponse(kGetChallengeDataUrl, kFakeChallengeDataResponse);
+      }));
+
   base::expected<std::string, GoogleServiceAuthError> response =
       GetChallengeBytes();
   ASSERT_TRUE(response.has_value());
diff --git a/chrome/browser/companion/visual_search/BUILD.gn b/chrome/browser/companion/visual_search/BUILD.gn
index 0499b37..129a6d1b 100644
--- a/chrome/browser/companion/visual_search/BUILD.gn
+++ b/chrome/browser/companion/visual_search/BUILD.gn
@@ -6,6 +6,8 @@
   sources = [
     "features.cc",
     "features.h",
+    "visual_search_classifier_host.cc",
+    "visual_search_classifier_host.h",
     "visual_search_suggestions_service.cc",
     "visual_search_suggestions_service.h",
   ]
@@ -21,12 +23,17 @@
 
 source_set("unit_tests") {
   testonly = true
-  sources = [ "visual_search_suggestions_service_unittest.cc" ]
-
+  sources = [
+    "visual_search_classifier_host_unittest.cc",
+    "visual_search_suggestions_service_unittest.cc",
+  ]
   deps = [
     ":visual_search",
     "//base",
     "//base/test:test_support",
+    "//chrome/browser",
+    "//chrome/common",
+    "//chrome/test:test_support",
     "//components/optimization_guide/core:test_support",
     "//components/optimization_guide/proto:optimization_guide_proto",
     "//testing/gmock",
diff --git a/chrome/browser/companion/visual_search/visual_search_classifier_host.cc b/chrome/browser/companion/visual_search/visual_search_classifier_host.cc
new file mode 100644
index 0000000..878d695
--- /dev/null
+++ b/chrome/browser/companion/visual_search/visual_search_classifier_host.cc
@@ -0,0 +1,115 @@
+// 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 "chrome/browser/companion/visual_search/visual_search_classifier_host.h"
+
+#include "base/base64.h"
+#include "base/metrics/histogram_macros_local.h"
+#include "base/task/thread_pool.h"
+#include "chrome/browser/companion/visual_search/features.h"
+#include "chrome/browser/companion/visual_search/visual_search_suggestions_service.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "third_party/skia/include/core/SkColorSpace.h"
+#include "third_party/skia/include/core/SkStream.h"
+#include "third_party/skia/include/encode/SkJpegEncoder.h"
+#include "ui/gfx/image/buffer_w_stream.h"
+#include "ui/gfx/image/image_skia.h"
+#include "ui/gfx/image/image_skia_rep.h"
+#include "url/gurl.h"
+
+namespace companion::visual_search {
+
+namespace {
+absl::optional<std::string> Base64EncodeBitmap(const SkBitmap& bitmap) {
+  gfx::BufferWStream stream;
+  const bool encoding_succeeded =
+      SkJpegEncoder::Encode(&stream, bitmap.pixmap(), {});
+
+  if (!encoding_succeeded) {
+    return absl::nullopt;
+  }
+
+  base::StringPiece mime_subtype = "jpg";
+  std::string result = "data:image/";
+  result.append(mime_subtype.begin(), mime_subtype.end());
+  result.append(";base64,");
+  result.append(
+      base::Base64Encode(base::as_bytes(base::make_span(stream.TakeBuffer()))));
+  return result;
+}
+
+// Close the provided model file.
+void CloseModelFile(base::File model_file) {
+  if (!model_file.IsValid()) {
+    return;
+  }
+  model_file.Close();
+}
+}  // namespace
+
+VisualSearchClassifierHost::VisualSearchClassifierHost(
+    VisualSearchSuggestionsService* visual_search_service)
+    : visual_search_service_(visual_search_service) {}
+
+VisualSearchClassifierHost::~VisualSearchClassifierHost() = default;
+
+void VisualSearchClassifierHost::OnClassificationResult(
+    const std::vector<SkBitmap>& images) {
+  std::vector<std::string> data_uris;
+  data_uris.reserve(images.size());
+  LOCAL_HISTOGRAM_COUNTS_100("Companion.VisualSearch.ClassificationResultsSize",
+                             images.size());
+
+  // converts list of SkBitmaps to data uris used as img.src for browser.
+  for (const auto& image : images) {
+    auto data_uri = Base64EncodeBitmap(image);
+    if (data_uri) {
+      data_uris.push_back(data_uri.value());
+    }
+  }
+
+  // TODO(b/284648407): Do mojom IPC to side panel using mojom::CompanionPage.
+}
+
+// TODO(pstjuste): RenderFrameHost is used to setup IPC communication with
+// renderer process via the InterfaceRegistry. Eventually, the url will be
+// used for caching visual search suggestions without having to do IPC.
+void VisualSearchClassifierHost::StartClassification(
+    content::RenderFrameHost* render_frame_host,
+    const GURL& validated_url) {
+  base::File model = visual_search_service_->GetModelFile();
+  LOCAL_HISTOGRAM_BOOLEAN("Companion.VisualSearch.ModelFileSuccess",
+                          model.IsValid());
+  if (!model.IsValid()) {
+    return;
+  }
+
+  std::string base64_config;
+  absl::optional<std::string> config_switch =
+      switches::GetVisualSearchConfigForCompanionOverride();
+
+  // Replace empty string with config switch if we have one.
+  if (config_switch) {
+    base64_config = std::move(config_switch.value());
+  }
+
+  VLOG(1) << "ClassificationSuccess " << classifier_agent_.is_null();
+  LOCAL_HISTOGRAM_BOOLEAN("Companion.VisualSearch.StartClassificationSuccess",
+                          !classifier_agent_.is_null());
+  if (!classifier_agent_.is_null()) {
+    std::move(classifier_agent_).Run(0, std::move(model), base64_config);
+  } else {
+    // Closing file in background thread since we did not make IPC.
+    base::ThreadPool::PostTask(
+        FROM_HERE, {base::MayBlock()},
+        base::BindOnce(&CloseModelFile, std::move(model)));
+  }
+}
+
+void VisualSearchClassifierHost::SetClassifierAgentForTesting(
+    ClassifierAgent agent) {
+  classifier_agent_ = std::move(agent);
+}
+}  // namespace companion::visual_search
diff --git a/chrome/browser/companion/visual_search/visual_search_classifier_host.h b/chrome/browser/companion/visual_search/visual_search_classifier_host.h
new file mode 100644
index 0000000..7277636
--- /dev/null
+++ b/chrome/browser/companion/visual_search/visual_search_classifier_host.h
@@ -0,0 +1,55 @@
+// 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.
+
+#ifndef CHROME_BROWSER_COMPANION_VISUAL_SEARCH_VISUAL_SEARCH_CLASSIFIER_HOST_H_
+#define CHROME_BROWSER_COMPANION_VISUAL_SEARCH_VISUAL_SEARCH_CLASSIFIER_HOST_H_
+
+#include <memory>
+#include "chrome/browser/companion/visual_search/visual_search_suggestions_service.h"
+#include "content/public/browser/render_frame_host.h"
+#include "url/gurl.h"
+
+namespace companion::visual_search {
+
+// This class serves as the main orchestator for visual search suggestions
+// components. It handles mojom IPC with both main renderer and side panel.
+// It also fetches model file descriptors from the keyed service.
+class VisualSearchClassifierHost {
+ public:
+  using ClassifierAgent =
+      base::OnceCallback<void(int, base::File, std::string)>;
+
+  explicit VisualSearchClassifierHost(
+      VisualSearchSuggestionsService* visual_search_service);
+
+  VisualSearchClassifierHost(const VisualSearchClassifierHost&) = delete;
+  VisualSearchClassifierHost& operator=(const VisualSearchClassifierHost&) =
+      delete;
+  ~VisualSearchClassifierHost();
+
+  // This is the main method used by the companion page handler to start the
+  // visual search classification task. The RenderFrameHost is needed to
+  // establish IPC channel with the Renderer process.
+  void StartClassification(content::RenderFrameHost* render_frame_host,
+                           const GURL& validated_url);
+
+  // Set the classifier agent that will be used to do mojom IPC.
+  // This should only be used for testing, not for production.
+  void SetClassifierAgentForTesting(ClassifierAgent agent);
+
+ private:
+  // Processes the list of images returned from the visual search classifier.
+  // Its main job is to take a list of SkBitmap and convert to data uris.
+  // The list of image data uris are sent to side panel companion for rendering.
+  void OnClassificationResult(const std::vector<SkBitmap>& images);
+
+  // Pointer to visual search service which we do not own.
+  raw_ptr<VisualSearchSuggestionsService> visual_search_service_ = nullptr;
+
+  // This classifier agent is used to send mojom IPC to renderer.
+  ClassifierAgent classifier_agent_;
+};
+}  // namespace companion::visual_search
+
+#endif  // CHROME_BROWSER_COMPANION_VISUAL_SEARCH_VISUAL_SEARCH_CLASSIFIER_HOST_H_
diff --git a/chrome/browser/companion/visual_search/visual_search_classifier_host_unittest.cc b/chrome/browser/companion/visual_search/visual_search_classifier_host_unittest.cc
new file mode 100644
index 0000000..67ccfa1a
--- /dev/null
+++ b/chrome/browser/companion/visual_search/visual_search_classifier_host_unittest.cc
@@ -0,0 +1,175 @@
+// 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 "chrome/browser/companion/visual_search/visual_search_classifier_host.h"
+
+#include <memory>
+
+#include "base/containers/flat_set.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/functional/bind.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/path_service.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/task_traits.h"
+#include "base/task/thread_pool.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "chrome/browser/companion/visual_search/features.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "components/optimization_guide/core/test_model_info_builder.h"
+#include "components/optimization_guide/core/test_optimization_guide_model_provider.h"
+#include "components/optimization_guide/proto/models.pb.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/mock_render_process_host.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/public/test/web_contents_tester.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace companion::visual_search {
+
+namespace {
+
+static const char kModelFilename[] = "visual_model.tflite";
+
+}  // namespace
+
+class VisualSearchClassifierHostTest : public ChromeRenderViewHostTestHarness {
+ public:
+  VisualSearchClassifierHostTest() : url_("www.style-files.com") {}
+  ~VisualSearchClassifierHostTest() override = default;
+
+  void SetUp() override {
+    ChromeRenderViewHostTestHarness::SetUp();
+
+    scoped_refptr<base::SequencedTaskRunner> background_task_runner =
+        base::ThreadPool::CreateSequencedTaskRunner(
+            {base::MayBlock(), base::TaskPriority::BEST_EFFORT});
+    test_model_provider_ = std::make_unique<
+        optimization_guide::TestOptimizationGuideModelProvider>();
+    service_ = std::make_unique<
+        companion::visual_search::VisualSearchSuggestionsService>(
+        test_model_provider_.get(), background_task_runner);
+
+    visual_search_host_ =
+        std::make_unique<companion::visual_search::VisualSearchClassifierHost>(
+            service_.get());
+  }
+
+  void SetModelPath() {
+    base::FilePath test_data_dir;
+    base::PathService::Get(base::DIR_SOURCE_ROOT, &test_data_dir);
+    test_data_dir = test_data_dir.AppendASCII("components/test/data");
+
+    base::flat_set<base::FilePath> additional_files;
+    additional_files.insert(test_data_dir.AppendASCII(kModelFilename));
+
+    model_info_ =
+        optimization_guide::TestModelInfoBuilder()
+            .SetModelFilePath(test_data_dir.AppendASCII(kModelFilename))
+            .SetAdditionalFiles(additional_files)
+            .SetVersion(123)
+            .Build();
+
+    service_->OnModelUpdated(
+        optimization_guide::proto::OptimizationTarget::
+            OPTIMIZATION_TARGET_VISUAL_SEARCH_CLASSIFICATION,
+        *model_info_);
+
+    base::RunLoop().RunUntilIdle();
+  }
+
+  void TearDown() override {
+    service_->Shutdown();
+    ChromeRenderViewHostTestHarness::TearDown();
+  }
+
+ protected:
+  std::unique_ptr<optimization_guide::TestOptimizationGuideModelProvider>
+      test_model_provider_;
+  std::unique_ptr<optimization_guide::ModelInfo> model_info_;
+  std::unique_ptr<companion::visual_search::VisualSearchSuggestionsService>
+      service_;
+  std::unique_ptr<companion::visual_search::VisualSearchClassifierHost>
+      visual_search_host_;
+  const GURL url_;
+  base::HistogramTester histogram_tester_;
+};
+
+TEST_F(VisualSearchClassifierHostTest, StartClassification) {
+  SetModelPath();
+  VisualSearchClassifierHost::ClassifierAgent agent = base::BindOnce(
+      [](int request_id, base::File model_file, std::string proto_string) {
+        EXPECT_EQ(request_id, 0);
+        ASSERT_TRUE(model_file.IsValid());
+        EXPECT_EQ(proto_string, "");
+      });
+  visual_search_host_->SetClassifierAgentForTesting(std::move(agent));
+  visual_search_host_->StartClassification(
+      web_contents()->GetPrimaryMainFrame(), url_);
+  histogram_tester_.ExpectBucketCount("Companion.VisualSearch.ModelFileSuccess",
+                                      true, 1);
+  histogram_tester_.ExpectBucketCount(
+      "Companion.VisualSearch.StartClassificationSuccess", true, 1);
+}
+
+TEST_F(VisualSearchClassifierHostTest, StartClassification_WithOverride) {
+  SetModelPath();
+  const std::string config_string = "config_string";
+  base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
+      switches::kVisualSearchConfigForCompanion, config_string);
+  VisualSearchClassifierHost::ClassifierAgent agent = base::BindOnce(
+      [](int request_id, base::File model_file, std::string proto_string) {
+        EXPECT_EQ(request_id, 0);
+        ASSERT_TRUE(model_file.IsValid());
+        EXPECT_EQ(proto_string, "config_string");
+      });
+  visual_search_host_->SetClassifierAgentForTesting(std::move(agent));
+  visual_search_host_->StartClassification(
+      web_contents()->GetPrimaryMainFrame(), url_);
+  histogram_tester_.ExpectBucketCount("Companion.VisualSearch.ModelFileSuccess",
+                                      true, 1);
+  histogram_tester_.ExpectBucketCount(
+      "Companion.VisualSearch.StartClassificationSuccess", true, 1);
+}
+
+TEST_F(VisualSearchClassifierHostTest, StartClassification_NoModelSet) {
+  VisualSearchClassifierHost::ClassifierAgent agent = base::BindOnce(
+      [](int request_id, base::File model_file, std::string proto_string) {
+        EXPECT_EQ(request_id, 0);
+        ASSERT_FALSE(model_file.IsValid());
+        EXPECT_EQ(proto_string, "");
+      });
+  visual_search_host_->SetClassifierAgentForTesting(std::move(agent));
+  visual_search_host_->StartClassification(
+      web_contents()->GetPrimaryMainFrame(), url_);
+  histogram_tester_.ExpectBucketCount("Companion.VisualSearch.ModelFileSuccess",
+                                      false, 1);
+}
+
+TEST_F(VisualSearchClassifierHostTest,
+       StartClassification_ModelSetWithNoCallbackSet) {
+  SetModelPath();
+  visual_search_host_->StartClassification(
+      web_contents()->GetPrimaryMainFrame(), url_);
+  histogram_tester_.ExpectBucketCount("Companion.VisualSearch.ModelFileSuccess",
+                                      true, 1);
+  histogram_tester_.ExpectBucketCount(
+      "Companion.VisualSearch.StartClassificationSuccess", false, 1);
+}
+
+TEST_F(VisualSearchClassifierHostTest,
+       StartClassification_NoModelSetAndNoCallbackSet) {
+  base::HistogramTester histogram_tester;
+  visual_search_host_->StartClassification(
+      web_contents()->GetPrimaryMainFrame(), url_);
+  histogram_tester_.ExpectBucketCount("Companion.VisualSearch.ModelFileSuccess",
+                                      false, 1);
+}
+
+}  // namespace companion::visual_search
diff --git a/chrome/browser/extensions/extension_keeplist_chromeos.cc b/chrome/browser/extensions/extension_keeplist_chromeos.cc
index cf19d3e..ab92bb8 100644
--- a/chrome/browser/extensions/extension_keeplist_chromeos.cc
+++ b/chrome/browser/extensions/extension_keeplist_chromeos.cc
@@ -74,6 +74,7 @@
     extension_misc::kSigninProfileTestExtensionId,
     extension_misc::kGuestModeTestExtensionId,
     extension_misc::kHelpAppExtensionId,
+    extension_misc::kAutotestPrivateTestExtensionId,
     file_manager::kImageLoaderExtensionId,
 #endif
     extension_misc::kKeyboardExtensionId,
diff --git a/chrome/browser/extensions/service_worker_lifetime_strong_keepalive_browsertest.cc b/chrome/browser/extensions/service_worker_lifetime_strong_keepalive_browsertest.cc
index 4971738b..2913c4c 100644
--- a/chrome/browser/extensions/service_worker_lifetime_strong_keepalive_browsertest.cc
+++ b/chrome/browser/extensions/service_worker_lifetime_strong_keepalive_browsertest.cc
@@ -201,8 +201,16 @@
 
 // Tests that the service workers will not stop if both extensions are
 // allowlisted via policy and the port is not closed.
+// TODO(https://crbug.com/1454339): Flakes on Linux.
+#if BUILDFLAG(IS_LINUX)
+#define MAYBE_ServiceWorkersDoNotTimeOutWithPolicy \
+  DISABLED_ServiceWorkersDoNotTimeOutWithPolicy
+#else
+#define MAYBE_ServiceWorkersDoNotTimeOutWithPolicy \
+  ServiceWorkersDoNotTimeOutWithPolicy
+#endif
 IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeStrongKeepaliveBrowsertest,
-                       ServiceWorkersDoNotTimeOutWithPolicy) {
+                       MAYBE_ServiceWorkersDoNotTimeOutWithPolicy) {
   base::Value::List urls;
   // Both extensions receive extended lifetime.
   urls.Append(kTestOpenerExtensionUrl);
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 1064c8a..9b4bda3 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -1292,6 +1292,11 @@
     "expiry_milestone": 122
   },
   {
+    "name": "cros-battery-saver-always-on",
+    "owners": [ "chromeos-bsm@google.com", "cwd" ],
+    "expiry_milestone": 122
+  },
+  {
     "name": "cros-labs-overview-desk-navigation",
     "owners": [ "richui", "janetmac" ],
     "expiry_milestone": 122
@@ -6860,6 +6865,11 @@
     "expiry_milestone": 116
   },
   {
+    "name": "service-worker-static-router",
+    "owners": [ "yyanagisawa@google.com", "sisidovski@google.com", "chrome-worker@google.com" ],
+    "expiry_milestone": 125
+  },
+  {
     "name": "set-market-url-for-testing",
     "owners": [ "//chrome/android/java/src/org/chromium/chrome/browser/omaha/OWNERS" ],
     // This is required by test teams to verify functionality on devices which
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 6021349..6569e33 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -740,6 +740,11 @@
     "fetch handler, the feature may affect the page load. This feature will be "
     "overridden by chrome://flags/#service-worker-bypass-fetch-handler";
 
+const char kServiceWorkerStaticRouterName[] = "Service Worker Static Router";
+const char kServiceWorkerStaticRouterDescription[] =
+    "When enabled, Chrome will enable the Service Worker Static Routing API. "
+    "https://chromestatus.com/feature/5185352976826368";
+
 const char kChromeLabsName[] = "Chrome Labs";
 const char kChromeLabsDescription[] =
     "Access Chrome Labs through the toolbar menu to see featured user-facing "
@@ -5349,6 +5354,12 @@
     "Show a desk button that provides quick access to the desk menu in the "
     "shelf in clamshell mode when there is more than one desk.";
 
+const char kCrosBatterySaverAlwaysOnName[] =
+    "Make ChromeOS Battery Saver on all the time";
+const char kCrosBatterySaverAlwaysOnDescription[] =
+    "Turns on ChomeOS Battery Saver all the time, even when charging or fully "
+    "charged. Used for testing ChromeOS Battery Saver Mode.";
+
 const char kCrosBatterySaverName[] =
     "Enable ChromeOS Battery Saver Mode Support";
 const char kCrosBatterySaverDescription[] =
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 02cad043..fa256da 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -403,6 +403,9 @@
 extern const char kServiceWorkerBypassFetchHandlerForMainResourceName[];
 extern const char kServiceWorkerBypassFetchHandlerForMainResourceDescription[];
 
+extern const char kServiceWorkerStaticRouterName[];
+extern const char kServiceWorkerStaticRouterDescription[];
+
 extern const char kCanvasOopRasterizationName[];
 extern const char kCanvasOopRasterizationDescription[];
 
@@ -3059,6 +3062,9 @@
 extern const char kCaptureModeGifRecordingName[];
 extern const char kCaptureModeGifRecordingDescription[];
 
+extern const char kCrosBatterySaverAlwaysOnName[];
+extern const char kCrosBatterySaverAlwaysOnDescription[];
+
 extern const char kCrosBatterySaverName[];
 extern const char kCrosBatterySaverDescription[];
 
diff --git a/chrome/browser/pdf/pdf_extension_accessibility_test.cc b/chrome/browser/pdf/pdf_extension_accessibility_test.cc
index 98a9b1dc..21aee1f 100644
--- a/chrome/browser/pdf/pdf_extension_accessibility_test.cc
+++ b/chrome/browser/pdf/pdf_extension_accessibility_test.cc
@@ -640,6 +640,13 @@
     return enabled;
   }
 
+  std::vector<base::test::FeatureRef> GetDisabledFeatures() const override {
+    auto disabled = PDFExtensionAccessibilityTest::GetDisabledFeatures();
+    // PDF OCR should not modify the dump.
+    disabled.push_back(::features::kPdfOcr);
+    return disabled;
+  }
+
   void RunPDFTest(const base::FilePath::CharType* pdf_file) {
     base::FilePath test_path = ui_test_utils::GetTestFilePath(
         base::FilePath(FILE_PATH_LITERAL("pdf")),
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_category_button.html b/chrome/browser/resources/chromeos/emoji_picker/emoji_category_button.html
index e5d2e5e8..0846bc12 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_category_button.html
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_category_button.html
@@ -40,5 +40,6 @@
   iron-icon="[[icon]]"
   on-click="handleClick"
   aria-label$="[[getAriaLabel(name, gifSupport)]]"
+  aria-pressed$="[[getAriaPressedState(gifSupport)]]"
 >
 </cr-icon-button>
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_category_button.ts b/chrome/browser/resources/chromeos/emoji_picker/emoji_category_button.ts
index f0ee885..c68d0916 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_category_button.ts
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_category_button.ts
@@ -63,6 +63,10 @@
 
     return ARIA_LABELS_WITH_GIF_SUPPORT[name] ?? name;
   }
+
+  private getAriaPressedState(active: boolean): string {
+    return active ? 'true' : 'false';
+  }
 }
 
 declare global {
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_group_button.html b/chrome/browser/resources/chromeos/emoji_picker/emoji_group_button.html
index c328f5a3..66d74c4e 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_group_button.html
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_group_button.html
@@ -31,6 +31,7 @@
     class$="[[calculateClassName(active)]]"
     on-click="handleClick"
     aria-label="[[name]]"
+    aria-pressed$="[[getAriaPressedState(active)]]"
     disabled="[[disabled]]"
     custom-tab-index="[[customTabIndex]]">
 </cr-icon-button>
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_group_button.ts b/chrome/browser/resources/chromeos/emoji_picker/emoji_group_button.ts
index aa36ab3..6bc3d7ff 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_group_button.ts
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_group_button.ts
@@ -45,6 +45,10 @@
   private calculateClassName(active: boolean): string {
     return active ? 'emoji-group-active' : '';
   }
+
+  private getAriaPressedState(active: boolean): string {
+    return active ? 'true' : 'false';
+  }
 }
 
 declare global {
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.html b/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.html
index 6e95323..aa981d6 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.html
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.html
@@ -374,6 +374,7 @@
       </template>
       <cr-icon-button id="right-chevron" class="chevron"
         on-click="onRightChevronClick"
+        on-keydown="onRightChevronKeyDown"
         iron-icon="emoji_picker:keyboard_arrow_right">
       </cr-icon-button>
     </div>
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.ts b/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.ts
index f3e2992..49d2514 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.ts
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.ts
@@ -700,6 +700,28 @@
     }
   }
 
+  private onRightChevronKeyDown(event: KeyboardEvent) {
+    // Moves focus to the first button under the group of current category if
+    // user tries to move to the next element from right chevron button in a11y
+    // mode.
+    if (event.code === 'Tab' && !event.shiftKey) {
+      const currentGroups = this.shadowRoot!
+        .querySelectorAll<EmojiGroupComponent>(
+          `emoji-group[category='${this.category}'`);
+
+      // The first group might be a history group. If the user has no history
+      // item, we should continue to check the second group.
+      for (const group of currentGroups) {
+        const button = group.firstEmojiButton();
+        if (button) {
+          button.focus();
+          event.preventDefault();
+          return;
+        }
+      }
+    }
+  }
+
   private onLeftChevronClick() {
     this.pagination = Math.max(this.pagination - 1, 1);
     this.updateCurrentGroupTabs();
diff --git a/chrome/browser/safe_browsing/client_side_detection_service_unittest.cc b/chrome/browser/safe_browsing/client_side_detection_service_unittest.cc
index 979bfea..82006a59 100644
--- a/chrome/browser/safe_browsing/client_side_detection_service_unittest.cc
+++ b/chrome/browser/safe_browsing/client_side_detection_service_unittest.cc
@@ -106,7 +106,7 @@
 
 class ClientSideDetectionServiceTest
     : public testing::Test,
-      public testing::WithParamInterface<std::tuple<bool, bool>> {
+      public testing::WithParamInterface<std::tuple<bool, bool, bool>> {
  public:
   ClientSideDetectionServiceTest()
       : profile_manager_(TestingBrowserProcess::GetGlobal()) {
@@ -125,12 +125,18 @@
           {kSafeBrowsingDailyPhishingReportsLimit, params});
     }
 
+    if (ShouldEnableImageEmbeddingModelCacao()) {
+      enabled_features.push_back({kClientSideDetectionModelImageEmbedder, {}});
+    }
+
     feature_list_.InitWithFeaturesAndParameters(enabled_features, {});
   }
   bool ShouldEnableCacao() { return get<0>(GetParam()); }
 
   bool ShouldEnableESBDailyPhishingLimit() { return get<1>(GetParam()); }
 
+  bool ShouldEnableImageEmbeddingModelCacao() { return get<2>(GetParam()); }
+
  protected:
   void SetUp() override {
     test_shared_loader_factory_ =
@@ -316,7 +322,9 @@
 
 INSTANTIATE_TEST_SUITE_P(All,
                          ClientSideDetectionServiceTest,
-                         testing::Combine(testing::Bool(), testing::Bool()));
+                         testing::Combine(testing::Bool(),
+                                          testing::Bool(),
+                                          testing::Bool()));
 
 TEST_P(ClientSideDetectionServiceTest, ServiceObjectDeletedBeforeCallbackDone) {
   csd_service_ = std::make_unique<ClientSideDetectionService>(
@@ -604,4 +612,29 @@
   EXPECT_TRUE(csd_service_->enabled());
 }
 
+TEST_P(ClientSideDetectionServiceTest,
+       TestReceivingImageEmbedderUpdatesAfterResubscription) {
+  if (!(base::FeatureList::IsEnabled(
+            kClientSideDetectionModelOptimizationGuide) &&
+        base::FeatureList::IsEnabled(kClientSideDetectionModelImageEmbedder))) {
+    return;
+  }
+
+  profile_->GetPrefs()->SetBoolean(prefs::kSafeBrowsingEnabled, true);
+  profile_->GetPrefs()->SetBoolean(prefs::kSafeBrowsingEnhanced, true);
+  csd_service_ = std::make_unique<ClientSideDetectionService>(
+      std::make_unique<ChromeClientSideDetectionServiceDelegate>(profile_),
+      model_observer_tracker_.get(), background_task_runner_);
+
+  EXPECT_TRUE(csd_service_->IsSubscribedToImageEmbeddingModelUpdates());
+
+  profile_->GetPrefs()->SetBoolean(prefs::kSafeBrowsingEnhanced, false);
+  EXPECT_TRUE(csd_service_->IsSubscribedToImageEmbeddingModelUpdates());
+  EXPECT_FALSE(csd_service_->ShouldSendImageEmbeddingModelToRenderer());
+
+  profile_->GetPrefs()->SetBoolean(prefs::kSafeBrowsingEnhanced, true);
+  EXPECT_TRUE(csd_service_->IsSubscribedToImageEmbeddingModelUpdates());
+  EXPECT_TRUE(csd_service_->ShouldSendImageEmbeddingModelToRenderer());
+}
+
 }  // namespace safe_browsing
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 94549cd..0a794e87 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -510,6 +510,7 @@
     "//chrome/browser/breadcrumbs",
     "//chrome/browser/chrome_for_testing:buildflags",
     "//chrome/browser/companion/core",
+    "//chrome/browser/companion/visual_search",
     "//chrome/browser/devtools",
     "//chrome/browser/favicon",
     "//chrome/browser/google",
@@ -2118,6 +2119,10 @@
       "../ash/app_list/app_service/app_service_app_model_builder.h",
       "../ash/app_list/app_service/app_service_context_menu.cc",
       "../ash/app_list/app_service/app_service_context_menu.h",
+      "../ash/app_list/app_service/app_service_promise_app_item.cc",
+      "../ash/app_list/app_service/app_service_promise_app_item.h",
+      "../ash/app_list/app_service/app_service_promise_app_model_builder.cc",
+      "../ash/app_list/app_service/app_service_promise_app_model_builder.h",
       "../ash/app_list/app_sync_ui_state.cc",
       "../ash/app_list/app_sync_ui_state.h",
       "../ash/app_list/app_sync_ui_state_factory.cc",
diff --git a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarPhone.java b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarPhone.java
index 804935b..db277f2 100644
--- a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarPhone.java
+++ b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarPhone.java
@@ -1124,7 +1124,10 @@
      * Updates the toolbar height and bottom padding during URL focus changing.
      */
     private void updateToolbarLayoutForUrlFocusChangeAnimation() {
-        if (!OmniboxFeatures.shouldShowModernizeVisualUpdate(getContext())) {
+        // With the smallest margins variant enabled, we still increase the height of the location
+        // bar bg but don't increase the height of the toolbar.
+        if (!OmniboxFeatures.shouldShowModernizeVisualUpdate(getContext())
+                || OmniboxFeatures.shouldShowSmallestMargins(getContext())) {
             return;
         }
 
diff --git a/chrome/browser/ui/ash/shelf/chrome_shelf_controller_unittest.cc b/chrome/browser/ui/ash/shelf/chrome_shelf_controller_unittest.cc
index 653d67c1..33704616 100644
--- a/chrome/browser/ui/ash/shelf/chrome_shelf_controller_unittest.cc
+++ b/chrome/browser/ui/ash/shelf/chrome_shelf_controller_unittest.cc
@@ -1345,7 +1345,11 @@
 class ChromeShelfControllerLacrosTest : public ChromeShelfControllerTestBase {
  public:
   ChromeShelfControllerLacrosTest() {
-    feature_list_.InitAndEnableFeature(ash::features::kLacrosSupport);
+    feature_list_.InitWithFeatures(
+        {ash::features::kLacrosSupport, ash::features::kLacrosPrimary,
+         ash::features::kLacrosOnly,
+         ash::features::kLacrosProfileMigrationForceOff},
+        {});
     crosapi::browser_util::SetProfileMigrationCompletedForTest(true);
   }
   ChromeShelfControllerLacrosTest(const ChromeShelfControllerLacrosTest&) =
@@ -1380,18 +1384,22 @@
   std::unique_ptr<user_manager::ScopedUserManager> scoped_user_manager_;
 };
 
-class ChromeShelfControllerLacrosPrimaryTest
+// TODO(hidehiko): Merge this base class into ChromeShelfControllerLacrosTest.
+class ChromeShelfControllerLacrosOnlyTest
     : public ChromeShelfControllerLacrosTest {
  public:
-  ChromeShelfControllerLacrosPrimaryTest() {
+  ChromeShelfControllerLacrosOnlyTest() {
     scoped_feature_list_.InitWithFeatures(
-        {ash::features::kLacrosSupport, ash::features::kLacrosPrimary}, {});
+        {ash::features::kLacrosSupport, ash::features::kLacrosPrimary,
+         ash::features::kLacrosOnly,
+         ash::features::kLacrosProfileMigrationForceOff},
+        {});
   }
-  ChromeShelfControllerLacrosPrimaryTest(
-      const ChromeShelfControllerLacrosPrimaryTest&) = delete;
-  ChromeShelfControllerLacrosPrimaryTest& operator=(
-      const ChromeShelfControllerLacrosPrimaryTest&) = delete;
-  ~ChromeShelfControllerLacrosPrimaryTest() override = default;
+  ChromeShelfControllerLacrosOnlyTest(
+      const ChromeShelfControllerLacrosOnlyTest&) = delete;
+  ChromeShelfControllerLacrosOnlyTest& operator=(
+      const ChromeShelfControllerLacrosOnlyTest&) = delete;
+  ~ChromeShelfControllerLacrosOnlyTest() override = default;
 
   void SetUp() override {
     ChromeShelfControllerLacrosTest::SetUp();
@@ -1830,12 +1838,12 @@
 
 TEST_F(ChromeShelfControllerLacrosTest, LacrosPinnedByDefault) {
   InitShelfController();
-  EXPECT_EQ("Chrome, Lacros", GetPinnedAppStatus());
+  EXPECT_EQ("Chrome", GetPinnedAppStatus());
 }
 
 // Checks that AppService instance is updated appropriately for one Chrome app
 // window.
-TEST_F(ChromeShelfControllerLacrosPrimaryTest, ChromeAppWindow) {
+TEST_F(ChromeShelfControllerLacrosOnlyTest, ChromeAppWindow) {
   InitShelfController();
 
   auto window = std::make_unique<aura::Window>(nullptr);
@@ -1888,7 +1896,7 @@
 
 // Checks that AppService instance is updated appropriately for multiple Chrome
 // app windows.
-TEST_F(ChromeShelfControllerLacrosPrimaryTest, ChromeAppWindows) {
+TEST_F(ChromeShelfControllerLacrosOnlyTest, ChromeAppWindows) {
   InitShelfController();
 
   auto window1 = std::make_unique<aura::Window>(nullptr);
@@ -1956,7 +1964,7 @@
 }
 
 // Regression test for crash. crbug.com/1296949
-TEST_F(ChromeShelfControllerLacrosPrimaryTest, WithoutAppService) {
+TEST_F(ChromeShelfControllerLacrosOnlyTest, WithoutAppService) {
   Profile* const controller_profile = profile()->GetOffTheRecordProfile(
       Profile::OTRProfileID::CreateUniqueForTesting(),
       /*create_if_needed=*/true);
diff --git a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_browsertest.cc b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_browsertest.cc
index 6916c91..06010bd2 100644
--- a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_browsertest.cc
+++ b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_browsertest.cc
@@ -121,6 +121,77 @@
       test_extension_dir.UnpackedPath());
 }
 
+void TestDraggableRegions(BrowserView* browser_view,
+                          gfx::Point& draggable_point,
+                          gfx::Point& non_draggable_point,
+                          gfx::Point& border_point,
+                          bool is_isolated_web_app) {
+  views::NonClientFrameView* frame_view =
+      browser_view->GetWidget()->non_client_view()->frame_view();
+
+  views::View::ConvertPointToTarget(browser_view->contents_web_view(),
+                                    frame_view, &draggable_point);
+  views::View::ConvertPointToTarget(browser_view->contents_web_view(),
+                                    frame_view, &non_draggable_point);
+
+  // Wait for draggable regions value to update. This is to avoid flakiness
+  // crbug.com/1277860.
+  while (frame_view->NonClientHitTest(draggable_point) != HTCAPTION) {
+    base::RunLoop run_loop;
+    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
+        FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
+    run_loop.Run();
+  }
+
+  // Validate that a point marked "app-region: drag" is draggable.
+  EXPECT_EQ(frame_view->NonClientHitTest(draggable_point), HTCAPTION);
+  EXPECT_FALSE(browser_view->ShouldDescendIntoChildForEventHandling(
+      browser_view->GetWidget()->GetNativeView(), draggable_point));
+
+  // Validate that a point marked "app-region: no-drag" within a draggable
+  // region is not draggable.
+  EXPECT_EQ(frame_view->NonClientHitTest(non_draggable_point), HTCLIENT);
+  EXPECT_TRUE(browser_view->ShouldDescendIntoChildForEventHandling(
+      browser_view->GetWidget()->GetNativeView(), non_draggable_point));
+
+  // Validate that a point at the border that does not intersect with the
+  // overlays is not marked as draggable.
+  EXPECT_NE(frame_view->NonClientHitTest(border_point), HTCAPTION);
+  EXPECT_TRUE(browser_view->ShouldDescendIntoChildForEventHandling(
+      browser_view->GetWidget()->GetNativeView(), border_point));
+
+  // Validate that draggable region does not clear after history.replaceState is
+  // invoked.
+  std::string kHistoryReplaceState =
+      "history.replaceState({ test: 'test' }, null);";
+  EXPECT_TRUE(
+      ExecJs(browser_view->GetActiveWebContents(), kHistoryReplaceState));
+  EXPECT_FALSE(browser_view->ShouldDescendIntoChildForEventHandling(
+      browser_view->GetWidget()->GetNativeView(), draggable_point));
+
+  // Isolated Web Apps do not support out-scope navigations.
+  if (is_isolated_web_app) {
+    return;
+  }
+
+  // Validate that the draggable region reset behaviour on navigation.
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser_view->browser(),
+                                           GURL("http://example.test/")));
+
+  // Wait for draggable regions value to update. This is to avoid flakiness
+  // crbug.com/1277860.
+  while (frame_view->NonClientHitTest(draggable_point) == HTCAPTION) {
+    base::RunLoop run_loop;
+    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
+        FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
+    run_loop.Run();
+  }
+
+  EXPECT_NE(frame_view->NonClientHitTest(draggable_point), HTCAPTION);
+  EXPECT_TRUE(browser_view->ShouldDescendIntoChildForEventHandling(
+      browser_view->GetWidget()->GetNativeView(), draggable_point));
+}
+
 }  // namespace
 
 class WebAppFrameToolbarBrowserTest
@@ -790,6 +861,20 @@
   EXPECT_EQ(frame_view()->GetMinimumSize(), gfx::Size(1, 1));
 #endif
 }
+
+IN_PROC_BROWSER_TEST_F(BorderlessIsolatedWebAppBrowserTest, DraggableRegions) {
+  InstallAndLaunchIsolatedWebApp(/*uses_borderless=*/true);
+  GrantWindowManagementPermission();
+
+  // The draggable and non-draggable regions are defined in
+  // borderless_isolated_app/styles.css.
+  gfx::Point draggable_point(100, 100), non_draggable_point(106, 106),
+      border_point(100, 1);
+  TestDraggableRegions(browser_view(), draggable_point, non_draggable_point,
+                       border_point,
+                       /*is_isolated_web_app=*/true);
+}
+
 #endif  // (BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS))
 
 class WebAppFrameToolbarBrowserTest_WindowControlsOverlay
@@ -1241,13 +1326,19 @@
   EXPECT_EQ(initial_height_value, updated_rect_list[3].GetInt());
 }
 
-// TODO(https://crbug.com/1277860): Flaky. Also enable for borderless mode when
-// fixed.
 IN_PROC_BROWSER_TEST_F(WebAppFrameToolbarBrowserTest_WindowControlsOverlay,
-                       DISABLED_WindowControlsOverlayDraggableRegions) {
+                       DraggableRegions) {
   InstallAndLaunchWebApp();
   ToggleWindowControlsOverlayAndWait();
-  helper()->TestDraggableRegions();
+
+  // The draggable and non-draggable regions are defined in kTestHTML in
+  // `WebAppFrameToolbarTestHelper::
+  //  LoadWindowControlsOverlayTestPageWithDataAndGetURL`.
+  gfx::Point draggable_point(100, 100), non_draggable_point(106, 106),
+      border_point(100, 1);
+  TestDraggableRegions(helper()->browser_view(), draggable_point,
+                       non_draggable_point, border_point,
+                       /*is_isolated_web_app=*/false);
 }
 
 IN_PROC_BROWSER_TEST_F(WebAppFrameToolbarBrowserTest_WindowControlsOverlay,
diff --git a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_test_helper.cc b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_test_helper.cc
index 8cba4bc..7a5546d 100644
--- a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_test_helper.cc
+++ b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_test_helper.cc
@@ -7,7 +7,6 @@
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/notreached.h"
-#include "base/run_loop.h"
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/views/frame/browser_non_client_frame_view.h"
@@ -183,64 +182,6 @@
              "}"));
 }
 
-// TODO(https://crbug.com/1277860): Flaky.
-void WebAppFrameToolbarTestHelper::TestDraggableRegions() {
-  views::NonClientFrameView* frame_view =
-      browser_view()->GetWidget()->non_client_view()->frame_view();
-
-  // Draggable regions take some time to initialize after opening and tests fail
-  // if not exhausting the run loop before checking the value.
-  base::RunLoop run_loop;
-  run_loop.RunUntilIdle();
-
-  // Validate that a point marked "app-region: drag" is draggable. The draggable
-  // region is defined in the kTestHTML of WebAppFrameToolbarTestHelper's
-  // LoadWindowControlsOverlayTestPageWithDataAndGetURL.
-  gfx::Point draggable_point(100, 100);
-  views::View::ConvertPointToTarget(browser_view()->contents_web_view(),
-                                    frame_view, &draggable_point);
-
-  EXPECT_EQ(frame_view->NonClientHitTest(draggable_point), HTCAPTION);
-
-  EXPECT_FALSE(browser_view()->ShouldDescendIntoChildForEventHandling(
-      browser_view()->GetWidget()->GetNativeView(), draggable_point));
-
-  // Validate that a point marked "app-region: no-drag" within a draggable
-  // region is not draggable.
-  gfx::Point non_draggable_point(106, 106);
-  views::View::ConvertPointToTarget(browser_view()->contents_web_view(),
-                                    frame_view, &non_draggable_point);
-
-  EXPECT_EQ(frame_view->NonClientHitTest(non_draggable_point), HTCLIENT);
-
-  EXPECT_TRUE(browser_view()->ShouldDescendIntoChildForEventHandling(
-      browser_view()->GetWidget()->GetNativeView(), non_draggable_point));
-
-  // Validate that a point at the border that does not intersect with the
-  // overlays is not marked as draggable.
-  constexpr gfx::Point kBorderPoint(100, 1);
-  EXPECT_NE(frame_view->NonClientHitTest(kBorderPoint), HTCAPTION);
-  EXPECT_TRUE(browser_view()->ShouldDescendIntoChildForEventHandling(
-      browser_view()->GetWidget()->GetNativeView(), kBorderPoint));
-
-  // Validate that draggable region does not clear after history.replaceState is
-  // invoked.
-  std::string kHistoryReplaceState =
-      "history.replaceState({ test: 'test' }, null);";
-  EXPECT_TRUE(
-      ExecJs(browser_view()->GetActiveWebContents(), kHistoryReplaceState));
-  EXPECT_FALSE(browser_view()->ShouldDescendIntoChildForEventHandling(
-      browser_view()->GetWidget()->GetNativeView(), draggable_point));
-
-  // Validate that the draggable region is reset on navigation and the point is
-  // no longer draggable.
-  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser_view()->browser(),
-                                           GURL("http://example.test/")));
-  EXPECT_NE(frame_view->NonClientHitTest(draggable_point), HTCAPTION);
-  EXPECT_TRUE(browser_view()->ShouldDescendIntoChildForEventHandling(
-      browser_view()->GetWidget()->GetNativeView(), draggable_point));
-}
-
 BrowserView* WebAppFrameToolbarTestHelper::OpenPopup(
     const std::string& window_open_script) {
   content::ExecuteScriptAsync(browser_view_->GetActiveWebContents(),
diff --git a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_test_helper.h b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_test_helper.h
index b719837..7c088ac2 100644
--- a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_test_helper.h
+++ b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_test_helper.h
@@ -66,8 +66,6 @@
   // Add window-controls-overlay's ongeometrychange callback into the document.
   void SetupGeometryChangeCallback(content::WebContents* web_contents);
 
-  void TestDraggableRegions();
-
   // Opens a new popup window from |app_browser_| by running
   // |window_open_script| and returns the |BrowserView| it opened in.
   BrowserView* OpenPopup(const std::string& window_open_script);
diff --git a/chrome/browser/ui/webui/side_panel/companion/companion_page_handler.cc b/chrome/browser/ui/webui/side_panel/companion/companion_page_handler.cc
index d6d1e567..c4e0ac7 100644
--- a/chrome/browser/ui/webui/side_panel/companion/companion_page_handler.cc
+++ b/chrome/browser/ui/webui/side_panel/companion/companion_page_handler.cc
@@ -12,6 +12,8 @@
 #include "chrome/browser/companion/core/promo_handler.h"
 #include "chrome/browser/companion/text_finder/text_finder_manager.h"
 #include "chrome/browser/companion/text_finder/text_highlighter_manager.h"
+#include "chrome/browser/companion/visual_search/features.h"
+#include "chrome/browser/companion/visual_search/visual_search_suggestions_service_factory.h"
 #include "chrome/browser/feature_engagement/tracker_factory.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/search/search.h"
@@ -61,6 +63,13 @@
   identity_manager_observation_.Observe(
       IdentityManagerFactory::GetForProfile(GetProfile()));
   consent_helper_observation_.Observe(consent_helper_.get());
+  if (base::FeatureList::IsEnabled(
+          visual_search::features::kVisualSearchSuggestions)) {
+    visual_search_host_ =
+        std::make_unique<visual_search::VisualSearchClassifierHost>(
+            visual_search::VisualSearchSuggestionsServiceFactory::GetForProfile(
+                GetProfile()));
+  }
 }
 
 CompanionPageHandler::~CompanionPageHandler() {
@@ -128,6 +137,22 @@
   NotifyURLChanged(/*is_full_reload=*/false);
 }
 
+void CompanionPageHandler::DidFinishLoad(
+    content::RenderFrameHost* render_frame_host,
+    const GURL& validated_url) {
+  // We only want to classify images in the main frame.
+  if (!render_frame_host->IsInPrimaryMainFrame()) {
+    return;
+  }
+
+  // TODO(b/284640445) - Add browser test to verify side effect of feature
+  // on/off, use histogram check to determine whether or not classification was
+  // called.
+  if (visual_search_host_) {
+    visual_search_host_->StartClassification(render_frame_host, validated_url);
+  }
+}
+
 void CompanionPageHandler::ShowUI() {
   if (auto embedder = companion_untrusted_ui_->embedder()) {
     embedder->ShowUI();
diff --git a/chrome/browser/ui/webui/side_panel/companion/companion_page_handler.h b/chrome/browser/ui/webui/side_panel/companion/companion_page_handler.h
index 5409321..64713f8 100644
--- a/chrome/browser/ui/webui/side_panel/companion/companion_page_handler.h
+++ b/chrome/browser/ui/webui/side_panel/companion/companion_page_handler.h
@@ -10,6 +10,7 @@
 #include "base/scoped_observation.h"
 #include "chrome/browser/companion/core/constants.h"
 #include "chrome/browser/companion/core/mojom/companion.mojom.h"
+#include "chrome/browser/companion/visual_search/visual_search_classifier_host.h"
 #include "chrome/browser/ui/side_panel/side_panel_enums.h"
 #include "components/lens/buildflags.h"
 #include "components/signin/public/identity_manager/identity_manager.h"
@@ -72,6 +73,8 @@
   // IdentityManager::Observer overrides.
   void OnPrimaryAccountChanged(
       const signin::PrimaryAccountChangeEvent& event) override;
+  void DidFinishLoad(content::RenderFrameHost* render_frame_host,
+                     const GURL& validated_url) override;
 
   // UrlKeyedDataCollectionConsentHelper::Observer overrides.
   void OnUrlKeyedDataCollectionConsentStateChanged(
@@ -110,6 +113,10 @@
   std::unique_ptr<unified_consent::UrlKeyedDataCollectionConsentHelper>
       consent_helper_;
 
+  // Owns the orchestrator for visual search suggestions.
+  std::unique_ptr<visual_search::VisualSearchClassifierHost>
+      visual_search_host_;
+
   // Logs metrics for companion page. Reset when there is a new navigation.
   std::unique_ptr<CompanionMetricsLogger> metrics_logger_;
 
diff --git a/chrome/browser/web_applications/BUILD.gn b/chrome/browser/web_applications/BUILD.gn
index b45f448..660e8f6 100644
--- a/chrome/browser/web_applications/BUILD.gn
+++ b/chrome/browser/web_applications/BUILD.gn
@@ -183,10 +183,16 @@
     "preinstalled_web_apps/preinstalled_web_app_definition_utils.h",
     "preinstalled_web_apps/preinstalled_web_apps.cc",
     "preinstalled_web_apps/preinstalled_web_apps.h",
-    "remove_web_app_job.cc",
-    "remove_web_app_job.h",
     "scope_extension_info.cc",
     "scope_extension_info.h",
+    "uninstall/remove_install_source_job.cc",
+    "uninstall/remove_install_source_job.h",
+    "uninstall/remove_install_url_job.cc",
+    "uninstall/remove_install_url_job.h",
+    "uninstall/remove_web_app_job.cc",
+    "uninstall/remove_web_app_job.h",
+    "uninstall/uninstall_job.cc",
+    "uninstall/uninstall_job.h",
     "user_display_mode.cc",
     "user_display_mode.h",
     "user_uninstalled_preinstalled_web_app_prefs.cc",
diff --git a/chrome/browser/web_applications/commands/web_app_uninstall_command.cc b/chrome/browser/web_applications/commands/web_app_uninstall_command.cc
index 70c6fc6..2238544 100644
--- a/chrome/browser/web_applications/commands/web_app_uninstall_command.cc
+++ b/chrome/browser/web_applications/commands/web_app_uninstall_command.cc
@@ -5,147 +5,39 @@
 #include "chrome/browser/web_applications/commands/web_app_uninstall_command.h"
 
 #include <memory>
-#include <sstream>
 #include <utility>
 
-#include "base/containers/contains.h"
+#include "base/check.h"
 #include "base/functional/bind.h"
 #include "base/metrics/histogram_functions.h"
-#include "base/strings/to_string.h"
-#include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/web_applications/locks/all_apps_lock.h"
-#include "chrome/browser/web_applications/remove_web_app_job.h"
-#include "chrome/browser/web_applications/user_uninstalled_preinstalled_web_app_prefs.h"
-#include "chrome/browser/web_applications/web_app.h"
-#include "chrome/browser/web_applications/web_app_constants.h"
-#include "chrome/browser/web_applications/web_app_id.h"
-#include "chrome/browser/web_applications/web_app_install_manager.h"
-#include "chrome/browser/web_applications/web_app_install_utils.h"
-#include "chrome/browser/web_applications/web_app_registrar.h"
-#include "chrome/browser/web_applications/web_app_registry_update.h"
+#include "chrome/browser/web_applications/uninstall/uninstall_job.h"
 #include "components/webapps/browser/installable/installable_metrics.h"
 #include "components/webapps/browser/uninstall_result_code.h"
 
 namespace web_app {
 
-namespace {
-
-bool CanUninstallAllManagementSources(
-    webapps::WebappUninstallSource uninstall_source) {
-  // Check that the source was from a known 'user' or allowed ones such
-  // as kMigration.
-  return uninstall_source == webapps::WebappUninstallSource::kUnknown ||
-         uninstall_source == webapps::WebappUninstallSource::kAppMenu ||
-         uninstall_source == webapps::WebappUninstallSource::kAppsPage ||
-         uninstall_source == webapps::WebappUninstallSource::kOsSettings ||
-         uninstall_source == webapps::WebappUninstallSource::kAppManagement ||
-         uninstall_source == webapps::WebappUninstallSource::kMigration ||
-         uninstall_source == webapps::WebappUninstallSource::kAppList ||
-         uninstall_source == webapps::WebappUninstallSource::kShelf ||
-         uninstall_source == webapps::WebappUninstallSource::kSync ||
-         uninstall_source == webapps::WebappUninstallSource::kStartupCleanup ||
-         uninstall_source == webapps::WebappUninstallSource::kTestCleanup;
-}
-
-}  // namespace
-
 WebAppUninstallCommand::WebAppUninstallCommand(
-    const AppId& app_id,
-    absl::optional<WebAppManagement::Type> management_type_or_all,
-    webapps::WebappUninstallSource uninstall_source,
-    UninstallWebAppCallback callback,
-    Profile& profile)
+    std::unique_ptr<UninstallJob> job,
+    UninstallJob::Callback callback)
     : WebAppCommandTemplate<AllAppsLock>("WebAppUninstallCommand"),
       lock_description_(std::make_unique<AllAppsLockDescription>()),
-      app_id_(app_id),
-      callback_(std::move(callback)),
-      profile_(profile) {
-  // Initializing data for uninstallation tracking.
-  queued_uninstalls_.emplace_back(app_id_, management_type_or_all,
-                                  uninstall_source);
-
-  webapps::InstallableMetrics::TrackUninstallEvent(uninstall_source);
+      job_(std::move(job)),
+      callback_(std::move(callback)) {
+  webapps::InstallableMetrics::TrackUninstallEvent(job_->uninstall_source());
 }
 
 WebAppUninstallCommand::~WebAppUninstallCommand() = default;
 
 void WebAppUninstallCommand::StartWithLock(std::unique_ptr<AllAppsLock> lock) {
   lock_ = std::move(lock);
-
-  while (!queued_uninstalls_.empty()) {
-    DCHECK(!all_uninstalled_queued_);
-    const UninstallInfo current_uninstall =
-        std::move(queued_uninstalls_.back());
-    queued_uninstalls_.pop_back();
-    AppendUninstallInfoToDebugLog(current_uninstall);
-
-    const AppId& app_id = current_uninstall.app_id;
-    const WebApp* app = lock_->registrar().GetAppById(app_id);
-    if (!app) {
-      uninstall_results_[app_id] =
-          webapps::UninstallResultCode::kNoAppToUninstall;
-      AppendUninstallResultsToDebugLog(app_id);
-      continue;
-    }
-
-    // This contains the external uninstall logic.
-    if (current_uninstall.management_type_or_all.has_value()) {
-      const WebAppManagement::Type source =
-          current_uninstall.management_type_or_all.value();
-      // If there is more than a single source, then we can just remove the
-      // source from the web_app DB. Else we end up calling Uninstall() at the
-      // end.
-      if (!app->HasOnlySource(source)) {
-        // There is a chance that removed source type is NOT user uninstallable
-        // but the remaining source (after removal) types are user
-        // uninstallable. In this case, the following call will register os
-        // uninstallation.
-        apps_pending_uninstall_[app_id] = nullptr;
-        MaybeRegisterOsUninstall(
-            app, source, lock_->os_integration_manager(),
-            base::BindOnce(&WebAppUninstallCommand::
-                               RemoveManagementTypeAfterOsUninstallRegistration,
-                           weak_factory_.GetWeakPtr(), app_id, source,
-                           current_uninstall.uninstall_source));
-      } else {
-        Uninstall(app_id, current_uninstall.uninstall_source);
-      }
-    } else {
-      // This contains the user uninstall and sync uninstall logic.
-      DCHECK(
-          CanUninstallAllManagementSources(current_uninstall.uninstall_source));
-      // The following DCHECK streamlines the user uninstall and sync uninstall
-      // flow, because for sync uninstalls, the web_app source is removed before
-      // being synced, so the first condition fails by the time an Uninstall is
-      // invoked.
-      DCHECK(app->CanUserUninstallWebApp() ||
-             current_uninstall.uninstall_source ==
-                 webapps::WebappUninstallSource::kSync);
-      if (app->IsPreinstalledApp()) {
-        // Update the default uninstalled web_app prefs if it is a preinstalled
-        // app but being removed by user.
-        const WebApp::ExternalConfigMap& config_map =
-            app->management_to_external_config_map();
-        auto it = config_map.find(WebAppManagement::kDefault);
-        if (it != config_map.end()) {
-          UserUninstalledPreinstalledWebAppPrefs(profile_->GetPrefs())
-              .Add(app_id, it->second.install_urls);
-        } else {
-          base::UmaHistogramBoolean(
-              "WebApp.Preinstalled.ExternalConfigMapAbsentDuringUninstall",
-              true);
-        }
-      }
-      Uninstall(app_id, current_uninstall.uninstall_source);
-    }
-  }
-  all_uninstalled_queued_ = true;
-  MaybeFinishUninstallAndDestruct();
+  job_->Start(*lock_,
+              base::BindOnce(&WebAppUninstallCommand::CompleteAndSelfDestruct,
+                             weak_factory_.GetWeakPtr()));
 }
 
 void WebAppUninstallCommand::OnShutdown() {
-  Abort(webapps::UninstallResultCode::kError);
-  return;
+  CompleteAndSelfDestruct(webapps::UninstallResultCode::kShutdown);
 }
 
 const LockDescription& WebAppUninstallCommand::lock_description() const {
@@ -153,122 +45,28 @@
 }
 
 base::Value WebAppUninstallCommand::ToDebugValue() const {
-  base::Value::Dict uninstall_info;
-  uninstall_info.Set("command_data", debug_log_.Clone());
-  return base::Value(std::move(uninstall_info));
+  return job_->ToDebugValue();
 }
 
-WebAppUninstallCommand::UninstallInfo::UninstallInfo(
-    AppId app_id,
-    absl::optional<WebAppManagement::Type> management_type_or_all,
-    webapps::WebappUninstallSource uninstall_source)
-    : app_id(app_id),
-      management_type_or_all(management_type_or_all),
-      uninstall_source(uninstall_source) {}
-
-WebAppUninstallCommand::UninstallInfo::~UninstallInfo() = default;
-
-WebAppUninstallCommand::UninstallInfo::UninstallInfo(
-    const UninstallInfo& uninstall_info) = default;
-
-WebAppUninstallCommand::UninstallInfo::UninstallInfo(
-    UninstallInfo&& uninstall_info) = default;
-
-void WebAppUninstallCommand::AppendUninstallInfoToDebugLog(
-    const UninstallInfo& uninstall_info) {
-  base::Value::Dict source_info;
-  if (uninstall_info.management_type_or_all.has_value()) {
-    source_info.Set(
-        "management_type",
-        base::ToString(uninstall_info.management_type_or_all.value()));
-  }
-  source_info.Set("uninstall_source", ConvertUninstallSourceToStringType(
-                                          uninstall_info.uninstall_source));
-  debug_log_.Set(uninstall_info.app_id, base::Value(std::move(source_info)));
-}
-
-void WebAppUninstallCommand::AppendUninstallResultsToDebugLog(
-    const AppId& app_id) {
-  base::Value::Dict* app_dict = debug_log_.FindDict(app_id);
-  DCHECK(app_dict);
-  app_dict->Set("uninstall_result", webapps::ConvertUninstallResultCodeToString(
-                                        uninstall_results_[app_id]));
-}
-
-void WebAppUninstallCommand::Abort(webapps::UninstallResultCode code) {
+void WebAppUninstallCommand::CompleteAndSelfDestruct(
+    webapps::UninstallResultCode code) {
+  CHECK(callback_);
   base::UmaHistogramBoolean("WebApp.Uninstall.Result",
-                            (code == webapps::UninstallResultCode::kSuccess));
-  if (!callback_)
-    return;
-  SignalCompletionAndSelfDestruct(CommandResult::kFailure,
-                                  base::BindOnce(std::move(callback_), code));
-}
-
-void WebAppUninstallCommand::Uninstall(
-    const AppId& app_id,
-    webapps::WebappUninstallSource uninstall_source) {
-  QueueSubAppsForUninstallIfAny(app_id);
-  apps_pending_uninstall_[app_id] = RemoveWebAppJob::Start(
-      uninstall_source, app_id, *lock_, profile_.get(),
-      base::BindOnce(&WebAppUninstallCommand::OnSingleUninstallComplete,
-                     weak_factory_.GetWeakPtr(), app_id, uninstall_source));
-}
-
-void WebAppUninstallCommand::QueueSubAppsForUninstallIfAny(
-    const AppId& app_id) {
-  std::vector<AppId> sub_app_ids = lock_->registrar().GetAllSubAppIds(app_id);
-  for (const AppId& sub_app_id : sub_app_ids) {
-    queued_uninstalls_.emplace_back(
-        sub_app_id, WebAppManagement::kSubApp,
-        webapps::WebappUninstallSource::kParentUninstall);
-  }
-}
-
-void WebAppUninstallCommand::RemoveManagementTypeAfterOsUninstallRegistration(
-    const AppId& app_id,
-    const WebAppManagement::Type& management_type,
-    webapps::WebappUninstallSource uninstall_source,
-    OsHooksErrors os_hooks_errors) {
-  {
-    ScopedRegistryUpdate update(&lock_->sync_bridge());
-    WebApp* app_to_update = update->UpdateApp(app_id);
-    app_to_update->RemoveSource(management_type);
-    if (management_type == WebAppManagement::kSubApp) {
-      app_to_update->SetParentAppId(absl::nullopt);
-    }
-  }
-
-  lock_->install_manager().NotifyWebAppSourceRemoved(app_id);
-
-  // Registering an OS uninstall is also an "uninstall", so the
-  // state is updated for the command.
-  OnSingleUninstallComplete(app_id, uninstall_source,
-                            /*success=*/true);
-}
-
-void WebAppUninstallCommand::OnSingleUninstallComplete(
-    const AppId& app_id,
-    webapps::WebappUninstallSource source,
-    bool success) {
-  DCHECK(base::Contains(apps_pending_uninstall_, app_id));
-  apps_pending_uninstall_.erase(app_id);
-
-  if (source == webapps::WebappUninstallSource::kSync) {
-    base::UmaHistogramBoolean("Webapp.SyncInitiatedUninstallResult", success);
-  }
-  uninstall_results_[app_id] = success ? webapps::UninstallResultCode::kSuccess
-                                       : webapps::UninstallResultCode::kError;
-  AppendUninstallResultsToDebugLog(app_id);
-  MaybeFinishUninstallAndDestruct();
-}
-
-void WebAppUninstallCommand::MaybeFinishUninstallAndDestruct() {
-  if (apps_pending_uninstall_.empty() && all_uninstalled_queued_) {
-    // All uninstall jobs have finished.
-    SignalCompletionAndSelfDestruct(
-        CommandResult::kSuccess,
-        base::BindOnce(std::move(callback_), uninstall_results_[app_id_]));
-  }
+                            UninstallSucceeded(code));
+  SignalCompletionAndSelfDestruct(
+      [code]() {
+        switch (code) {
+          case webapps::UninstallResultCode::kSuccess:
+          case webapps::UninstallResultCode::kNoAppToUninstall:
+            return CommandResult::kSuccess;
+          case webapps::UninstallResultCode::kCancelled:
+          case webapps::UninstallResultCode::kError:
+            return CommandResult::kFailure;
+          case webapps::UninstallResultCode::kShutdown:
+            return CommandResult::kShutdown;
+        }
+      }(),
+      base::BindOnce(std::move(callback_), code));
 }
 
 }  // namespace web_app
diff --git a/chrome/browser/web_applications/commands/web_app_uninstall_command.h b/chrome/browser/web_applications/commands/web_app_uninstall_command.h
index c11d8f5..1fca1933 100644
--- a/chrome/browser/web_applications/commands/web_app_uninstall_command.h
+++ b/chrome/browser/web_applications/commands/web_app_uninstall_command.h
@@ -7,24 +7,13 @@
 
 #include <memory>
 
-#include "base/containers/circular_deque.h"
-#include "base/containers/flat_map.h"
-#include "base/functional/callback_forward.h"
-#include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "base/values.h"
 #include "chrome/browser/web_applications/commands/web_app_command.h"
-#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
-#include "chrome/browser/web_applications/web_app_id.h"
-#include "components/webapps/browser/installable/installable_metrics.h"
-#include "components/webapps/browser/uninstall_result_code.h"
-#include "third_party/abseil-cpp/absl/types/optional.h"
-
-class Profile;
+#include "chrome/browser/web_applications/uninstall/uninstall_job.h"
 
 namespace webapps {
 enum class UninstallResultCode;
-enum class WebappUninstallSource;
 }  // namespace webapps
 
 namespace web_app {
@@ -32,45 +21,13 @@
 class AllAppsLock;
 class AllAppsLockDescription;
 class LockDescription;
-class RemoveWebAppJob;
 
-// This command is used to uninstall a web_app. Once started, this command will:
-// 1. Start maintaining a queue of all app_ids that need to be uninstalled.
-// 2. For each app_id:
-//     a. If an app was triggered by an external install source (like policy,
-//     default or system) and was installed by multiple sources, it will
-//     remove the source that triggered the uninstallation. Sometimes doing so
-//     might change the behavior of an app from being user uninstallable to not
-//     being user uninstallable in which case, OS integration for user
-//     uninstallation is unregistered.
-//     b. If an app uninstallation was triggered by any other source and if the
-//     app was a default app, the app_id is stored in the
-//     UserUninstalledPreinstalledWebAppPrefs so that the
-//     ExternallyManagedAppManager does not auto synchronize and reinstall the
-//     default app on next step. See
-//     `ExternallyManagedAppManager::SynchronizeInstalledApps()` for more info.
-//     c. If the app being uninstalled is a parent app with multiple sub apps,
-//     all sub app IDs are queued onto the overall uninstallation queue.
-// 3. For all other use-cases, a RemoveWebAppJob is initialized and kicked
-//    off per app_id. The job is owned by the command, and the command keeps
-//    track of all currently running jobs.
-// 4. The command ends only when both of the conditions below are successful:
-//    a. All running RemoveWebAppJobs have been completed.
-//    b. The queue that was keeping track of app_ids that needed to be
-//    uninstalled is empty.
+// This command acquires the AllAppsLock needed by three uninstall related jobs:
+// `RemoveInstallUrlJob`, `RemoveInstallSourceJob`, and `RemoveWebAppJob`.
 class WebAppUninstallCommand : public WebAppCommandTemplate<AllAppsLock> {
  public:
-  using UninstallWebAppCallback =
-      base::OnceCallback<void(webapps::UninstallResultCode)>;
-  using RemoveManagementTypeCallback =
-      base::RepeatingCallback<void(const AppId& app_id)>;
-
-  WebAppUninstallCommand(
-      const AppId& app_id,
-      absl::optional<WebAppManagement::Type> management_type_or_all,
-      webapps::WebappUninstallSource uninstall_source,
-      UninstallWebAppCallback callback,
-      Profile& profile);
+  WebAppUninstallCommand(std::unique_ptr<UninstallJob> job,
+                         UninstallJob::Callback callback);
   ~WebAppUninstallCommand() override;
 
   // WebAppCommandTemplate<AllAppsLock>:
@@ -80,53 +37,13 @@
   base::Value ToDebugValue() const override;
 
  private:
-  // Used to store information needed for uninstalling an app with app_id.
-  struct UninstallInfo {
-    UninstallInfo(AppId app_id,
-                  absl::optional<WebAppManagement::Type> management_type_or_all,
-                  webapps::WebappUninstallSource uninstall_source);
-    ~UninstallInfo();
-    UninstallInfo(const UninstallInfo& uninstall_info);
-    UninstallInfo(UninstallInfo&& uninstall_info);
-    UninstallInfo& operator=(const UninstallInfo& uninstall_info) = delete;
-    UninstallInfo& operator=(UninstallInfo&& uninstall_info) = delete;
-
-    AppId app_id;
-    absl::optional<WebAppManagement::Type> management_type_or_all;
-    webapps::WebappUninstallSource uninstall_source;
-  };
-
-  void AppendUninstallInfoToDebugLog(const UninstallInfo& uninstall_info);
-  void AppendUninstallResultsToDebugLog(const AppId& app_id);
-  void Abort(webapps::UninstallResultCode code);
-  void Uninstall(const AppId& app_id,
-                 webapps::WebappUninstallSource uninstall_source);
-  void QueueSubAppsForUninstallIfAny(const AppId& app_id);
-  void RemoveManagementTypeAfterOsUninstallRegistration(
-      const AppId& app_id,
-      const WebAppManagement::Type& install_source,
-      webapps::WebappUninstallSource uninstall_source,
-      OsHooksErrors os_hooks_errors);
-  void OnSingleUninstallComplete(const AppId& app_id,
-                                 webapps::WebappUninstallSource source,
-                                 bool success);
-  void MaybeFinishUninstallAndDestruct();
+  void CompleteAndSelfDestruct(webapps::UninstallResultCode code);
 
   std::unique_ptr<AllAppsLockDescription> lock_description_;
   std::unique_ptr<AllAppsLock> lock_;
 
-  const AppId app_id_;
-  base::circular_deque<UninstallInfo> queued_uninstalls_;
-  base::flat_map<AppId, webapps::UninstallResultCode> uninstall_results_;
-  base::flat_map<AppId, std::unique_ptr<RemoveWebAppJob>>
-      apps_pending_uninstall_;
-  base::Value::Dict debug_log_;
-  bool all_uninstalled_queued_ = false;
-
-  UninstallWebAppCallback callback_;
-
-  // `this` is owned by `profile_`.
-  raw_ref<Profile> profile_;
+  std::unique_ptr<UninstallJob> job_;
+  UninstallJob::Callback callback_;
 
   base::WeakPtrFactory<WebAppUninstallCommand> weak_factory_{this};
 };
diff --git a/chrome/browser/web_applications/commands/web_app_uninstall_command_unittest.cc b/chrome/browser/web_applications/commands/web_app_uninstall_command_unittest.cc
index 04fc5f8..c934966 100644
--- a/chrome/browser/web_applications/commands/web_app_uninstall_command_unittest.cc
+++ b/chrome/browser/web_applications/commands/web_app_uninstall_command_unittest.cc
@@ -21,6 +21,9 @@
 #include "chrome/browser/web_applications/test/web_app_test.h"
 #include "chrome/browser/web_applications/test/web_app_test_observers.h"
 #include "chrome/browser/web_applications/test/web_app_test_utils.h"
+#include "chrome/browser/web_applications/uninstall/remove_install_source_job.h"
+#include "chrome/browser/web_applications/uninstall/remove_install_url_job.h"
+#include "chrome/browser/web_applications/uninstall/remove_web_app_job.h"
 #include "chrome/browser/web_applications/user_uninstalled_preinstalled_web_app_prefs.h"
 #include "chrome/browser/web_applications/web_app.h"
 #include "chrome/browser/web_applications/web_app_command_manager.h"
@@ -101,12 +104,12 @@
   base::RunLoop loop;
   provider()->command_manager().ScheduleCommand(
       std::make_unique<WebAppUninstallCommand>(
-          app_id, absl::nullopt, webapps::WebappUninstallSource::kAppMenu,
+          std::make_unique<RemoveWebAppJob>(
+              webapps::WebappUninstallSource::kAppMenu, *profile(), app_id),
           base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
             EXPECT_EQ(webapps::UninstallResultCode::kSuccess, code);
             loop.Quit();
-          }),
-          *profile()));
+          })));
 
   loop.Run();
   EXPECT_EQ(provider()->registrar_unsafe().GetAppById(app_id), nullptr);
@@ -137,13 +140,13 @@
   base::RunLoop loop;
   provider()->command_manager().ScheduleCommand(
       std::make_unique<WebAppUninstallCommand>(
-          app_id, WebAppManagement::kDefault,
-          webapps::WebappUninstallSource::kAppMenu,
+          std::make_unique<RemoveInstallSourceJob>(
+              webapps::WebappUninstallSource::kAppMenu, *profile(), app_id,
+              WebAppManagement::kDefault),
           base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
             EXPECT_EQ(webapps::UninstallResultCode::kSuccess, code);
             loop.Quit();
-          }),
-          *profile()));
+          })));
 
   loop.Run();
   EXPECT_EQ(provider()->registrar_unsafe().GetAppById(app_id), nullptr);
@@ -174,12 +177,12 @@
   base::RunLoop loop;
   provider()->command_manager().ScheduleCommand(
       std::make_unique<WebAppUninstallCommand>(
-          app_id, absl::nullopt, webapps::WebappUninstallSource::kAppMenu,
+          std::make_unique<RemoveWebAppJob>(
+              webapps::WebappUninstallSource::kAppMenu, *profile(), app_id),
           base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
             EXPECT_EQ(webapps::UninstallResultCode::kError, code);
             loop.Quit();
-          }),
-          *profile()));
+          })));
 
   loop.Run();
   EXPECT_EQ(provider()->registrar_unsafe().GetAppById(app_id), nullptr);
@@ -211,12 +214,12 @@
   base::RunLoop loop;
   provider()->command_manager().ScheduleCommand(
       std::make_unique<WebAppUninstallCommand>(
-          app_id, absl::nullopt, webapps::WebappUninstallSource::kAppMenu,
+          std::make_unique<RemoveWebAppJob>(
+              webapps::WebappUninstallSource::kAppMenu, *profile(), app_id),
           base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
             EXPECT_EQ(webapps::UninstallResultCode::kError, code);
             loop.Quit();
-          }),
-          *profile()));
+          })));
 
   loop.Run();
   EXPECT_EQ(provider()->registrar_unsafe().GetAppById(app_id), nullptr);
@@ -242,12 +245,12 @@
   base::RunLoop loop;
   provider()->command_manager().ScheduleCommand(
       std::make_unique<WebAppUninstallCommand>(
-          app_id, absl::nullopt, webapps::WebappUninstallSource::kAppMenu,
+          std::make_unique<RemoveWebAppJob>(
+              webapps::WebappUninstallSource::kAppMenu, *profile(), app_id),
           base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
             EXPECT_EQ(webapps::UninstallResultCode::kNoAppToUninstall, code);
             loop.Quit();
-          }),
-          *profile()));
+          })));
 
   loop.Run();
   EXPECT_EQ(provider()->registrar_unsafe().GetAppById(app_id), nullptr);
@@ -276,11 +279,11 @@
 
   provider()->command_manager().ScheduleCommand(
       std::make_unique<WebAppUninstallCommand>(
-          app_id, absl::nullopt, webapps::WebappUninstallSource::kAppMenu,
+          std::make_unique<RemoveWebAppJob>(
+              webapps::WebappUninstallSource::kAppMenu, *profile(), app_id),
           base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
             EXPECT_EQ(webapps::UninstallResultCode::kError, code);
-          }),
-          *profile()));
+          })));
 
   provider()->command_manager().Shutdown();
   // App is not uninstalled.
@@ -316,12 +319,12 @@
   base::RunLoop loop;
   provider()->command_manager().ScheduleCommand(
       std::make_unique<WebAppUninstallCommand>(
-          app_id, absl::nullopt, webapps::WebappUninstallSource::kAppMenu,
+          std::make_unique<RemoveWebAppJob>(
+              webapps::WebappUninstallSource::kAppMenu, *profile(), app_id),
           base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
             EXPECT_EQ(webapps::UninstallResultCode::kSuccess, code);
             loop.Quit();
-          }),
-          *profile()));
+          })));
 
   loop.Run();
   EXPECT_EQ(provider()->registrar_unsafe().GetAppById(app_id), nullptr);
@@ -354,12 +357,12 @@
   base::RunLoop loop;
   provider()->command_manager().ScheduleCommand(
       std::make_unique<WebAppUninstallCommand>(
-          app_id, absl::nullopt, webapps::WebappUninstallSource::kAppMenu,
+          std::make_unique<RemoveWebAppJob>(
+              webapps::WebappUninstallSource::kAppMenu, *profile(), app_id),
           base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
             EXPECT_EQ(webapps::UninstallResultCode::kSuccess, code);
             loop.Quit();
-          }),
-          *profile()));
+          })));
 
   loop.Run();
   EXPECT_EQ(provider()->registrar_unsafe().GetAppById(app_id), nullptr);
@@ -417,13 +420,13 @@
 
   base::RunLoop run_loop;
   auto command = std::make_unique<WebAppUninstallCommand>(
-      app_id, WebAppManagement::kPolicy,
-      webapps::WebappUninstallSource::kExternalPolicy,
+      std::make_unique<RemoveInstallSourceJob>(
+          webapps::WebappUninstallSource::kExternalPolicy, *profile(), app_id,
+          WebAppManagement::kPolicy),
       base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
         EXPECT_EQ(webapps::UninstallResultCode::kSuccess, code);
         run_loop.Quit();
-      }),
-      *profile());
+      }));
 
   WebAppInstallManagerObserverAdapter observer(profile());
   observer.SetWebAppSourceRemovedDelegate(
@@ -479,12 +482,12 @@
   base::RunLoop loop;
   provider()->command_manager().ScheduleCommand(
       std::make_unique<WebAppUninstallCommand>(
-          app_id, absl::nullopt, GetParam().source,
+          std::make_unique<RemoveWebAppJob>(GetParam().source, *profile(),
+                                            app_id),
           base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
             EXPECT_EQ(webapps::UninstallResultCode::kSuccess, code);
             loop.Quit();
-          }),
-          *profile()));
+          })));
 
   loop.Run();
   EXPECT_EQ(provider()->registrar_unsafe().GetAppById(app_id), nullptr);
diff --git a/chrome/browser/web_applications/extensions/externally_managed_app_install_task_unittest.cc b/chrome/browser/web_applications/extensions/externally_managed_app_install_task_unittest.cc
index 32098093..40f17dd5 100644
--- a/chrome/browser/web_applications/extensions/externally_managed_app_install_task_unittest.cc
+++ b/chrome/browser/web_applications/extensions/externally_managed_app_install_task_unittest.cc
@@ -4,55 +4,55 @@
 
 #include "chrome/browser/web_applications/externally_managed_app_install_task.h"
 
+#include <stddef.h>
+#include <initializer_list>
 #include <map>
 #include <memory>
 #include <string>
+#include <tuple>
+#include <type_traits>
 #include <utility>
 #include <vector>
 
+#include "base/check.h"
 #include "base/check_op.h"
 #include "base/containers/contains.h"
-#include "base/containers/flat_set.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "base/functional/callback_helpers.h"
+#include "base/location.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/scoped_refptr.h"
 #include "base/notreached.h"
 #include "base/run_loop.h"
+#include "base/strings/string_piece_forward.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/bind.h"
-#include "base/test/scoped_feature_list.h"
-#include "build/chromeos_buildflags.h"
-#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
+#include "base/test/test_future.h"
+#include "chrome/browser/web_applications/mojom/user_display_mode.mojom-shared.h"
+#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
 #include "chrome/browser/web_applications/test/fake_data_retriever.h"
 #include "chrome/browser/web_applications/test/fake_install_finalizer.h"
-#include "chrome/browser/web_applications/test/fake_os_integration_manager.h"
 #include "chrome/browser/web_applications/test/fake_web_app_provider.h"
 #include "chrome/browser/web_applications/test/fake_web_app_ui_manager.h"
 #include "chrome/browser/web_applications/test/test_web_app_url_loader.h"
 #include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
 #include "chrome/browser/web_applications/test/web_app_test_utils.h"
 #include "chrome/browser/web_applications/web_app.h"
-#include "chrome/browser/web_applications/web_app_command_scheduler.h"
 #include "chrome/browser/web_applications/web_app_helpers.h"
 #include "chrome/browser/web_applications/web_app_id.h"
 #include "chrome/browser/web_applications/web_app_install_finalizer.h"
 #include "chrome/browser/web_applications/web_app_install_info.h"
-#include "chrome/browser/web_applications/web_app_provider.h"
 #include "chrome/browser/web_applications/web_app_registrar.h"
-#include "chrome/browser/web_applications/web_app_sync_bridge.h"
 #include "chrome/browser/web_applications/web_contents/web_app_data_retriever.h"
-#include "chrome/common/chrome_features.h"
-#include "chrome/common/pref_names.h"
 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
 #include "chrome/test/base/testing_profile.h"
-#include "components/prefs/pref_service.h"
 #include "components/webapps/browser/install_result_code.h"
 #include "components/webapps/browser/installable/installable_logging.h"
 #include "components/webapps/browser/installable/installable_metrics.h"
 #include "components/webapps/browser/uninstall_result_code.h"
-#include "content/public/test/test_utils.h"
+#include "mojo/public/cpp/bindings/struct_ptr.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
@@ -372,33 +372,24 @@
   url_loader().SetNextLoadUrlResult(kWebAppUrl,
                                     WebAppUrlLoader::Result::kUrlLoaded);
 
-  base::RunLoop run_loop;
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task->Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-  task->Install(web_contents(),
-                base::BindLambdaForTesting(
-                    [&](ExternallyManagedAppManager::InstallResult result) {
-                      absl::optional<AppId> id =
-                          registrar()->LookupExternalAppId(kWebAppUrl);
+  absl::optional<AppId> id = registrar()->LookupExternalAppId(kWebAppUrl);
 
-                      EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                                result.code);
-                      EXPECT_TRUE(result.app_id.has_value());
+  EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+  EXPECT_TRUE(result.app_id.has_value());
 
-                      EXPECT_FALSE(IsPlaceholderApp(kWebAppUrl));
+  EXPECT_FALSE(IsPlaceholderApp(kWebAppUrl));
 
-                      EXPECT_EQ(result.app_id.value(), id.value());
+  EXPECT_EQ(result.app_id.value(), id.value());
 
-                      EXPECT_EQ(0u, finalizer()->num_reparent_tab_calls());
+  EXPECT_EQ(0u, finalizer()->num_reparent_tab_calls());
 
-                      EXPECT_EQ(web_app_info().user_display_mode,
-                                mojom::UserDisplayMode::kBrowser);
-                      EXPECT_EQ(webapps::WebappInstallSource::INTERNAL_DEFAULT,
-                                finalize_options().install_surface);
-
-                      run_loop.Quit();
-                    }));
-
-  run_loop.Run();
+  EXPECT_EQ(web_app_info().user_display_mode, mojom::UserDisplayMode::kBrowser);
+  EXPECT_EQ(webapps::WebappInstallSource::INTERNAL_DEFAULT,
+            finalize_options().install_surface);
 }
 
 TEST_F(ExternallyManagedAppInstallTaskTest, InstallFails) {
@@ -412,25 +403,17 @@
   url_loader().SetNextLoadUrlResult(kWebAppUrl,
                                     WebAppUrlLoader::Result::kUrlLoaded);
 
-  base::RunLoop run_loop;
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task->Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-  task->Install(
-      web_contents(),
-      base::BindLambdaForTesting(
-          [&](ExternallyManagedAppManager::InstallResult result) {
-            absl::optional<AppId> id =
-                registrar()->LookupExternalAppId(kWebAppUrl);
+  absl::optional<AppId> id = registrar()->LookupExternalAppId(kWebAppUrl);
 
-            EXPECT_EQ(webapps::InstallResultCode::kGetWebAppInstallInfoFailed,
-                      result.code);
-            EXPECT_FALSE(result.app_id.has_value());
+  EXPECT_EQ(webapps::InstallResultCode::kGetWebAppInstallInfoFailed,
+            result.code);
+  EXPECT_FALSE(result.app_id.has_value());
 
-            EXPECT_FALSE(id.has_value());
-
-            run_loop.Quit();
-          }));
-
-  run_loop.Run();
+  EXPECT_FALSE(id.has_value());
 }
 
 TEST_F(ExternallyManagedAppInstallTaskTest, InstallForcedContainerWindow) {
@@ -444,19 +427,14 @@
   url_loader().SetNextLoadUrlResult(kWebAppUrl,
                                     WebAppUrlLoader::Result::kUrlLoaded);
 
-  base::RunLoop run_loop;
-  task->Install(web_contents(),
-                base::BindLambdaForTesting(
-                    [&](ExternallyManagedAppManager::InstallResult result) {
-                      EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                                result.code);
-                      EXPECT_TRUE(result.app_id.has_value());
-                      EXPECT_EQ(web_app_info().user_display_mode,
-                                mojom::UserDisplayMode::kStandalone);
-                      run_loop.Quit();
-                    }));
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task->Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-  run_loop.Run();
+  EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+  EXPECT_TRUE(result.app_id.has_value());
+  EXPECT_EQ(web_app_info().user_display_mode,
+            mojom::UserDisplayMode::kStandalone);
 }
 
 TEST_F(ExternallyManagedAppInstallTaskTest, InstallForcedContainerTab) {
@@ -470,19 +448,13 @@
   url_loader().SetNextLoadUrlResult(kWebAppUrl,
                                     WebAppUrlLoader::Result::kUrlLoaded);
 
-  base::RunLoop run_loop;
-  task->Install(web_contents(),
-                base::BindLambdaForTesting(
-                    [&](ExternallyManagedAppManager::InstallResult result) {
-                      EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                                result.code);
-                      EXPECT_TRUE(result.app_id.has_value());
-                      EXPECT_EQ(web_app_info().user_display_mode,
-                                mojom::UserDisplayMode::kBrowser);
-                      run_loop.Quit();
-                    }));
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task->Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-  run_loop.Run();
+  EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+  EXPECT_TRUE(result.app_id.has_value());
+  EXPECT_EQ(web_app_info().user_display_mode, mojom::UserDisplayMode::kBrowser);
 }
 
 TEST_F(ExternallyManagedAppInstallTaskTest, InstallPreinstalledApp) {
@@ -495,20 +467,15 @@
   url_loader().SetNextLoadUrlResult(kWebAppUrl,
                                     WebAppUrlLoader::Result::kUrlLoaded);
 
-  base::RunLoop run_loop;
-  task->Install(web_contents(),
-                base::BindLambdaForTesting(
-                    [&](ExternallyManagedAppManager::InstallResult result) {
-                      EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                                result.code);
-                      EXPECT_TRUE(result.app_id.has_value());
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task->Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-                      EXPECT_EQ(webapps::WebappInstallSource::INTERNAL_DEFAULT,
-                                finalize_options().install_surface);
-                      run_loop.Quit();
-                    }));
+  EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+  EXPECT_TRUE(result.app_id.has_value());
 
-  run_loop.Run();
+  EXPECT_EQ(webapps::WebappInstallSource::INTERNAL_DEFAULT,
+            finalize_options().install_surface);
 }
 
 TEST_F(ExternallyManagedAppInstallTaskTest, InstallAppFromPolicy) {
@@ -521,20 +488,15 @@
   url_loader().SetNextLoadUrlResult(kWebAppUrl,
                                     WebAppUrlLoader::Result::kUrlLoaded);
 
-  base::RunLoop run_loop;
-  task->Install(web_contents(),
-                base::BindLambdaForTesting(
-                    [&](ExternallyManagedAppManager::InstallResult result) {
-                      EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                                result.code);
-                      EXPECT_TRUE(result.app_id.has_value());
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task->Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-                      EXPECT_EQ(webapps::WebappInstallSource::EXTERNAL_POLICY,
-                                finalize_options().install_surface);
-                      run_loop.Quit();
-                    }));
+  EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+  EXPECT_TRUE(result.app_id.has_value());
 
-  run_loop.Run();
+  EXPECT_EQ(webapps::WebappInstallSource::EXTERNAL_POLICY,
+            finalize_options().install_surface);
 }
 
 TEST_F(ExternallyManagedAppInstallTaskTest, InstallPlaceholder) {
@@ -548,33 +510,27 @@
   url_loader().SetNextLoadUrlResult(
       kWebAppUrl, WebAppUrlLoader::Result::kRedirectedUrlLoaded);
 
-  base::RunLoop run_loop;
-  task->Install(
-      web_contents(),
-      base::BindLambdaForTesting(
-          [&](ExternallyManagedAppManager::InstallResult result) {
-            EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                      result.code);
-            EXPECT_TRUE(result.app_id.has_value());
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task->Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-            EXPECT_TRUE(IsPlaceholderApp(kWebAppUrl));
+  EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+  EXPECT_TRUE(result.app_id.has_value());
 
-            EXPECT_EQ(1u, finalizer()->finalize_options_list().size());
-            EXPECT_EQ(webapps::WebappInstallSource::EXTERNAL_POLICY,
-                      finalize_options().install_surface);
-            const WebAppInstallInfo& web_app_info =
-                finalizer()->web_app_info_list().at(0);
+  EXPECT_TRUE(IsPlaceholderApp(kWebAppUrl));
 
-            EXPECT_EQ(base::UTF8ToUTF16(kWebAppUrl.spec()), web_app_info.title);
-            EXPECT_EQ(kWebAppUrl, web_app_info.start_url);
-            EXPECT_EQ(web_app_info.user_display_mode,
-                      mojom::UserDisplayMode::kStandalone);
-            EXPECT_TRUE(web_app_info.manifest_icons.empty());
-            EXPECT_TRUE(web_app_info.icon_bitmaps.any.empty());
+  EXPECT_EQ(1u, finalizer()->finalize_options_list().size());
+  EXPECT_EQ(webapps::WebappInstallSource::EXTERNAL_POLICY,
+            finalize_options().install_surface);
+  const WebAppInstallInfo& web_app_info =
+      finalizer()->web_app_info_list().at(0);
 
-            run_loop.Quit();
-          }));
-  run_loop.Run();
+  EXPECT_EQ(base::UTF8ToUTF16(kWebAppUrl.spec()), web_app_info.title);
+  EXPECT_EQ(kWebAppUrl, web_app_info.start_url);
+  EXPECT_EQ(web_app_info.user_display_mode,
+            mojom::UserDisplayMode::kStandalone);
+  EXPECT_TRUE(web_app_info.manifest_icons.empty());
+  EXPECT_TRUE(web_app_info.icon_bitmaps.any.empty());
 }
 
 TEST_F(ExternallyManagedAppInstallTaskTest, InstallPlaceholderTwice) {
@@ -592,19 +548,14 @@
     url_loader().SetNextLoadUrlResult(
         kWebAppUrl, WebAppUrlLoader::Result::kRedirectedUrlLoaded);
 
-    base::RunLoop run_loop;
-    task->Install(
-        web_contents(),
-        base::BindLambdaForTesting(
-            [&](ExternallyManagedAppManager::InstallResult result) {
-              EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                        result.code);
-              placeholder_app_id = result.app_id.value();
+    base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+    task->Install(web_contents(), future.GetCallback());
+    const auto& result = future.Get();
 
-              EXPECT_EQ(1u, finalizer()->finalize_options_list().size());
-              run_loop.Quit();
-            }));
-    run_loop.Run();
+    EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+    placeholder_app_id = result.app_id.value();
+
+    EXPECT_EQ(1u, finalizer()->finalize_options_list().size());
   }
 
   // Try to install it again.
@@ -613,21 +564,15 @@
   url_loader().SetNextLoadUrlResult(
       kWebAppUrl, WebAppUrlLoader::Result::kRedirectedUrlLoaded);
 
-  base::RunLoop run_loop;
-  task->Install(web_contents(),
-                base::BindLambdaForTesting(
-                    [&](ExternallyManagedAppManager::InstallResult result) {
-                      EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                                result.code);
-                      EXPECT_EQ(placeholder_app_id, result.app_id.value());
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task->Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-                      // There shouldn't be a second call to the finalizer.
-                      EXPECT_EQ(1u,
-                                finalizer()->finalize_options_list().size());
+  EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+  EXPECT_EQ(placeholder_app_id, result.app_id.value());
 
-                      run_loop.Quit();
-                    }));
-  run_loop.Run();
+  // There shouldn't be a second call to the finalizer.
+  EXPECT_EQ(1u, finalizer()->finalize_options_list().size());
 }
 
 TEST_F(ExternallyManagedAppInstallTaskTest, ReinstallPlaceholderSucceeds) {
@@ -645,19 +590,14 @@
     url_loader().SetNextLoadUrlResult(
         kWebAppUrl, WebAppUrlLoader::Result::kRedirectedUrlLoaded);
 
-    base::RunLoop run_loop;
-    task->Install(
-        web_contents(),
-        base::BindLambdaForTesting(
-            [&](ExternallyManagedAppManager::InstallResult result) {
-              EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                        result.code);
-              placeholder_app_id = result.app_id.value();
+    base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+    task->Install(web_contents(), future.GetCallback());
+    const auto& result = future.Get();
 
-              EXPECT_EQ(1u, finalizer()->finalize_options_list().size());
-              run_loop.Quit();
-            }));
-    run_loop.Run();
+    EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+    placeholder_app_id = result.app_id.value();
+
+    EXPECT_EQ(1u, finalizer()->finalize_options_list().size());
   }
 
   // Replace the placeholder with a real app.
@@ -670,24 +610,16 @@
   url_loader().SetNextLoadUrlResult(kWebAppUrl,
                                     WebAppUrlLoader::Result::kUrlLoaded);
 
-  base::RunLoop run_loop;
-  task->Install(
-      web_contents(),
-      base::BindLambdaForTesting(
-          [&](ExternallyManagedAppManager::InstallResult result) {
-            EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                      result.code);
-            EXPECT_TRUE(result.app_id.has_value());
-            EXPECT_FALSE(IsPlaceholderApp(kWebAppUrl));
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task->Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-            EXPECT_EQ(1u,
-                      finalizer()->uninstall_external_web_app_urls().size());
-            EXPECT_EQ(kWebAppUrl,
-                      finalizer()->uninstall_external_web_app_urls().at(0));
+  EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+  EXPECT_TRUE(result.app_id.has_value());
+  EXPECT_FALSE(IsPlaceholderApp(kWebAppUrl));
 
-            run_loop.Quit();
-          }));
-  run_loop.Run();
+  EXPECT_EQ(1u, finalizer()->uninstall_external_web_app_urls().size());
+  EXPECT_EQ(kWebAppUrl, finalizer()->uninstall_external_web_app_urls().at(0));
 }
 
 TEST_F(ExternallyManagedAppInstallTaskTest, ReinstallPlaceholderFails) {
@@ -705,20 +637,14 @@
     url_loader().SetNextLoadUrlResult(
         kWebAppUrl, WebAppUrlLoader::Result::kRedirectedUrlLoaded);
 
-    base::RunLoop run_loop;
-    task->Install(
-        web_contents(),
-        base::BindLambdaForTesting(
-            [&](ExternallyManagedAppManager::InstallResult result) {
-              EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                        result.code);
-              placeholder_app_id = result.app_id.value();
+    base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+    task->Install(web_contents(), future.GetCallback());
+    const auto& result = future.Get();
 
-              EXPECT_EQ(1u, finalizer()->finalize_options_list().size());
+    EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+    placeholder_app_id = result.app_id.value();
 
-              run_loop.Quit();
-            }));
-    run_loop.Run();
+    EXPECT_EQ(1u, finalizer()->finalize_options_list().size());
   }
 
   // Replace the placeholder with a real app.
@@ -732,27 +658,20 @@
   url_loader().SetNextLoadUrlResult(kWebAppUrl,
                                     WebAppUrlLoader::Result::kUrlLoaded);
 
-  base::RunLoop run_loop;
-  task->Install(
-      web_contents(),
-      base::BindLambdaForTesting(
-          [&](ExternallyManagedAppManager::InstallResult result) {
-            EXPECT_EQ(webapps::InstallResultCode::kFailedPlaceholderUninstall,
-                      result.code);
-            EXPECT_FALSE(result.app_id.has_value());
-            EXPECT_TRUE(IsPlaceholderApp(kWebAppUrl));
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task->Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-            EXPECT_EQ(1u,
-                      finalizer()->uninstall_external_web_app_urls().size());
-            EXPECT_EQ(kWebAppUrl,
-                      finalizer()->uninstall_external_web_app_urls().at(0));
+  EXPECT_EQ(webapps::InstallResultCode::kFailedPlaceholderUninstall,
+            result.code);
+  EXPECT_FALSE(result.app_id.has_value());
+  EXPECT_TRUE(IsPlaceholderApp(kWebAppUrl));
 
-            // There should have been no new calls to install a placeholder.
-            EXPECT_EQ(1u, finalizer()->finalize_options_list().size());
+  EXPECT_EQ(1u, finalizer()->uninstall_external_web_app_urls().size());
+  EXPECT_EQ(kWebAppUrl, finalizer()->uninstall_external_web_app_urls().at(0));
 
-            run_loop.Quit();
-          }));
-  run_loop.Run();
+  // There should have been no new calls to install a placeholder.
+  EXPECT_EQ(1u, finalizer()->finalize_options_list().size());
 }
 
 #if defined(CHROMEOS)
@@ -770,22 +689,16 @@
   url_loader().SetNextLoadUrlResult(
       kWebAppUrl, WebAppUrlLoader::Result::kRedirectedUrlLoaded);
 
-  base::RunLoop run_loop;
-  task->Install(web_contents(),
-                base::BindLambdaForTesting(
-                    [&](ExternallyManagedAppManager::InstallResult result) {
-                      EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                                result.code);
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task->Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-                      const WebAppInstallInfo& web_app_info =
-                          finalizer()->web_app_info_list().at(0);
+  EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
 
-                      EXPECT_EQ(base::UTF8ToUTF16(kCustomName),
-                                web_app_info.title);
+  const WebAppInstallInfo& web_app_info =
+      finalizer()->web_app_info_list().at(0);
 
-                      run_loop.Quit();
-                    }));
-  run_loop.Run();
+  EXPECT_EQ(base::UTF8ToUTF16(kCustomName), web_app_info.title);
 }
 #endif  // defined(CHROMEOS)
 
@@ -798,7 +711,6 @@
     // Migrate app1 and app2.
     options.uninstall_and_replace = {"app1", "app2"};
 
-    base::RunLoop run_loop;
     auto task = GetInstallationTaskWithTestMocks(options);
     url_loader().AddPrepareForLoadResults(
         {WebAppUrlLoader::Result::kUrlLoaded,
@@ -806,26 +718,19 @@
     url_loader().SetNextLoadUrlResult(kWebAppUrl,
                                       WebAppUrlLoader::Result::kUrlLoaded);
 
-    task->Install(
-        web_contents(),
-        base::BindLambdaForTesting(
-            [&](ExternallyManagedAppManager::InstallResult result) {
-              app_id = result.app_id.value();
+    base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+    task->Install(web_contents(), future.GetCallback());
+    const auto& result = future.Get();
 
-              EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                        result.code);
-              EXPECT_EQ(result.app_id,
-                        *registrar()->LookupExternalAppId(kWebAppUrl));
+    app_id = result.app_id.value();
 
-              run_loop.Quit();
-            }));
-    run_loop.Run();
+    EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+    EXPECT_EQ(result.app_id, *registrar()->LookupExternalAppId(kWebAppUrl));
   }
   {
     // Migration should run on every install of the app.
     options.uninstall_and_replace = {"app3"};
 
-    base::RunLoop run_loop;
     auto task = GetInstallationTaskWithTestMocks(options);
     url_loader().AddPrepareForLoadResults(
         {WebAppUrlLoader::Result::kUrlLoaded,
@@ -833,17 +738,12 @@
     url_loader().SetNextLoadUrlResult(kWebAppUrl,
                                       WebAppUrlLoader::Result::kUrlLoaded);
 
-    task->Install(
-        web_contents(),
-        base::BindLambdaForTesting(
-            [&](ExternallyManagedAppManager::InstallResult result) {
-              EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
-                        result.code);
-              EXPECT_EQ(app_id, result.app_id.value());
+    base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+    task->Install(web_contents(), future.GetCallback());
+    const auto& result = future.Get();
 
-              run_loop.Quit();
-            }));
-    run_loop.Run();
+    EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, result.code);
+    EXPECT_EQ(app_id, result.app_id.value());
   }
 }
 
@@ -859,8 +759,6 @@
                        webapps::InstallResultCode::kInstallURLLoadTimeOut}};
 
   for (const auto& result_pair : result_pairs) {
-    base::RunLoop run_loop;
-
     ExternalInstallOptions install_options(
         GURL(), mojom::UserDisplayMode::kStandalone,
         ExternalInstallSource::kInternalDefault);
@@ -871,15 +769,11 @@
     url_loader().SetPrepareForLoadResultLoaded();
     url_loader().SetNextLoadUrlResult(GURL(), result_pair.loader_result);
 
-    install_task.Install(
-        web_contents(),
-        base::BindLambdaForTesting(
-            [&](ExternallyManagedAppManager::InstallResult result) {
-              EXPECT_EQ(result.code, result_pair.install_result);
-              run_loop.Quit();
-            }));
+    base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+    install_task.Install(web_contents(), future.GetCallback());
+    const auto& result = future.Get();
 
-    run_loop.Run();
+    EXPECT_EQ(result.code, result_pair.install_result);
   }
 }
 
@@ -923,31 +817,25 @@
   finalizer()->SetNextFinalizeInstallResult(
       kWebAppUrl, webapps::InstallResultCode::kSuccessNewInstall);
 
-  base::RunLoop run_loop;
-  task.Install(
-      /*web_contents=*/nullptr,
-      base::BindLambdaForTesting(
-          [&](ExternallyManagedAppManager::InstallResult result) {
-            absl::optional<AppId> id =
-                registrar()->LookupExternalAppId(kWebAppUrl);
-            EXPECT_EQ(webapps::InstallResultCode::kSuccessOfflineOnlyInstall,
-                      result.code);
-            EXPECT_TRUE(result.app_id.has_value());
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task.Install(/*web_contents=*/nullptr, future.GetCallback());
+  const auto& result = future.Get();
 
-            EXPECT_FALSE(IsPlaceholderApp(kWebAppUrl));
+  absl::optional<AppId> id = registrar()->LookupExternalAppId(kWebAppUrl);
+  EXPECT_EQ(webapps::InstallResultCode::kSuccessOfflineOnlyInstall,
+            result.code);
+  EXPECT_TRUE(result.app_id.has_value());
 
-            EXPECT_EQ(result.app_id.value(), id.value());
+  EXPECT_FALSE(IsPlaceholderApp(kWebAppUrl));
 
-            EXPECT_EQ(0u, finalizer()->num_reparent_tab_calls());
+  EXPECT_EQ(result.app_id.value(), id.value());
 
-            EXPECT_EQ(web_app_info().user_display_mode,
-                      mojom::UserDisplayMode::kStandalone);
-            EXPECT_EQ(webapps::WebappInstallSource::SYSTEM_DEFAULT,
-                      finalize_options().install_surface);
+  EXPECT_EQ(0u, finalizer()->num_reparent_tab_calls());
 
-            run_loop.Quit();
-          }));
-  run_loop.Run();
+  EXPECT_EQ(web_app_info().user_display_mode,
+            mojom::UserDisplayMode::kStandalone);
+  EXPECT_EQ(webapps::WebappInstallSource::SYSTEM_DEFAULT,
+            finalize_options().install_surface);
 }
 
 TEST_F(ExternallyManagedAppInstallTaskTest, InstallWithWebAppInfoFails) {
@@ -971,24 +859,16 @@
   finalizer()->SetNextFinalizeInstallResult(
       kWebAppUrl, webapps::InstallResultCode::kWriteDataFailed);
 
-  base::RunLoop run_loop;
+  base::test::TestFuture<ExternallyManagedAppManager::InstallResult> future;
+  task.Install(web_contents(), future.GetCallback());
+  const auto& result = future.Get();
 
-  task.Install(web_contents(),
-               base::BindLambdaForTesting(
-                   [&](ExternallyManagedAppManager::InstallResult result) {
-                     absl::optional<AppId> id =
-                         registrar()->LookupExternalAppId(kWebAppUrl);
+  absl::optional<AppId> id = registrar()->LookupExternalAppId(kWebAppUrl);
 
-                     EXPECT_EQ(webapps::InstallResultCode::kWriteDataFailed,
-                               result.code);
-                     EXPECT_FALSE(result.app_id.has_value());
+  EXPECT_EQ(webapps::InstallResultCode::kWriteDataFailed, result.code);
+  EXPECT_FALSE(result.app_id.has_value());
 
-                     EXPECT_FALSE(id.has_value());
-
-                     run_loop.Quit();
-                   }));
-
-  run_loop.Run();
+  EXPECT_FALSE(id.has_value());
 }
 
 }  // namespace web_app
diff --git a/chrome/browser/web_applications/externally_managed_app_install_task.h b/chrome/browser/web_applications/externally_managed_app_install_task.h
index 01b4b41..a6380965 100644
--- a/chrome/browser/web_applications/externally_managed_app_install_task.h
+++ b/chrome/browser/web_applications/externally_managed_app_install_task.h
@@ -5,20 +5,20 @@
 #ifndef CHROME_BROWSER_WEB_APPLICATIONS_EXTERNALLY_MANAGED_APP_INSTALL_TASK_H_
 #define CHROME_BROWSER_WEB_APPLICATIONS_EXTERNALLY_MANAGED_APP_INSTALL_TASK_H_
 
+#include <memory>
+
 #include "base/functional/callback.h"
-#include "base/functional/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "chrome/browser/web_applications/external_install_options.h"
 #include "chrome/browser/web_applications/externally_managed_app_manager.h"
-#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
+#include "chrome/browser/web_applications/web_app_constants.h"
 #include "chrome/browser/web_applications/web_app_id.h"
-#include "chrome/browser/web_applications/web_app_install_info.h"
-#include "chrome/browser/web_applications/web_app_install_utils.h"
 #include "chrome/browser/web_applications/web_contents/web_app_url_loader.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
 class Profile;
+class GURL;
 
 namespace content {
 class WebContents;
@@ -31,7 +31,6 @@
 
 namespace web_app {
 
-class WebAppUrlLoader;
 class WebAppInstallFinalizer;
 class WebAppCommandScheduler;
 class WebAppUiManager;
diff --git a/chrome/browser/web_applications/externally_managed_app_manager.cc b/chrome/browser/web_applications/externally_managed_app_manager.cc
index d00ff4eb..31a6535 100644
--- a/chrome/browser/web_applications/externally_managed_app_manager.cc
+++ b/chrome/browser/web_applications/externally_managed_app_manager.cc
@@ -174,8 +174,7 @@
         base::BindOnce(
             [](const UninstallCallback& callback, const GURL& app_url,
                webapps::UninstallResultCode code) {
-              callback.Run(app_url,
-                           code == webapps::UninstallResultCode::kSuccess);
+              callback.Run(app_url, UninstallSucceeded(code));
             },
             callback, url));
   }
diff --git a/chrome/browser/web_applications/externally_managed_app_manager_unittest.cc b/chrome/browser/web_applications/externally_managed_app_manager_unittest.cc
index 0f02a47..7339efb 100644
--- a/chrome/browser/web_applications/externally_managed_app_manager_unittest.cc
+++ b/chrome/browser/web_applications/externally_managed_app_manager_unittest.cc
@@ -501,6 +501,113 @@
                       /*additional_policy_ids=*/{}))));
 }
 
+TEST_F(ExternallyAppManagerTest, RemovingInstallUrlsFromSource) {
+  const GURL kStartUrl = GURL("https://www.example.com/index.html");
+  const GURL kInstallUrl1 =
+      GURL("https://www.example.com/nested/install_url.html");
+  const GURL kInstallUrl2 =
+      GURL("https://www.example.com/nested/install_url2.html");
+  const GURL kManifestUrl = GURL("https://www.example.com/manifest.json");
+
+  AppId app_id = PopulateBasicInstallPageWithManifest(kInstallUrl1,
+                                                      kManifestUrl, kStartUrl);
+  AppId app_id2 = PopulateBasicInstallPageWithManifest(kInstallUrl2,
+                                                       kManifestUrl, kStartUrl);
+  EXPECT_EQ(app_id, app_id2);
+
+  // Synchronize with 2 install URLs.
+  {
+    SynchronizeFuture result;
+    provider().externally_managed_app_manager().SynchronizeInstalledApps(
+        CreateExternalInstallOptionsFromTemplate(
+            {kInstallUrl1, kInstallUrl2},
+            ExternalInstallSource::kExternalPolicy),
+        ExternalInstallSource::kExternalPolicy, result.GetCallback());
+    ASSERT_TRUE(result.Wait());
+
+    // Empty uninstall results.
+    EXPECT_THAT(result.Get<UninstallResults>(), IsEmpty());
+
+    // Installs should have both succeeded.
+    EXPECT_THAT(
+        result.Get<InstallResults>(),
+        UnorderedElementsAre(
+            std::make_pair(
+                kInstallUrl1,
+                ExternallyManagedAppManager::InstallResult(
+                    webapps::InstallResultCode::kSuccessNewInstall, app_id)),
+            std::make_pair(
+                kInstallUrl2,
+                ExternallyManagedAppManager::InstallResult(
+                    webapps::InstallResultCode::kSuccessNewInstall, app_id))));
+
+    EXPECT_EQ(app_registrar().GetAppIds().size(), 1ul);
+    const WebApp* app = app_registrar().GetAppById(app_id);
+    ASSERT_TRUE(app);
+    EXPECT_THAT(app->management_to_external_config_map(),
+                ElementsAre(std::make_pair(
+                    WebAppManagement::kPolicy,
+                    WebApp::ExternalManagementConfig(
+                        /*is_placeholder=*/false,
+                        /*install_urls=*/{kInstallUrl1, kInstallUrl2},
+                        /*additional_policy_ids=*/{}))));
+  }
+
+  // Synchronize with 1 install URL.
+  {
+    SynchronizeFuture result;
+    provider().externally_managed_app_manager().SynchronizeInstalledApps(
+        CreateExternalInstallOptionsFromTemplate(
+            {kInstallUrl1}, ExternalInstallSource::kExternalPolicy),
+        ExternalInstallSource::kExternalPolicy, result.GetCallback());
+    ASSERT_TRUE(result.Wait());
+
+    // Empty install results.
+    EXPECT_THAT(result.Get<InstallResults>(),
+                UnorderedElementsAre(std::make_pair(
+                    kInstallUrl1,
+                    ExternallyManagedAppManager::InstallResult(
+                        webapps::InstallResultCode::kSuccessAlreadyInstalled,
+                        app_id))));
+
+    // One install URL uninstalled.
+    EXPECT_THAT(result.Get<UninstallResults>(),
+                UnorderedElementsAre(std::make_pair(kInstallUrl2, true)));
+
+    EXPECT_EQ(app_registrar().GetAppIds().size(), 1ul);
+    const WebApp* app = app_registrar().GetAppById(app_id);
+    ASSERT_TRUE(app);
+    EXPECT_THAT(app->management_to_external_config_map(),
+                ElementsAre(std::make_pair(WebAppManagement::kPolicy,
+                                           WebApp::ExternalManagementConfig(
+                                               /*is_placeholder=*/false,
+                                               /*install_urls=*/{kInstallUrl1},
+                                               /*additional_policy_ids=*/{}))));
+  }
+
+  // Synchronize with 0 install URLs.
+  {
+    SynchronizeFuture result;
+    provider().externally_managed_app_manager().SynchronizeInstalledApps(
+        CreateExternalInstallOptionsFromTemplate(
+            {}, ExternalInstallSource::kExternalPolicy),
+        ExternalInstallSource::kExternalPolicy, result.GetCallback());
+    ASSERT_TRUE(result.Wait());
+
+    // Empty install results.
+    EXPECT_THAT(result.Get<InstallResults>(), IsEmpty());
+
+    // One install URL uninstalled.
+    EXPECT_THAT(result.Get<UninstallResults>(),
+                UnorderedElementsAre(std::make_pair(kInstallUrl1, true)));
+
+    // App should be cleaned up.
+    EXPECT_EQ(app_registrar().GetAppIds().size(), 0ul);
+    const WebApp* app = app_registrar().GetAppById(app_id);
+    ASSERT_FALSE(app);
+  }
+}
+
 TEST_F(ExternallyAppManagerTest, InstallUrlChanges) {
   const GURL kStartUrl = GURL("https://www.example.com/index.html");
   const GURL kInstallUrl =
diff --git a/chrome/browser/web_applications/remove_web_app_job.cc b/chrome/browser/web_applications/remove_web_app_job.cc
deleted file mode 100644
index 674e3262..0000000
--- a/chrome/browser/web_applications/remove_web_app_job.cc
+++ /dev/null
@@ -1,181 +0,0 @@
-// 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 "chrome/browser/web_applications/remove_web_app_job.h"
-
-#include <memory>
-
-#include "base/functional/bind.h"
-#include "base/memory/ptr_util.h"
-#include "base/metrics/histogram_functions.h"
-#include "chrome/browser/web_applications/locks/with_app_resources.h"
-#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
-#include "chrome/browser/web_applications/web_app.h"
-#include "chrome/browser/web_applications/web_app_icon_manager.h"
-#include "chrome/browser/web_applications/web_app_install_finalizer.h"
-#include "chrome/browser/web_applications/web_app_install_manager.h"
-#include "chrome/browser/web_applications/web_app_registrar.h"
-#include "chrome/browser/web_applications/web_app_registry_update.h"
-#include "chrome/browser/web_applications/web_app_sync_bridge.h"
-#include "chrome/browser/web_applications/web_app_translation_manager.h"
-#include "components/webapps/browser/uninstall_result_code.h"
-
-#if BUILDFLAG(IS_CHROMEOS_LACROS)
-#include "base/feature_list.h"
-#include "base/functional/callback_helpers.h"
-#include "chrome/browser/browser_process.h"
-#include "chrome/browser/profiles/profile_attributes_storage.h"
-#include "chrome/browser/profiles/profile_manager.h"
-#include "chrome/browser/profiles/profile_metrics.h"
-#include "chrome/browser/web_applications/web_app_utils.h"
-#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)
-
-namespace web_app {
-
-RemoveWebAppJob::~RemoveWebAppJob() = default;
-
-std::unique_ptr<RemoveWebAppJob> RemoveWebAppJob::Start(
-    webapps::WebappUninstallSource uninstall_source,
-    AppId app_id,
-    WithAppResources& resources,
-    Profile& profile,
-    UninstallCallback callback) {
-  CHECK(resources.registrar().GetAppById(app_id));
-
-  auto job = base::WrapUnique(new RemoveWebAppJob(
-      uninstall_source, app_id, resources, profile, std::move(callback)));
-  base::WeakPtr<RemoveWebAppJob> job_weak_ptr =
-      job->weak_ptr_factory_.GetWeakPtr();
-
-  resources.install_manager().NotifyWebAppWillBeUninstalled(app_id);
-
-  {
-    // Note: It is supported to re-start an uninstall on startup, so
-    // `is_uninstalling()` is not checked. It is a class invariant that there
-    // can never be more than one uninstall task operating on the same web app
-    // at the same time.
-    ScopedRegistryUpdate update(&resources.sync_bridge());
-    WebApp* app = update->UpdateApp(app_id);
-    CHECK(app);
-    app->SetIsUninstalling(true);
-  }
-
-  auto synchronize_barrier = OsIntegrationManager::GetBarrierForSynchronize(
-      base::BindOnce(&RemoveWebAppJob::OnOsHooksUninstalled, job_weak_ptr));
-
-  // TODO(crbug.com/1401125): Remove UninstallAllOsHooks() once OS integration
-  // sub managers have been implemented.
-  resources.os_integration_manager().UninstallAllOsHooks(app_id,
-                                                         synchronize_barrier);
-  resources.os_integration_manager().Synchronize(
-      app_id, base::BindOnce(synchronize_barrier, OsHooksErrors()));
-
-  // While sometimes `Synchronize` needs to read icon data, for the uninstall
-  // case it never needs to be read. Thus, it is safe to schedule this now and
-  // not after the `Synchronize` call completes.
-  resources.icon_manager().DeleteData(
-      app_id,
-      base::BindOnce(&RemoveWebAppJob::OnIconDataDeleted, job_weak_ptr));
-
-  resources.translation_manager().DeleteTranslations(
-      app_id,
-      base::BindOnce(&RemoveWebAppJob::OnTranslationDataDeleted, job_weak_ptr));
-
-#if BUILDFLAG(IS_CHROMEOS_LACROS)
-  if (ResolveExperimentalWebAppIsolationFeature() ==
-      ExperimentalWebAppIsolationMode::kProfile) {
-    const WebApp& app = *resources.registrar().GetAppById(app_id);
-    if (app.chromeos_data() && app.chromeos_data()->app_profile_path) {
-      const base::FilePath& app_profile_path =
-          app.chromeos_data()->app_profile_path.value();
-      CHECK(Profile::IsWebAppProfilePath(app_profile_path));
-      if (g_browser_process->profile_manager()
-              ->GetProfileAttributesStorage()
-              .GetProfileAttributesWithPath(app_profile_path)) {
-        job->pending_app_profile_deletion_ = true;
-        g_browser_process->profile_manager()
-            ->GetDeleteProfileHelper()
-            .MaybeScheduleProfileForDeletion(
-                app_profile_path,
-                base::BindOnce(&RemoveWebAppJob::OnWebAppProfileDeleted,
-                               job_weak_ptr),
-                ProfileMetrics::ProfileDelete::DELETE_PROFILE_USER_MANAGER);
-      } else {
-        LOG(ERROR) << "cannot find web app profile at " << app_profile_path;
-      }
-    }
-  }
-#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)
-
-  return job;
-}
-
-RemoveWebAppJob::RemoveWebAppJob(
-    webapps::WebappUninstallSource uninstall_source,
-    AppId app_id,
-    WithAppResources& resources,
-    Profile& profile,
-    UninstallCallback callback)
-    : uninstall_source_(uninstall_source),
-      app_id_(app_id),
-      resources_(resources),
-      profile_(profile),
-      callback_(std::move(callback)) {}
-
-void RemoveWebAppJob::OnOsHooksUninstalled(OsHooksErrors errors) {
-  CHECK(!done_);
-  CHECK(!hooks_uninstalled_);
-  hooks_uninstalled_ = true;
-  base::UmaHistogramBoolean("WebApp.Uninstall.OsHookSuccess", errors.none());
-  errors_ = errors_ || errors.any();
-  MaybeFinishUninstall();
-}
-
-void RemoveWebAppJob::OnIconDataDeleted(bool success) {
-  CHECK(!done_);
-  CHECK(!app_data_deleted_);
-  app_data_deleted_ = true;
-  base::UmaHistogramBoolean("WebApp.Uninstall.IconDataSuccess", success);
-  errors_ = errors_ || !success;
-  MaybeFinishUninstall();
-}
-
-void RemoveWebAppJob::OnTranslationDataDeleted(bool success) {
-  CHECK(!done_);
-  CHECK(!translation_data_deleted_);
-  translation_data_deleted_ = true;
-  errors_ = errors_ || !success;
-  MaybeFinishUninstall();
-}
-
-void RemoveWebAppJob::OnWebAppProfileDeleted(Profile* profile) {
-  CHECK(!done_);
-  CHECK(pending_app_profile_deletion_);
-  // This must be an isolated web app profile rather than the WebAppProvider
-  // profile.
-  CHECK_NE(&profile_.get(), profile);
-  pending_app_profile_deletion_ = false;
-  MaybeFinishUninstall();
-}
-
-void RemoveWebAppJob::MaybeFinishUninstall() {
-  CHECK(!done_);
-  if (!hooks_uninstalled_ || !app_data_deleted_ || !translation_data_deleted_ ||
-      pending_app_profile_deletion_) {
-    return;
-  }
-  done_ = true;
-
-  base::UmaHistogramBoolean("WebApp.Uninstall.Result", !errors_);
-  {
-    CHECK_NE(resources_->registrar().GetAppById(app_id_), nullptr);
-    ScopedRegistryUpdate update(&resources_->sync_bridge());
-    update->DeleteApp(app_id_);
-  }
-  resources_->install_manager().NotifyWebAppUninstalled(app_id_,
-                                                        uninstall_source_);
-  std::move(callback_).Run(!errors_);
-}
-
-}  // namespace web_app
diff --git a/chrome/browser/web_applications/remove_web_app_job.h b/chrome/browser/web_applications/remove_web_app_job.h
deleted file mode 100644
index acd5567..0000000
--- a/chrome/browser/web_applications/remove_web_app_job.h
+++ /dev/null
@@ -1,83 +0,0 @@
-// 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.
-
-#ifndef CHROME_BROWSER_WEB_APPLICATIONS_REMOVE_WEB_APP_JOB_H_
-#define CHROME_BROWSER_WEB_APPLICATIONS_REMOVE_WEB_APP_JOB_H_
-
-#include <memory>
-
-#include "base/functional/callback.h"
-#include "base/memory/raw_ref.h"
-#include "base/memory/weak_ptr.h"
-#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
-#include "chrome/browser/web_applications/web_app_id.h"
-
-namespace webapps {
-enum class UninstallResultCode;
-enum class WebappUninstallSource;
-}  // namespace webapps
-
-class Profile;
-
-namespace web_app {
-
-class WithAppResources;
-
-// Uninstalls a given web app by:
-// 1) Unregistering OS hooks.
-// 2) Deleting the app from the database.
-// 3) Deleting data on disk.
-// Extra invariants:
-// * There is never more than one uninstall task operating on the same app at
-//   the same time.
-class RemoveWebAppJob {
- public:
-  using UninstallCallback = base::OnceCallback<void(bool success)>;
-
-  static std::unique_ptr<RemoveWebAppJob> Start(
-      webapps::WebappUninstallSource uninstall_source,
-      AppId app_id,
-      WithAppResources& resources,
-      Profile& profile,
-      UninstallCallback callback);
-
-  ~RemoveWebAppJob();
-
- private:
-  RemoveWebAppJob(webapps::WebappUninstallSource uninstall_source,
-                  AppId app_id,
-                  WithAppResources& resources,
-                  Profile& profile,
-                  UninstallCallback callback);
-
-  void OnOsHooksUninstalled(OsHooksErrors errors);
-  void OnIconDataDeleted(bool success);
-  void OnTranslationDataDeleted(bool success);
-  void OnWebAppProfileDeleted(Profile* profile);
-  void MaybeFinishUninstall();
-
-  webapps::WebappUninstallSource uninstall_source_;
-  AppId app_id_;
-  // The RemoveWebAppJob is kicked off by the WebAppUninstallCommand
-  // and is constructed and destructed well within the lifetime of the
-  // Uninstall command. This ensures that this class is guaranteed to be
-  // destructed before any of the WebAppProvider systems shut down.
-  raw_ref<WithAppResources> resources_;
-  // `this` is owned by `profile_`.
-  raw_ref<Profile> profile_;
-  UninstallCallback callback_;
-
-  bool app_data_deleted_ = false;
-  bool translation_data_deleted_ = false;
-  bool hooks_uninstalled_ = false;
-  bool pending_app_profile_deletion_ = false;
-  bool errors_ = false;
-  bool done_ = false;
-
-  base::WeakPtrFactory<RemoveWebAppJob> weak_ptr_factory_{this};
-};
-
-}  // namespace web_app
-
-#endif  // CHROME_BROWSER_WEB_APPLICATIONS_REMOVE_WEB_APP_JOB_H_
diff --git a/chrome/browser/web_applications/test/web_app_install_test_utils.cc b/chrome/browser/web_applications/test/web_app_install_test_utils.cc
index 98fc35af..54a0d04 100644
--- a/chrome/browser/web_applications/test/web_app_install_test_utils.cc
+++ b/chrome/browser/web_applications/test/web_app_install_test_utils.cc
@@ -120,17 +120,12 @@
 
 void UninstallWebApp(Profile* profile, const AppId& app_id) {
   WebAppProvider* const provider = WebAppProvider::GetForTest(profile);
-  base::RunLoop run_loop;
-
+  base::test::TestFuture<webapps::UninstallResultCode> future;
   DCHECK(provider->registrar_unsafe().CanUserUninstallWebApp(app_id));
   provider->install_finalizer().UninstallWebApp(
-      app_id, webapps::WebappUninstallSource::kAppMenu,
-      base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
-        EXPECT_EQ(code, webapps::UninstallResultCode::kSuccess);
-        run_loop.Quit();
-      }));
+      app_id, webapps::WebappUninstallSource::kAppMenu, future.GetCallback());
+  EXPECT_TRUE(UninstallSucceeded(future.Get()));
 
-  run_loop.Run();
   // Allow updates to be published to App Service listeners.
   base::RunLoop().RunUntilIdle();
 }
diff --git a/chrome/browser/web_applications/uninstall/remove_install_source_job.cc b/chrome/browser/web_applications/uninstall/remove_install_source_job.cc
new file mode 100644
index 0000000..8993e708
--- /dev/null
+++ b/chrome/browser/web_applications/uninstall/remove_install_source_job.cc
@@ -0,0 +1,152 @@
+// 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 "chrome/browser/web_applications/uninstall/remove_install_source_job.h"
+
+#include "base/strings/to_string.h"
+#include "chrome/browser/web_applications/locks/all_apps_lock.h"
+#include "chrome/browser/web_applications/uninstall/remove_web_app_job.h"
+#include "chrome/browser/web_applications/web_app.h"
+#include "chrome/browser/web_applications/web_app_install_manager.h"
+#include "chrome/browser/web_applications/web_app_install_utils.h"
+#include "chrome/browser/web_applications/web_app_registrar.h"
+#include "chrome/browser/web_applications/web_app_registry_update.h"
+#include "chrome/browser/web_applications/web_app_sync_bridge.h"
+
+namespace web_app {
+
+namespace {
+
+enum class Action {
+  kNone,
+  kRemoveInstallSource,
+  kRemoveApp,
+};
+
+Action GetAction(const WebAppSources& sources,
+                 WebAppManagement::Type install_source) {
+  if (sources.none()) {
+    // TODO(crbug.com/1427340): Return a different UninstallResultCode
+    // for this case and log it in metrics.
+    return Action::kRemoveApp;
+  }
+
+  if (!sources[install_source]) {
+    return Action::kNone;
+  }
+
+  if (sources.count() > 1) {
+    return Action::kRemoveInstallSource;
+  }
+
+  CHECK_EQ(sources.count(), 1u);
+  CHECK(sources[install_source]);
+  return Action::kRemoveApp;
+}
+
+}  // namespace
+
+RemoveInstallSourceJob::RemoveInstallSourceJob(
+    webapps::WebappUninstallSource uninstall_source,
+    Profile& profile,
+    AppId app_id,
+    WebAppManagement::Type install_source)
+    : uninstall_source_(uninstall_source),
+      profile_(profile),
+      app_id_(app_id),
+      install_source_(install_source) {}
+
+RemoveInstallSourceJob::~RemoveInstallSourceJob() = default;
+
+void RemoveInstallSourceJob::Start(AllAppsLock& lock, Callback callback) {
+  lock_ = &lock;
+  callback_ = std::move(callback);
+
+  const WebApp* app = lock_->registrar().GetAppById(app_id_);
+  if (!app) {
+    CompleteAndSelfDestruct(webapps::UninstallResultCode::kNoAppToUninstall);
+    return;
+  }
+
+  WebAppManagement::Type install_source = install_source_;
+  switch (GetAction(app->GetSources(), install_source)) {
+    case Action::kNone:
+      // TODO(crbug.com/1427340): Return a different UninstallResultCode
+      // for when no action is taken instead of being overly specific to the "no
+      // app" case.
+      CompleteAndSelfDestruct(webapps::UninstallResultCode::kNoAppToUninstall);
+      return;
+
+    case Action::kRemoveInstallSource:
+      // Install sources may block user uninstallation (e.g. policy), if one of
+      // these install sources is being removed then the ability to uninstall
+      // may need to be re-deployed into the OS.
+      MaybeRegisterOsUninstall(
+          app, install_source, lock_->os_integration_manager(),
+          base::BindOnce(
+              &RemoveInstallSourceJob::RemoveInstallSourceFromDatabase,
+              weak_ptr_factory_.GetWeakPtr()));
+      return;
+
+    case Action::kRemoveApp:
+      sub_job_ = std::make_unique<RemoveWebAppJob>(
+          uninstall_source_, profile_.get(), app_id_,
+          /*is_initial_request=*/false);
+      sub_job_->Start(
+          *lock_,
+          base::BindOnce(&RemoveInstallSourceJob::CompleteAndSelfDestruct,
+                         weak_ptr_factory_.GetWeakPtr()));
+      return;
+  }
+}
+
+base::Value RemoveInstallSourceJob::ToDebugValue() const {
+  base::Value::Dict dict;
+  dict.Set("!job", "RemoveInstallSourceJob");
+  dict.Set("app_id", app_id_);
+  dict.Set("install_source", base::ToString(install_source_));
+  dict.Set("callback", callback_.is_null());
+  dict.Set("active_sub_job",
+           sub_job_ ? sub_job_->ToDebugValue() : base::Value());
+  dict.Set("completed_sub_job", completed_sub_job_debug_value_.Clone());
+  return base::Value(std::move(dict));
+}
+
+webapps::WebappUninstallSource RemoveInstallSourceJob::uninstall_source()
+    const {
+  return uninstall_source_;
+}
+
+void RemoveInstallSourceJob::RemoveInstallSourceFromDatabase(
+    OsHooksErrors os_hooks_errors) {
+  {
+    ScopedRegistryUpdate update(&lock_->sync_bridge());
+    WebApp* app = update->UpdateApp(app_id_);
+    app->RemoveSource(install_source_);
+    if (install_source_ == WebAppManagement::kSubApp) {
+      app->SetParentAppId(absl::nullopt);
+    }
+    // TODO(crbug.com/1447308): Make sync uninstall not synchronously
+    // remove its sync install source even while a command has an app lock so
+    // that we can CHECK(app->HasAnySources()) here.
+  }
+
+  lock_->install_manager().NotifyWebAppSourceRemoved(app_id_);
+
+  CompleteAndSelfDestruct(webapps::UninstallResultCode::kSuccess);
+}
+
+void RemoveInstallSourceJob::CompleteAndSelfDestruct(
+    webapps::UninstallResultCode code) {
+  CHECK(callback_);
+
+  if (sub_job_) {
+    completed_sub_job_debug_value_ = sub_job_->ToDebugValue();
+    sub_job_.reset();
+  }
+
+  std::move(callback_).Run(code);
+}
+
+}  // namespace web_app
diff --git a/chrome/browser/web_applications/uninstall/remove_install_source_job.h b/chrome/browser/web_applications/uninstall/remove_install_source_job.h
new file mode 100644
index 0000000..3636841
--- /dev/null
+++ b/chrome/browser/web_applications/uninstall/remove_install_source_job.h
@@ -0,0 +1,62 @@
+// 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.
+
+#ifndef CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_REMOVE_INSTALL_SOURCE_JOB_H_
+#define CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_REMOVE_INSTALL_SOURCE_JOB_H_
+
+#include "base/functional/callback.h"
+#include "base/values.h"
+#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
+#include "chrome/browser/web_applications/uninstall/uninstall_job.h"
+#include "chrome/browser/web_applications/web_app_constants.h"
+#include "chrome/browser/web_applications/web_app_id.h"
+#include "components/webapps/browser/installable/installable_metrics.h"
+
+namespace web_app {
+
+class RemoveWebAppJob;
+
+// Removes an install source from a given web app, will uninstall the web app if
+// no install sources remain.
+// May cause a web app to become user uninstallable, will deploy uninstall OS
+// hooks in that case.
+class RemoveInstallSourceJob : public UninstallJob {
+ public:
+  RemoveInstallSourceJob(webapps::WebappUninstallSource uninstall_source,
+                         Profile& profile,
+                         AppId app_id,
+                         WebAppManagement::Type install_source);
+  ~RemoveInstallSourceJob() override;
+
+  const AppId& app_id() const { return app_id_; }
+
+  // UninstallJob:
+  void Start(AllAppsLock& lock, Callback callback) override;
+  base::Value ToDebugValue() const override;
+  webapps::WebappUninstallSource uninstall_source() const override;
+
+ private:
+  void RemoveInstallSourceFromDatabase(OsHooksErrors os_hooks_errors);
+  void CompleteAndSelfDestruct(webapps::UninstallResultCode code);
+
+  webapps::WebappUninstallSource uninstall_source_;
+  // `this` must be owned by `profile_`.
+  raw_ref<Profile> profile_;
+  AppId app_id_;
+  WebAppManagement::Type install_source_;
+
+  // `this` must be started and run within the scope of a WebAppCommand's
+  // AllAppsLock.
+  raw_ptr<AllAppsLock> lock_;
+  Callback callback_;
+
+  std::unique_ptr<RemoveWebAppJob> sub_job_;
+  base::Value completed_sub_job_debug_value_;
+
+  base::WeakPtrFactory<RemoveInstallSourceJob> weak_ptr_factory_{this};
+};
+
+}  // namespace web_app
+
+#endif  // CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_REMOVE_INSTALL_SOURCE_JOB_H_
diff --git a/chrome/browser/web_applications/uninstall/remove_install_url_job.cc b/chrome/browser/web_applications/uninstall/remove_install_url_job.cc
new file mode 100644
index 0000000..c50d3da
--- /dev/null
+++ b/chrome/browser/web_applications/uninstall/remove_install_url_job.cc
@@ -0,0 +1,105 @@
+// 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 "chrome/browser/web_applications/uninstall/remove_install_url_job.h"
+
+#include "base/containers/contains.h"
+#include "base/strings/to_string.h"
+#include "base/values.h"
+#include "chrome/browser/web_applications/locks/all_apps_lock.h"
+#include "chrome/browser/web_applications/uninstall/remove_install_source_job.h"
+#include "chrome/browser/web_applications/web_app.h"
+#include "chrome/browser/web_applications/web_app_registrar.h"
+#include "chrome/browser/web_applications/web_app_registry_update.h"
+#include "chrome/browser/web_applications/web_app_sync_bridge.h"
+
+namespace web_app {
+
+RemoveInstallUrlJob::RemoveInstallUrlJob(
+    webapps::WebappUninstallSource uninstall_source,
+    Profile& profile,
+    WebAppManagement::Type install_source,
+    GURL install_url)
+    : uninstall_source_(uninstall_source),
+      profile_(profile),
+      install_source_(install_source),
+      install_url_(std::move(install_url)) {}
+
+RemoveInstallUrlJob::~RemoveInstallUrlJob() = default;
+
+void RemoveInstallUrlJob::Start(AllAppsLock& lock, Callback callback) {
+  lock_ = &lock;
+  callback_ = std::move(callback);
+
+  const WebApp* app = nullptr;
+  bool is_only_install_url = false;
+  for (const WebApp& candidate_app : lock_->registrar().GetApps()) {
+    const WebApp::ExternalConfigMap& config_map =
+        candidate_app.management_to_external_config_map();
+    auto it = config_map.find(install_source_);
+    if (it != config_map.end()) {
+      const WebApp::ExternalManagementConfig& config = it->second;
+      if (base::Contains(config.install_urls, install_url_)) {
+        app = &candidate_app;
+        is_only_install_url = config.install_urls.size() == 1u;
+        break;
+      }
+    }
+  }
+
+  if (!app) {
+    CompleteAndSelfDestruct(webapps::UninstallResultCode::kNoAppToUninstall);
+    return;
+  }
+
+  if (is_only_install_url) {
+    sub_job_ = std::make_unique<RemoveInstallSourceJob>(
+        uninstall_source_, profile_.get(), app->app_id(), install_source_);
+    sub_job_->Start(
+        *lock_, base::BindOnce(&RemoveInstallUrlJob::CompleteAndSelfDestruct,
+                               weak_ptr_factory_.GetWeakPtr()));
+    return;
+  }
+
+  {
+    ScopedRegistryUpdate update(&lock_->sync_bridge());
+    CHECK(update->UpdateApp(app->app_id())
+              ->RemoveInstallUrlForSource(install_source_, install_url_));
+
+    // This is no longer a valid pointer after the registry update, clear it for
+    // safety.
+    app = nullptr;
+  }
+  CompleteAndSelfDestruct(webapps::UninstallResultCode::kSuccess);
+}
+
+base::Value RemoveInstallUrlJob::ToDebugValue() const {
+  base::Value::Dict dict;
+  dict.Set("job", "RemoveInstallUrlJob");
+  dict.Set("install_source", base::ToString(install_source_));
+  dict.Set("install_url", install_url_.spec());
+  dict.Set("callback", callback_.is_null());
+  dict.Set("active_sub_job",
+           sub_job_ ? sub_job_->ToDebugValue() : base::Value());
+  dict.Set("completed_sub_job", completed_sub_job_debug_value_.Clone());
+  return base::Value(std::move(dict));
+}
+
+webapps::WebappUninstallSource RemoveInstallUrlJob::uninstall_source() const {
+  return uninstall_source_;
+}
+
+void RemoveInstallUrlJob::CompleteAndSelfDestruct(
+    webapps::UninstallResultCode code) {
+  CHECK(callback_);
+
+  if (sub_job_) {
+    completed_sub_job_debug_value_ = sub_job_->ToDebugValue();
+    sub_job_.reset();
+  }
+
+  std::move(callback_).Run(code);
+}
+
+}  // namespace web_app
diff --git a/chrome/browser/web_applications/uninstall/remove_install_url_job.h b/chrome/browser/web_applications/uninstall/remove_install_url_job.h
new file mode 100644
index 0000000..eb16b6d
--- /dev/null
+++ b/chrome/browser/web_applications/uninstall/remove_install_url_job.h
@@ -0,0 +1,67 @@
+// 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.
+
+#ifndef CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_REMOVE_INSTALL_URL_JOB_H_
+#define CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_REMOVE_INSTALL_URL_JOB_H_
+
+#include <memory>
+
+#include "base/functional/callback.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ref.h"
+#include "base/memory/weak_ptr.h"
+#include "base/values.h"
+#include "chrome/browser/web_applications/uninstall/uninstall_job.h"
+#include "chrome/browser/web_applications/web_app_constants.h"
+#include "chrome/browser/web_applications/web_app_id.h"
+#include "components/webapps/browser/installable/installable_metrics.h"
+#include "components/webapps/browser/uninstall_result_code.h"
+#include "url/gurl.h"
+
+class Profile;
+
+namespace web_app {
+
+class RemoveInstallSourceJob;
+
+// Removes an install source's install URL from the first matching web app.
+// This will remove the install source if there are no remaining install URLs
+// for that install source which in turn will remove the web app if there are no
+// remaining install sources for the web app.
+class RemoveInstallUrlJob : public UninstallJob {
+ public:
+  RemoveInstallUrlJob(webapps::WebappUninstallSource uninstall_source,
+                      Profile& profile,
+                      WebAppManagement::Type install_source,
+                      GURL install_url);
+  ~RemoveInstallUrlJob() override;
+
+  // UninstallJob:
+  void Start(AllAppsLock& lock, Callback callback) override;
+  base::Value ToDebugValue() const override;
+  webapps::WebappUninstallSource uninstall_source() const override;
+
+ private:
+  void CompleteAndSelfDestruct(webapps::UninstallResultCode code);
+
+  webapps::WebappUninstallSource uninstall_source_;
+  // `this` must be owned by `profile_`.
+  raw_ref<Profile> profile_;
+  WebAppManagement::Type install_source_;
+  GURL install_url_;
+
+  // `this` must be started and run within the scope of a WebAppCommand's
+  // AllAppsLock.
+  raw_ptr<AllAppsLock> lock_;
+  Callback callback_;
+
+  std::unique_ptr<RemoveInstallSourceJob> sub_job_;
+  base::Value completed_sub_job_debug_value_;
+
+  base::WeakPtrFactory<RemoveInstallUrlJob> weak_ptr_factory_{this};
+};
+
+}  // namespace web_app
+
+#endif  // CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_REMOVE_INSTALL_URL_JOB_H_
diff --git a/chrome/browser/web_applications/uninstall/remove_web_app_job.cc b/chrome/browser/web_applications/uninstall/remove_web_app_job.cc
new file mode 100644
index 0000000..9cb2c24f
--- /dev/null
+++ b/chrome/browser/web_applications/uninstall/remove_web_app_job.cc
@@ -0,0 +1,297 @@
+// 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 "chrome/browser/web_applications/uninstall/remove_web_app_job.h"
+
+#include <memory>
+
+#include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
+#include "base/memory/ptr_util.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/to_string.h"
+#include "chrome/browser/web_applications/locks/all_apps_lock.h"
+#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
+#include "chrome/browser/web_applications/uninstall/remove_install_source_job.h"
+#include "chrome/browser/web_applications/user_uninstalled_preinstalled_web_app_prefs.h"
+#include "chrome/browser/web_applications/web_app.h"
+#include "chrome/browser/web_applications/web_app_icon_manager.h"
+#include "chrome/browser/web_applications/web_app_install_finalizer.h"
+#include "chrome/browser/web_applications/web_app_install_manager.h"
+#include "chrome/browser/web_applications/web_app_registrar.h"
+#include "chrome/browser/web_applications/web_app_registry_update.h"
+#include "chrome/browser/web_applications/web_app_sync_bridge.h"
+#include "chrome/browser/web_applications/web_app_translation_manager.h"
+#include "components/webapps/browser/uninstall_result_code.h"
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+#include "base/feature_list.h"
+#include "base/functional/callback_helpers.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_metrics.h"
+#include "chrome/browser/web_applications/web_app_utils.h"
+#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)
+
+namespace web_app {
+
+namespace {
+
+bool CanUninstallAllManagementSources(
+    webapps::WebappUninstallSource uninstall_source) {
+  // Check that the source was from a known 'user' or allowed ones such
+  // as kMigration.
+  return uninstall_source == webapps::WebappUninstallSource::kUnknown ||
+         uninstall_source == webapps::WebappUninstallSource::kAppMenu ||
+         uninstall_source == webapps::WebappUninstallSource::kAppsPage ||
+         uninstall_source == webapps::WebappUninstallSource::kOsSettings ||
+         uninstall_source == webapps::WebappUninstallSource::kAppManagement ||
+         uninstall_source == webapps::WebappUninstallSource::kMigration ||
+         uninstall_source == webapps::WebappUninstallSource::kAppList ||
+         uninstall_source == webapps::WebappUninstallSource::kShelf ||
+         uninstall_source == webapps::WebappUninstallSource::kSync ||
+         uninstall_source == webapps::WebappUninstallSource::kStartupCleanup ||
+         uninstall_source == webapps::WebappUninstallSource::kTestCleanup;
+}
+
+}  // namespace
+
+RemoveWebAppJob::RemoveWebAppJob(
+    webapps::WebappUninstallSource uninstall_source,
+    Profile& profile,
+    AppId app_id,
+    bool is_initial_request)
+    : uninstall_source_(uninstall_source),
+      profile_(profile),
+      app_id_(app_id),
+      is_initial_request_(is_initial_request) {
+  if (is_initial_request_) {
+    CHECK(CanUninstallAllManagementSources(uninstall_source_));
+  }
+}
+
+RemoveWebAppJob::~RemoveWebAppJob() = default;
+
+void RemoveWebAppJob::Start(AllAppsLock& lock, Callback callback) {
+  lock_ = &lock;
+  callback_ = std::move(callback);
+
+  const WebApp* app = lock_->registrar().GetAppById(app_id_);
+  if (!app) {
+    CompleteAndSelfDestruct(webapps::UninstallResultCode::kNoAppToUninstall);
+    return;
+  }
+
+  if (is_initial_request_) {
+    // The following CHECK streamlines the user uninstall and sync uninstall
+    // flow, because for sync uninstalls, the web_app source is removed before
+    // being synced, so the first condition fails by the time an Uninstall is
+    // invoked.
+    // TODO(crbug.com/1447308): Checking kSync shouldn't be needed once
+    // this issue is resolved.
+    // TODO(crbug.com/1427340): Change this to be:
+    // if (uninstall_source is user initiated) {
+    //   CHECK(user can uninstall);
+    //   Add to user uninstalled prefs.
+    // }
+    CHECK(app->CanUserUninstallWebApp() ||
+          uninstall_source_ == webapps::WebappUninstallSource::kSync);
+
+    if (app->IsPreinstalledApp()) {
+      // Update the default uninstalled web_app prefs if it is a preinstalled
+      // app but being removed by user.
+      const WebApp::ExternalConfigMap& config_map =
+          app->management_to_external_config_map();
+      auto it = config_map.find(WebAppManagement::kDefault);
+      if (it != config_map.end()) {
+        UserUninstalledPreinstalledWebAppPrefs(profile_->GetPrefs())
+            .Add(app_id_, it->second.install_urls);
+      } else {
+        base::UmaHistogramBoolean(
+            "WebApp.Preinstalled.ExternalConfigMapAbsentDuringUninstall", true);
+      }
+    }
+  }
+
+  sub_apps_pending_removal_ = lock_->registrar().GetAllSubAppIds(app_id_);
+
+  lock_->install_manager().NotifyWebAppWillBeUninstalled(app_id_);
+
+  {
+    ScopedRegistryUpdate update(&lock_->sync_bridge());
+    WebApp* mutable_app = update->UpdateApp(app_id_);
+    CHECK(mutable_app);
+    mutable_app->SetIsUninstalling(true);
+  }
+
+  auto synchronize_barrier = OsIntegrationManager::GetBarrierForSynchronize(
+      base::BindOnce(&RemoveWebAppJob::OnOsHooksUninstalled,
+                     weak_ptr_factory_.GetWeakPtr()));
+
+  // TODO(crbug.com/1401125): Remove UninstallAllOsHooks() once OS integration
+  // sub managers have been implemented.
+  lock_->os_integration_manager().UninstallAllOsHooks(app_id_,
+                                                      synchronize_barrier);
+  lock_->os_integration_manager().Synchronize(
+      app_id_, base::BindOnce(synchronize_barrier, OsHooksErrors()));
+
+  // While sometimes `Synchronize` needs to read icon data, for the uninstall
+  // case it never needs to be read. Thus, it is safe to schedule this now and
+  // not after the `Synchronize` call completes.
+  lock_->icon_manager().DeleteData(
+      app_id_, base::BindOnce(&RemoveWebAppJob::OnIconDataDeleted,
+                              weak_ptr_factory_.GetWeakPtr()));
+
+  lock_->translation_manager().DeleteTranslations(
+      app_id_, base::BindOnce(&RemoveWebAppJob::OnTranslationDataDeleted,
+                              weak_ptr_factory_.GetWeakPtr()));
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+  if (ResolveExperimentalWebAppIsolationFeature() ==
+      ExperimentalWebAppIsolationMode::kProfile) {
+    if (app->chromeos_data() && app->chromeos_data()->app_profile_path) {
+      const base::FilePath& app_profile_path =
+          app->chromeos_data()->app_profile_path.value();
+      CHECK(Profile::IsWebAppProfilePath(app_profile_path));
+      if (g_browser_process->profile_manager()
+              ->GetProfileAttributesStorage()
+              .GetProfileAttributesWithPath(app_profile_path)) {
+        pending_app_profile_deletion_ = true;
+        g_browser_process->profile_manager()
+            ->GetDeleteProfileHelper()
+            .MaybeScheduleProfileForDeletion(
+                app_profile_path,
+                base::BindOnce(&RemoveWebAppJob::OnWebAppProfileDeleted,
+                               weak_ptr_factory_.GetWeakPtr()),
+                ProfileMetrics::ProfileDelete::DELETE_PROFILE_USER_MANAGER);
+      } else {
+      }
+    }
+  }
+#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)
+}
+
+base::Value RemoveWebAppJob::ToDebugValue() const {
+  base::Value::Dict dict;
+  dict.Set("!job", "RemoveWebAppJob");
+  dict.Set("app_id", app_id_);
+  dict.Set("is_initial_request", is_initial_request_);
+  dict.Set("callback", callback_.is_null());
+  dict.Set("app_data_deleted", app_data_deleted_);
+  dict.Set("translation_data_deleted", translation_data_deleted_);
+  dict.Set("hooks_uninstalled", hooks_uninstalled_);
+  dict.Set("pending_app_profile_deletion", pending_app_profile_deletion_);
+  dict.Set("errors", errors_);
+  dict.Set("primary_removal_result",
+           primary_removal_result_
+               ? base::Value(base::ToString(primary_removal_result_.value()))
+               : base::Value());
+  {
+    base::Value::List list;
+    for (const AppId& sub_app_id : sub_apps_pending_removal_) {
+      list.Append(sub_app_id);
+    }
+    dict.Set("sub_apps_pending_removal", std::move(list));
+  }
+  dict.Set("active_sub_job",
+           sub_job_ ? sub_job_->ToDebugValue() : base::Value());
+  dict.Set("completed_sub_jobs", completed_sub_job_debug_dict_.Clone());
+  return base::Value(std::move(dict));
+}
+
+webapps::WebappUninstallSource RemoveWebAppJob::uninstall_source() const {
+  return uninstall_source_;
+}
+
+void RemoveWebAppJob::OnOsHooksUninstalled(OsHooksErrors errors) {
+  CHECK(!primary_removal_result_.has_value());
+  CHECK(!hooks_uninstalled_);
+  hooks_uninstalled_ = true;
+  base::UmaHistogramBoolean("WebApp.Uninstall.OsHookSuccess", errors.none());
+  errors_ = errors_ || errors.any();
+  MaybeFinishPrimaryRemoval();
+}
+
+void RemoveWebAppJob::OnIconDataDeleted(bool success) {
+  CHECK(!primary_removal_result_.has_value());
+  CHECK(!app_data_deleted_);
+  app_data_deleted_ = true;
+  base::UmaHistogramBoolean("WebApp.Uninstall.IconDataSuccess", success);
+  errors_ = errors_ || !success;
+  MaybeFinishPrimaryRemoval();
+}
+
+void RemoveWebAppJob::OnTranslationDataDeleted(bool success) {
+  CHECK(!primary_removal_result_.has_value());
+  CHECK(!translation_data_deleted_);
+  translation_data_deleted_ = true;
+  errors_ = errors_ || !success;
+  MaybeFinishPrimaryRemoval();
+}
+
+void RemoveWebAppJob::OnWebAppProfileDeleted(Profile* profile) {
+  CHECK(!primary_removal_result_.has_value());
+  CHECK(pending_app_profile_deletion_);
+  // This must be an isolated web app profile rather than the WebAppProvider
+  // profile.
+  CHECK_NE(&profile_.get(), profile);
+  pending_app_profile_deletion_ = false;
+  MaybeFinishPrimaryRemoval();
+}
+
+void RemoveWebAppJob::MaybeFinishPrimaryRemoval() {
+  CHECK(!primary_removal_result_.has_value());
+  if (!hooks_uninstalled_ || !app_data_deleted_ || !translation_data_deleted_ ||
+      pending_app_profile_deletion_) {
+    return;
+  }
+
+  {
+    CHECK_NE(lock_->registrar().GetAppById(app_id_), nullptr);
+    ScopedRegistryUpdate update(&lock_->sync_bridge());
+    update->DeleteApp(app_id_);
+  }
+
+  primary_removal_result_ = errors_ ? webapps::UninstallResultCode::kError
+                                    : webapps::UninstallResultCode::kSuccess;
+  base::UmaHistogramBoolean("WebApp.Uninstall.Result", !errors_);
+  lock_->install_manager().NotifyWebAppUninstalled(app_id_, uninstall_source_);
+
+  ProcessSubAppsPendingRemovalOrComplete();
+}
+
+void RemoveWebAppJob::ProcessSubAppsPendingRemovalOrComplete() {
+  CHECK(primary_removal_result_.has_value());
+
+  if (sub_job_) {
+    completed_sub_job_debug_dict_.Set(sub_job_->app_id(),
+                                      sub_job_->ToDebugValue());
+    sub_job_.reset();
+  }
+
+  if (sub_apps_pending_removal_.empty()) {
+    CompleteAndSelfDestruct(primary_removal_result_.value());
+    return;
+  }
+
+  AppId sub_app_id = std::move(sub_apps_pending_removal_.back());
+  sub_apps_pending_removal_.pop_back();
+
+  sub_job_ = std::make_unique<RemoveInstallSourceJob>(
+      uninstall_source_, profile_.get(), sub_app_id,
+      WebAppManagement::Type::kSubApp);
+  sub_job_->Start(*lock_,
+                  base::IgnoreArgs<webapps::UninstallResultCode>(base::BindOnce(
+                      &RemoveWebAppJob::ProcessSubAppsPendingRemovalOrComplete,
+                      weak_ptr_factory_.GetWeakPtr())));
+}
+
+void RemoveWebAppJob::CompleteAndSelfDestruct(
+    webapps::UninstallResultCode code) {
+  CHECK(callback_);
+  std::move(callback_).Run(code);
+}
+
+}  // namespace web_app
diff --git a/chrome/browser/web_applications/uninstall/remove_web_app_job.h b/chrome/browser/web_applications/uninstall/remove_web_app_job.h
new file mode 100644
index 0000000..b09e672
--- /dev/null
+++ b/chrome/browser/web_applications/uninstall/remove_web_app_job.h
@@ -0,0 +1,79 @@
+// 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.
+
+#ifndef CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_REMOVE_WEB_APP_JOB_H_
+#define CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_REMOVE_WEB_APP_JOB_H_
+
+#include "base/functional/callback.h"
+#include "base/values.h"
+#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
+#include "chrome/browser/web_applications/uninstall/uninstall_job.h"
+#include "chrome/browser/web_applications/web_app_id.h"
+#include "components/webapps/browser/installable/installable_metrics.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+class Profile;
+
+namespace web_app {
+
+class RemoveInstallSourceJob;
+
+// Removes a web app from the database and cleans up all assets and OS
+// integrations. Disconnects it from any of its sub apps and uninstalls them too
+// if they have no other install sources.
+// Adds it to `UserUninstalledPreinstalledWebAppPrefs` if it was default
+// installed and the removal was user initiated.
+class RemoveWebAppJob : public UninstallJob {
+ public:
+  // `is_initial_request` indicates that this operation is not a byproduct of
+  // removing the last install source from a web app via external management and
+  // will be treated as a user uninstall.
+  RemoveWebAppJob(webapps::WebappUninstallSource uninstall_source,
+                  Profile& profile,
+                  AppId app_id,
+                  bool is_initial_request = true);
+  ~RemoveWebAppJob() override;
+
+  // UninstallJob:
+  void Start(AllAppsLock& lock, Callback callback) override;
+  base::Value ToDebugValue() const override;
+  webapps::WebappUninstallSource uninstall_source() const override;
+
+ private:
+  void OnOsHooksUninstalled(OsHooksErrors errors);
+  void OnIconDataDeleted(bool success);
+  void OnTranslationDataDeleted(bool success);
+  void OnWebAppProfileDeleted(Profile* profile);
+  void MaybeFinishPrimaryRemoval();
+  void ProcessSubAppsPendingRemovalOrComplete();
+  void CompleteAndSelfDestruct(webapps::UninstallResultCode code);
+
+  webapps::WebappUninstallSource uninstall_source_;
+  // `this` must be owned by `profile_`.
+  raw_ref<Profile> profile_;
+  AppId app_id_;
+  bool is_initial_request_;
+
+  // `this` must be started and run within the scope of a WebAppCommand's
+  // AllAppsLock.
+  raw_ptr<AllAppsLock> lock_;
+  Callback callback_;
+
+  bool app_data_deleted_ = false;
+  bool translation_data_deleted_ = false;
+  bool hooks_uninstalled_ = false;
+  bool pending_app_profile_deletion_ = false;
+  bool errors_ = false;
+  absl::optional<webapps::UninstallResultCode> primary_removal_result_;
+
+  std::vector<AppId> sub_apps_pending_removal_;
+  std::unique_ptr<RemoveInstallSourceJob> sub_job_;
+  base::Value::Dict completed_sub_job_debug_dict_;
+
+  base::WeakPtrFactory<RemoveWebAppJob> weak_ptr_factory_{this};
+};
+
+}  // namespace web_app
+
+#endif  // CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_REMOVE_WEB_APP_JOB_H_
diff --git a/chrome/browser/web_applications/uninstall/uninstall_job.cc b/chrome/browser/web_applications/uninstall/uninstall_job.cc
new file mode 100644
index 0000000..4c41dc7
--- /dev/null
+++ b/chrome/browser/web_applications/uninstall/uninstall_job.cc
@@ -0,0 +1,11 @@
+// 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 "chrome/browser/web_applications/uninstall/uninstall_job.h"
+
+namespace web_app {
+
+UninstallJob::~UninstallJob() = default;
+
+}  // namespace web_app
diff --git a/chrome/browser/web_applications/uninstall/uninstall_job.h b/chrome/browser/web_applications/uninstall/uninstall_job.h
new file mode 100644
index 0000000..f57cc81
--- /dev/null
+++ b/chrome/browser/web_applications/uninstall/uninstall_job.h
@@ -0,0 +1,34 @@
+// 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.
+
+#ifndef CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_UNINSTALL_JOB_H_
+#define CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_UNINSTALL_JOB_H_
+
+#include "base/functional/callback.h"
+#include "components/webapps/browser/installable/installable_metrics.h"
+#include "components/webapps/browser/uninstall_result_code.h"
+
+namespace base {
+class Value;
+}
+
+namespace web_app {
+
+class AllAppsLock;
+
+// Common interface for uninstall related jobs used by WebAppUninstallCommand.
+class UninstallJob {
+ public:
+  using Callback = base::OnceCallback<void(webapps::UninstallResultCode)>;
+
+  virtual ~UninstallJob();
+
+  virtual void Start(AllAppsLock& lock, Callback) = 0;
+  virtual base::Value ToDebugValue() const = 0;
+  virtual webapps::WebappUninstallSource uninstall_source() const = 0;
+};
+
+}  // namespace web_app
+
+#endif  // CHROME_BROWSER_WEB_APPLICATIONS_UNINSTALL_UNINSTALL_JOB_H_
diff --git a/chrome/browser/web_applications/web_app_command_scheduler.cc b/chrome/browser/web_applications/web_app_command_scheduler.cc
index 3a538b28..82d31ff 100644
--- a/chrome/browser/web_applications/web_app_command_scheduler.cc
+++ b/chrome/browser/web_applications/web_app_command_scheduler.cc
@@ -44,6 +44,8 @@
 #include "chrome/browser/web_applications/locks/shared_web_contents_lock.h"
 #include "chrome/browser/web_applications/locks/shared_web_contents_with_app_lock.h"
 #include "chrome/browser/web_applications/os_integration/os_integration_sub_manager.h"
+#include "chrome/browser/web_applications/uninstall/remove_install_source_job.h"
+#include "chrome/browser/web_applications/uninstall/remove_web_app_job.h"
 #include "chrome/browser/web_applications/web_app_command_manager.h"
 #include "chrome/browser/web_applications/web_app_constants.h"
 #include "chrome/browser/web_applications/web_app_provider.h"
@@ -438,7 +440,7 @@
     const AppId& app_id,
     absl::optional<WebAppManagement::Type> external_install_source,
     webapps::WebappUninstallSource uninstall_source,
-    WebAppUninstallCommand::UninstallWebAppCallback callback,
+    UninstallJob::Callback callback,
     const base::Location& location) {
   if (IsShuttingDown()) {
     base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
@@ -446,10 +448,17 @@
                                   webapps::UninstallResultCode::kCancelled));
     return;
   }
+  std::unique_ptr<UninstallJob> job;
+  if (external_install_source.has_value()) {
+    job = std::make_unique<RemoveInstallSourceJob>(
+        uninstall_source, *profile_, app_id, external_install_source.value());
+  } else {
+    job =
+        std::make_unique<RemoveWebAppJob>(uninstall_source, *profile_, app_id);
+  }
   provider_->command_manager().ScheduleCommand(
-      std::make_unique<WebAppUninstallCommand>(
-          app_id, external_install_source, uninstall_source,
-          std::move(callback), profile_.get()),
+      std::make_unique<WebAppUninstallCommand>(std::move(job),
+                                               std::move(callback)),
       location);
 }
 
diff --git a/chrome/browser/web_applications/web_app_command_scheduler.h b/chrome/browser/web_applications/web_app_command_scheduler.h
index dba56415..44924e1 100644
--- a/chrome/browser/web_applications/web_app_command_scheduler.h
+++ b/chrome/browser/web_applications/web_app_command_scheduler.h
@@ -21,6 +21,7 @@
 #include "chrome/browser/web_applications/commands/navigate_and_trigger_install_dialog_command.h"
 #include "chrome/browser/web_applications/external_install_options.h"
 #include "chrome/browser/web_applications/isolated_web_apps/install_isolated_web_app_command.h"
+#include "chrome/browser/web_applications/uninstall/uninstall_job.h"
 #include "chrome/browser/web_applications/web_app_install_params.h"
 #include "chrome/browser/web_applications/web_app_ui_manager.h"
 #include "components/webapps/browser/installable/installable_metrics.h"
@@ -211,11 +212,18 @@
                        OnceInstallCallback callback,
                        const base::Location& location = FROM_HERE);
 
-  // Schedules a command that uninstalls a web app.
+  // Schedules a command that, if `external_install_source` is set, removes the
+  // install source from a web app, otherwise uninstalls the web app. If the
+  // last install source of a web app is removed the web app will be
+  // uninstalled. If the uninstalled web app has sub apps their parent install
+  // source will be removed, uninstalling them too if they no longer have any
+  // install sources, this process will repeat as many times as needed.
+  // TODO(crbug.com/1427340): Expose this as separate RemoveInstallUrl(),
+  // RemoveInstallSource() and UninstallWebApp() methods.
   void Uninstall(const AppId& app_id,
                  absl::optional<WebAppManagement::Type> external_install_source,
                  webapps::WebappUninstallSource uninstall_source,
-                 WebAppInstallFinalizer::UninstallWebAppCallback callback,
+                 UninstallJob::Callback callback,
                  const base::Location& location = FROM_HERE);
 
   // Schedules a command that updates run on os login to provided `login_mode`
diff --git a/chrome/browser/web_applications/web_app_install_finalizer.cc b/chrome/browser/web_applications/web_app_install_finalizer.cc
index 090d577..33aec3e0 100644
--- a/chrome/browser/web_applications/web_app_install_finalizer.cc
+++ b/chrome/browser/web_applications/web_app_install_finalizer.cc
@@ -29,6 +29,9 @@
 #include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
 #include "chrome/browser/web_applications/os_integration/web_app_shortcuts_menu.h"
 #include "chrome/browser/web_applications/policy/web_app_policy_manager.h"
+#include "chrome/browser/web_applications/uninstall/remove_install_source_job.h"
+#include "chrome/browser/web_applications/uninstall/remove_install_url_job.h"
+#include "chrome/browser/web_applications/uninstall/remove_web_app_job.h"
 #include "chrome/browser/web_applications/web_app.h"
 #include "chrome/browser/web_applications/web_app_command_manager.h"
 #include "chrome/browser/web_applications/web_app_helpers.h"
@@ -102,6 +105,25 @@
   }
 }
 
+bool IsExternalInstallSource(WebAppManagement::Type install_source) {
+  switch (install_source) {
+    case WebAppManagement::Type::kSystem:
+    case WebAppManagement::Type::kKiosk:
+    case WebAppManagement::Type::kPolicy:
+    case WebAppManagement::Type::kSubApp:
+    case WebAppManagement::Type::kWebAppStore:
+    case WebAppManagement::Type::kDefault:
+      return true;
+    case WebAppManagement::Type::kSync:
+    // TODO(crbug.com/1427340): These are classified incorrectly, this check
+    // isn't really useful and should be removed.
+    case WebAppManagement::Type::kOneDriveIntegration:
+    case WebAppManagement::Type::kCommandLine:
+    case WebAppManagement::Type::kOem:
+      return false;
+  }
+}
+
 }  // namespace
 
 WebAppInstallFinalizer::FinalizeOptions::FinalizeOptions(
@@ -279,13 +301,7 @@
     webapps::WebappUninstallSource uninstall_source,
     UninstallWebAppCallback callback) {
   DCHECK(started_);
-
-  DCHECK(external_install_source == WebAppManagement::Type::kSystem ||
-         external_install_source == WebAppManagement::Type::kKiosk ||
-         external_install_source == WebAppManagement::Type::kPolicy ||
-         external_install_source == WebAppManagement::Type::kSubApp ||
-         external_install_source == WebAppManagement::Type::kWebAppStore ||
-         external_install_source == WebAppManagement::Type::kDefault);
+  DCHECK(IsExternalInstallSource(external_install_source));
 
   ScheduleUninstallCommand(app_id, external_install_source, uninstall_source,
                            std::move(callback));
@@ -296,20 +312,11 @@
     WebAppManagement::Type external_install_source,
     webapps::WebappUninstallSource uninstall_source,
     UninstallWebAppCallback callback) {
-  absl::optional<AppId> app_id =
-      GetWebAppRegistrar().LookupExternalAppId(app_url);
-  if (!app_id.has_value()) {
-    LOG(WARNING) << "Couldn't uninstall web app with url " << app_url
-                 << "; No corresponding web app for url.";
-    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
-        FROM_HERE,
-        base::BindOnce(std::move(callback),
-                       webapps::UninstallResultCode::kNoAppToUninstall));
-    return;
-  }
-
-  UninstallExternalWebApp(app_id.value(), external_install_source,
-                          uninstall_source, std::move(callback));
+  DCHECK(IsExternalInstallSource(external_install_source));
+  command_manager_->ScheduleCommand(std::make_unique<WebAppUninstallCommand>(
+      std::make_unique<RemoveInstallUrlJob>(uninstall_source, *profile_,
+                                            external_install_source, app_url),
+      std::move(callback)));
 }
 
 void WebAppInstallFinalizer::UninstallWebApp(
@@ -707,16 +714,24 @@
   }
 }
 
+// TODO(crbug.com/1427340): Remove this interface in favour of calling into the
+// WebAppCommandScheduler directly.
 void WebAppInstallFinalizer::ScheduleUninstallCommand(
     const AppId& app_id,
     absl::optional<WebAppManagement::Type> external_install_source,
     webapps::WebappUninstallSource uninstall_source,
     UninstallWebAppCallback callback) {
-  auto uninstall_command = std::make_unique<WebAppUninstallCommand>(
-      app_id, external_install_source, uninstall_source, std::move(callback),
-      *profile_);
+  std::unique_ptr<UninstallJob> job;
+  if (external_install_source.has_value()) {
+    job = std::make_unique<RemoveInstallSourceJob>(
+        uninstall_source, *profile_, app_id, external_install_source.value());
+  } else {
+    job =
+        std::make_unique<RemoveWebAppJob>(uninstall_source, *profile_, app_id);
+  }
 
-  command_manager_->ScheduleCommand(std::move(uninstall_command));
+  command_manager_->ScheduleCommand(std::make_unique<WebAppUninstallCommand>(
+      std::move(job), std::move(callback)));
 }
 
 FileHandlerUpdateAction WebAppInstallFinalizer::GetFileHandlerUpdateAction(
diff --git a/chrome/browser/web_applications/web_app_sync_bridge.cc b/chrome/browser/web_applications/web_app_sync_bridge.cc
index 3d34fea..413b6dd 100644
--- a/chrome/browser/web_applications/web_app_sync_bridge.cc
+++ b/chrome/browser/web_applications/web_app_sync_bridge.cc
@@ -512,7 +512,7 @@
     const AppId& app,
     webapps::UninstallResultCode code) {
   base::UmaHistogramBoolean("Webapp.SyncInitiatedUninstallResult",
-                            code == webapps::UninstallResultCode::kSuccess);
+                            UninstallSucceeded(code));
 }
 
 void WebAppSyncBridge::ReportErrorToChangeProcessor(
diff --git a/chrome/build/lacros64.pgo.txt b/chrome/build/lacros64.pgo.txt
index 6419ea8..59c7850d3 100644
--- a/chrome/build/lacros64.pgo.txt
+++ b/chrome/build/lacros64.pgo.txt
@@ -1 +1 @@
-chrome-chromeos-amd64-generic-main-1686569992-cadbff87684e467b60f416db70f7174d4f771626.profdata
+chrome-chromeos-amd64-generic-main-1686614843-fb023098bd54d5f9232a1aab9a5addc0eb0e66f9.profdata
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index d20cd10..141c0f6 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-main-1686592783-12f8170d67690f1acad6573f0402eca64644f8f6.profdata
+chrome-linux-main-1686614344-b40512faafcbf1a450705d568f696240d79af67e.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 1e1f242..0923d41 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1686607171-743d6a79a755c8a4c4616fa1ab322ae584b6b424.profdata
+chrome-mac-arm-main-1686628682-4adcbdf54b11aa11df116454d7dff7e825f3eb43.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index 3ad3e79..fbdb8c2c 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-main-1686592783-1607a28f8d7d28e7ba5762ed24afa975b3d88500.profdata
+chrome-mac-main-1686614344-808ac7995b1cf6853fa7f0128c09cb3b69189308.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 8835530..205e901 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1686603569-e7868c60e5a1da1934d0615259b23d04fd0aedfc.profdata
+chrome-win32-main-1686625113-10f7c4ee5c417dcac022725baa02c44e7cda1edf.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 925c3c9d..9ce230c5 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1686603569-c33ab0a6a9068249df3039b1848349c92b813cf7.profdata
+chrome-win64-main-1686625113-76fa6f71e60b16f08a61c19af03c00697b955c34.profdata
diff --git a/chrome/common/extensions/api/file_manager_private.idl b/chrome/common/extensions/api/file_manager_private.idl
index 7749b59e..c9d8967 100644
--- a/chrome/common/extensions/api/file_manager_private.idl
+++ b/chrome/common/extensions/api/file_manager_private.idl
@@ -388,13 +388,15 @@
 };
 
 // Describes the stage the bulk pinning manager is in. This enum should be kept
-// in sync with chromeos/ash/components/drivefs/drivefs_pin_manager.h
+// in sync with chromeos/ash/components/drivefs/mojom/pin_manager_types.mojom.
 enum BulkPinStage {
   // Initial stage.
   stopped,
 
   // Paused because of unfavorable network conditions.
-  paused,
+  paused_offline,
+  // Paused due to battery saver mode active.
+  paused_battery_saver,
 
   // In-progress stages.
   getting_free_space,
diff --git a/chrome/common/extensions/extension_constants.cc b/chrome/common/extensions/extension_constants.cc
index a14ac1b..27579fd 100644
--- a/chrome/common/extensions/extension_constants.cc
+++ b/chrome/common/extensions/extension_constants.cc
@@ -93,6 +93,8 @@
     "accessibility_common_manifest.json";
 const char kAccessibilityCommonGuestManifestFilename[] =
     "accessibility_common_manifest_guest.json";
+const char kAutotestPrivateTestExtensionId[] =
+    "ddammdhioacbehjngdmkjcjbnfginlla";
 const char kChromeVoxExtensionPath[] = "chromeos/accessibility";
 const char kChromeVoxManifestFilename[] = "chromevox_manifest.json";
 const char kChromeVoxGuestManifestFilename[] = "chromevox_manifest_guest.json";
diff --git a/chrome/common/extensions/extension_constants.h b/chrome/common/extensions/extension_constants.h
index 61d38ef8..0ad5613bc 100644
--- a/chrome/common/extensions/extension_constants.h
+++ b/chrome/common/extensions/extension_constants.h
@@ -184,6 +184,8 @@
 extern const char kAccessibilityCommonManifestFilename[];
 // The guest manifest filename of the Accessibility Common extension.
 extern const char kAccessibilityCommonGuestManifestFilename[];
+// Extension ID of the autotest_private test extension.
+extern const char kAutotestPrivateTestExtensionId[];
 // Path to preinstalled ChromeVox screen reader extension (relative to
 // |chrome::DIR_RESOURCES|).
 extern const char kChromeVoxExtensionPath[];
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 98cfff5b..345c267 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -3883,6 +3883,7 @@
         "../browser/ash/app_list/app_list_sort_browsertest.cc",
         "../browser/ash/app_list/app_service/app_service_app_item_browsertest.cc",
         "../browser/ash/app_list/app_service/app_service_context_menu_browsertest.cc",
+        "../browser/ash/app_list/app_service/app_service_promise_app_item_browsertest.cc",
         "../browser/ash/app_list/arc/arc_usb_host_permission_browsertest.cc",
         "../browser/ash/app_list/chrome_app_list_item_browsertest.cc",
         "../browser/ash/app_list/chrome_app_list_model_updater_browsertest.cc",
diff --git a/chrome/test/data/extensions/api_test/autotest_private/test.js b/chrome/test/data/extensions/api_test/autotest_private/test.js
index 7b0ee69..c619ea96 100644
--- a/chrome/test/data/extensions/api_test/autotest_private/test.js
+++ b/chrome/test/data/extensions/api_test/autotest_private/test.js
@@ -1612,9 +1612,9 @@
         chrome.autotestPrivate.getLacrosInfo(
             chrome.test.callbackPass(function(lacrosInfo) {
               chrome.test.assertEq('Unavailable', lacrosInfo['state']);
-              chrome.test.assertTrue(!lacrosInfo['isKeepAlive']);
+              chrome.test.assertTrue(lacrosInfo['isKeepAlive']);
               chrome.test.assertEq('', lacrosInfo['lacrosPath']);
-              chrome.test.assertEq('SideBySide', lacrosInfo['mode']);
+              chrome.test.assertEq('Only', lacrosInfo['mode']);
             }));
       },
     ]
diff --git a/chromeos/ash/components/drivefs/BUILD.gn b/chromeos/ash/components/drivefs/BUILD.gn
index 3847c05..1aa9b202 100644
--- a/chromeos/ash/components/drivefs/BUILD.gn
+++ b/chromeos/ash/components/drivefs/BUILD.gn
@@ -46,6 +46,8 @@
     "//chromeos/ash/components/drivefs/mojom:pin_manager_types",
     "//chromeos/ash/components/network",
     "//chromeos/components/mojo_bootstrap",
+    "//chromeos/dbus/power:power",
+    "//chromeos/dbus/power:power_manager_proto",
     "//components/account_id",
     "//components/drive",
     "//components/signin/public/identity_manager",
@@ -102,6 +104,8 @@
     "//chromeos/ash/components/drivefs/mojom",
     "//chromeos/ash/components/drivefs/mojom:pin_manager_types",
     "//chromeos/components/mojo_bootstrap",
+    "//chromeos/dbus/power",
+    "//chromeos/dbus/power:power_manager_proto",
     "//components/account_id",
     "//components/drive",
     "//components/invalidation/impl:test_support",
diff --git a/chromeos/ash/components/drivefs/drivefs_pin_manager.cc b/chromeos/ash/components/drivefs/drivefs_pin_manager.cc
index 90d5113..992cca84 100644
--- a/chromeos/ash/components/drivefs/drivefs_pin_manager.cc
+++ b/chromeos/ash/components/drivefs/drivefs_pin_manager.cc
@@ -20,6 +20,7 @@
 #include "base/strings/stringprintf.h"
 #include "base/task/sequenced_task_runner.h"
 #include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h"
+#include "chromeos/dbus/power/power_manager_client.h"
 #include "components/drive/file_errors.h"
 #include "third_party/cros_system_api/constants/cryptohome.h"
 
@@ -299,7 +300,8 @@
 
     case Stage::kGettingFreeSpace:
     case Stage::kListingFiles:
-    case Stage::kPaused:
+    case Stage::kPausedOffline:
+    case Stage::kPausedBatterySaver:
     case Stage::kSuccess:
     case Stage::kSyncing:
     case Stage::kStopped:
@@ -317,7 +319,29 @@
       return true;
 
     case Stage::kStopped:
-    case Stage::kPaused:
+    case Stage::kPausedOffline:
+    case Stage::kPausedBatterySaver:
+    case Stage::kSuccess:
+    case Stage::kCannotGetFreeSpace:
+    case Stage::kCannotListFiles:
+    case Stage::kNotEnoughSpace:
+    case Stage::kCannotEnableDocsOffline:
+      return false;
+  }
+
+  NOTREACHED_NORETURN() << "Unexpected Stage " << Quote(stage);
+}
+
+bool IsPaused(const Stage stage) {
+  switch (stage) {
+    case Stage::kPausedOffline:
+    case Stage::kPausedBatterySaver:
+      return true;
+
+    case Stage::kGettingFreeSpace:
+    case Stage::kListingFiles:
+    case Stage::kSyncing:
+    case Stage::kStopped:
     case Stage::kSuccess:
     case Stage::kCannotGetFreeSpace:
     case Stage::kCannotListFiles:
@@ -583,6 +607,9 @@
       space_getter_(base::BindRepeating(&GetFreeSpace)) {
   DCHECK(drivefs_);
   ash::UserDataAuthClient::Get()->AddObserver(this);
+  chromeos::PowerManagerClient::Get()->AddObserver(this);
+  chromeos::PowerManagerClient::Get()->GetBatterySaverModeState(base::BindOnce(
+      &PinManager::OnGotBatterySaverState, weak_ptr_factory_.GetWeakPtr()));
 }
 
 PinManager::~PinManager() {
@@ -590,6 +617,7 @@
 
   StopMonitoringSpace();
   ash::UserDataAuthClient::Get()->RemoveObserver(this);
+  chromeos::PowerManagerClient::Get()->RemoveObserver(this);
 
   DCHECK(!InProgress(progress_.stage))
       << "Pin manager is " << Quote(progress_.stage);
@@ -617,7 +645,12 @@
 
   if (!is_online_) {
     LOG(WARNING) << "Device is currently offline";
-    return Complete(Stage::kPaused);
+    return Complete(Stage::kPausedOffline);
+  }
+
+  if (!is_battery_ok_) {
+    LOG(WARNING) << "Device is currently in battery saver mode";
+    return Complete(Stage::kPausedBatterySaver);
   }
 
   VLOG(2) << "Getting free space";
@@ -708,6 +741,30 @@
   }
 }
 
+void PinManager::OnGotBatterySaverState(
+    absl::optional<power_manager::BatterySaverModeState> state) {
+  if (state) {
+    BatterySaverModeStateChanged(*state);
+  }
+}
+
+void PinManager::BatterySaverModeStateChanged(
+    const power_manager::BatterySaverModeState& state) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  is_battery_ok_ = !state.enabled();
+  VLOG(2) << "Battery saver mode changed, online=" << is_online_
+          << ", battery=" << is_battery_ok_;
+  if (!is_battery_ok_ && InProgress(progress_.stage)) {
+    VLOG(1) << "Pausing for battery saver";
+    return Complete(Stage::kPausedBatterySaver);
+  }
+
+  if (is_online_ && is_battery_ok_ && IsPaused(progress_.stage)) {
+    VLOG(1) << "Restarting from battery saver";
+    Start();
+  }
+}
+
 void PinManager::ListItems(const Id dir_id, Path dir_path) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   VLOG(1) << "Visiting " << dir_id << " " << Quote(dir_path);
@@ -949,8 +1006,12 @@
       VLOG(1) << "Finished with success";
       break;
 
-    case Stage::kPaused:
-      VLOG(1) << "Paused";
+    case Stage::kPausedOffline:
+      VLOG(1) << "Paused offline";
+      break;
+
+    case Stage::kPausedBatterySaver:
+      VLOG(1) << "Paused battery saver";
       break;
 
     case Stage::kStopped:
@@ -1481,18 +1542,18 @@
 }
 
 void PinManager::SetOnline(const bool online) {
-  VLOG(2) << "Online: " << online;
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  VLOG(2) << "Online: " << online << ", battery: " << is_battery_ok_;
   is_online_ = online;
 
-  if (!is_online_ && InProgress(progress_.stage)) {
+  if (InProgress(progress_.stage)) {
     VLOG(1) << "Going offline";
-    return Complete(Stage::kPaused);
+    return Complete(Stage::kPausedOffline);
   }
 
-  if (is_online_ && progress_.stage == Stage::kPaused) {
-    VLOG(1) << "Coming back online";
-    return Start();
+  if (is_online_ && is_battery_ok_ && IsPaused(progress_.stage)) {
+    VLOG(1) << "Restarting from battery saver";
+    Start();
   }
 }
 
diff --git a/chromeos/ash/components/drivefs/drivefs_pin_manager.h b/chromeos/ash/components/drivefs/drivefs_pin_manager.h
index a791030..7084f63 100644
--- a/chromeos/ash/components/drivefs/drivefs_pin_manager.h
+++ b/chromeos/ash/components/drivefs/drivefs_pin_manager.h
@@ -29,6 +29,7 @@
 #include "chromeos/ash/components/drivefs/drivefs_host_observer.h"
 #include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h"
 #include "chromeos/ash/components/drivefs/mojom/pin_manager_types.mojom.h"
+#include "chromeos/dbus/power/power_manager_client.h"
 #include "components/drive/file_errors.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
@@ -149,7 +150,8 @@
 class COMPONENT_EXPORT(CHROMEOS_ASH_COMPONENTS_DRIVEFS) PinManager
     : public DriveFsHostObserver,
       ash::UserDataAuthClient::Observer,
-      ash::SpacedClient::Observer {
+      ash::SpacedClient::Observer,
+      chromeos::PowerManagerClient::Observer {
  public:
   using Path = base::FilePath;
 
@@ -387,6 +389,15 @@
   // ash::SpacedClient::Observer
   void OnSpaceUpdate(const SpaceEvent& event) override;
 
+  // chromeos::PowerManagerClient::Observer
+  void BatterySaverModeStateChanged(
+      const power_manager::BatterySaverModeState& state) override;
+
+  // Callback used to query battery saver state from PowerManagerClient on
+  // startup.
+  void OnGotBatterySaverState(
+      absl::optional<power_manager::BatterySaverModeState> state);
+
   // Starts and stops monitoring space using the SpacedClient::Observer.
   bool StartMonitoringSpace();
   void StopMonitoringSpace();
@@ -416,6 +427,9 @@
   // tests.
   bool is_online_ GUARDED_BY_CONTEXT(sequence_checker_) = true;
 
+  // Is the device battery ok for doing sync (e.g. not in battery saver mode).
+  bool is_battery_ok_ GUARDED_BY_CONTEXT(sequence_checker_) = true;
+
   // Should the feature actually pin files, or should it stop after checking the
   // space requirements?
   bool should_pin_ GUARDED_BY_CONTEXT(sequence_checker_) = true;
@@ -473,7 +487,7 @@
   FRIEND_TEST_ALL_PREFIXES(DriveFsPinManagerTest, NotEnoughSpace3);
   FRIEND_TEST_ALL_PREFIXES(DriveFsPinManagerTest, OnSpaceUpdate);
   FRIEND_TEST_ALL_PREFIXES(DriveFsPinManagerTest, StartMonitoringSpace);
-  FRIEND_TEST_ALL_PREFIXES(DriveFsPinManagerTest, SetOnline);
+  FRIEND_TEST_ALL_PREFIXES(DriveFsPinManagerTest, SetOnlineAndBatteryOk);
   FRIEND_TEST_ALL_PREFIXES(DriveFsPinManagerTest, OnTransientError);
   FRIEND_TEST_ALL_PREFIXES(DriveFsPinManagerTest, OnError);
   FRIEND_TEST_ALL_PREFIXES(DriveFsPinManagerTest, StartWhenInProgress);
diff --git a/chromeos/ash/components/drivefs/drivefs_pin_manager_unittest.cc b/chromeos/ash/components/drivefs/drivefs_pin_manager_unittest.cc
index 207ca50..92e1366a 100644
--- a/chromeos/ash/components/drivefs/drivefs_pin_manager_unittest.cc
+++ b/chromeos/ash/components/drivefs/drivefs_pin_manager_unittest.cc
@@ -30,6 +30,7 @@
 #include "chromeos/ash/components/dbus/userdataauth/userdataauth_client.h"
 #include "chromeos/ash/components/drivefs/mojom/drivefs.mojom-test-utils.h"
 #include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h"
+#include "chromeos/dbus/power/power_manager_client.h"
 #include "components/drive/file_errors.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "testing/gmock/include/gmock/gmock.h"
@@ -215,11 +216,13 @@
   void SetUp() override {
     UserDataAuthClient::InitializeFake();
     SpacedClient::InitializeFake();
+    chromeos::PowerManagerClient::InitializeFake();
   }
 
   void TearDown() override {
     UserDataAuthClient::Shutdown();
     SpacedClient::Shutdown();
+    chromeos::PowerManagerClient::Shutdown();
   }
 
   PinManager::SpaceGetter GetSpaceGetter() {
@@ -242,7 +245,8 @@
   std::unordered_set<std::string> labels;
   for (const Stage stage : {
            Stage::kStopped,
-           Stage::kPaused,
+           Stage::kPausedOffline,
+           Stage::kPausedBatterySaver,
            Stage::kGettingFreeSpace,
            Stage::kListingFiles,
            Stage::kSyncing,
@@ -274,7 +278,8 @@
 
   for (const Stage stage : {
            Stage::kStopped,
-           Stage::kPaused,
+           Stage::kPausedOffline,
+           Stage::kPausedBatterySaver,
            Stage::kGettingFreeSpace,
            Stage::kListingFiles,
            Stage::kSyncing,
@@ -2389,13 +2394,14 @@
 }
 
 // Tests PinManager::SetOnline().
-TEST_F(DriveFsPinManagerTest, SetOnline) {
+TEST_F(DriveFsPinManagerTest, SetOnlineAndBatteryOk) {
   PinManager manager(profile_path_, mount_path_, &drivefs_);
   manager.SetSpaceGetter(GetSpaceGetter());
 
   DCHECK_CALLED_ON_VALID_SEQUENCE(manager.sequence_checker_);
   EXPECT_EQ(manager.progress_.stage, Stage::kStopped);
   EXPECT_TRUE(manager.is_online_);
+  EXPECT_TRUE(manager.is_battery_ok_);
 
   manager.SetOnline(false);
   EXPECT_EQ(manager.progress_.stage, Stage::kStopped);
@@ -2409,8 +2415,50 @@
   EXPECT_EQ(manager.progress_.stage, Stage::kStopped);
   EXPECT_FALSE(manager.is_online_);
 
+  power_manager::BatterySaverModeState state;
+  state.set_enabled(true);
+  manager.BatterySaverModeStateChanged(state);
+  EXPECT_EQ(manager.progress_.stage, Stage::kStopped);
+  EXPECT_FALSE(manager.is_battery_ok_);
+
+  state.set_enabled(false);
+  manager.BatterySaverModeStateChanged(state);
+  EXPECT_EQ(manager.progress_.stage, Stage::kStopped);
+  EXPECT_TRUE(manager.is_battery_ok_);
+
+  manager.SetOnline(false);
+  state.set_enabled(true);
+  manager.BatterySaverModeStateChanged(state);
+  EXPECT_FALSE(manager.is_online_);
+  EXPECT_FALSE(manager.is_battery_ok_);
+
   manager.Start();
-  EXPECT_EQ(manager.progress_.stage, Stage::kPaused);
+  EXPECT_EQ(manager.progress_.stage, Stage::kPausedOffline);
+  EXPECT_FALSE(manager.is_online_);
+  EXPECT_FALSE(manager.is_battery_ok_);
+
+  manager.SetOnline(true);
+  EXPECT_EQ(manager.progress_.stage, Stage::kPausedOffline);
+  EXPECT_CALL(space_getter_, GetFreeSpace(gcache_dir_, _)).Times(1);
+  state.set_enabled(false);
+  manager.BatterySaverModeStateChanged(state);
+  EXPECT_EQ(manager.progress_.stage, Stage::kGettingFreeSpace);
+  state.set_enabled(true);
+  manager.BatterySaverModeStateChanged(state);
+  EXPECT_EQ(manager.progress_.stage, Stage::kPausedBatterySaver);
+
+  manager.SetOnline(false);
+  state.set_enabled(false);
+  manager.BatterySaverModeStateChanged(state);
+  EXPECT_EQ(manager.progress_.stage, Stage::kPausedBatterySaver);
+
+  EXPECT_CALL(space_getter_, GetFreeSpace(gcache_dir_, _)).Times(1);
+  manager.SetOnline(true);
+  EXPECT_EQ(manager.progress_.stage, Stage::kGettingFreeSpace);
+  EXPECT_TRUE(manager.is_online_);
+
+  manager.SetOnline(false);
+  EXPECT_EQ(manager.progress_.stage, Stage::kPausedOffline);
   EXPECT_FALSE(manager.is_online_);
 
   EXPECT_CALL(space_getter_, GetFreeSpace(gcache_dir_, _)).Times(1);
@@ -2419,16 +2467,7 @@
   EXPECT_TRUE(manager.is_online_);
 
   manager.SetOnline(false);
-  EXPECT_EQ(manager.progress_.stage, Stage::kPaused);
-  EXPECT_FALSE(manager.is_online_);
-
-  EXPECT_CALL(space_getter_, GetFreeSpace(gcache_dir_, _)).Times(1);
-  manager.SetOnline(true);
-  EXPECT_EQ(manager.progress_.stage, Stage::kGettingFreeSpace);
-  EXPECT_TRUE(manager.is_online_);
-
-  manager.SetOnline(false);
-  EXPECT_EQ(manager.progress_.stage, Stage::kPaused);
+  EXPECT_EQ(manager.progress_.stage, Stage::kPausedOffline);
   EXPECT_FALSE(manager.is_online_);
 
   manager.Stop();
diff --git a/chromeos/ash/components/drivefs/mojom/pin_manager_types.mojom b/chromeos/ash/components/drivefs/mojom/pin_manager_types.mojom
index f301a0a..911bd98 100644
--- a/chromeos/ash/components/drivefs/mojom/pin_manager_types.mojom
+++ b/chromeos/ash/components/drivefs/mojom/pin_manager_types.mojom
@@ -14,19 +14,20 @@
   kStopped = 0,
 
   // Paused because of unfavorable network conditions.
-  kPaused = 1,
+  kPausedOffline = 1,
+  kPausedBatterySaver = 2,
 
   // In-progress stages.
-  kGettingFreeSpace = 2,
-  kListingFiles = 3,
-  kSyncing = 4,
+  kGettingFreeSpace = 3,
+  kListingFiles = 4,
+  kSyncing = 5,
 
   // Final success stage.
-  kSuccess = 5,
+  kSuccess = 6,
 
   // Final error stages.
-  kCannotGetFreeSpace = 6,
-  kCannotListFiles = 7,
-  kNotEnoughSpace = 8,
-  kCannotEnableDocsOffline = 9,
+  kCannotGetFreeSpace = 7,
+  kCannotListFiles = 8,
+  kNotEnoughSpace = 9,
+  kCannotEnableDocsOffline = 10,
 };
diff --git a/chromeos/ash/services/assistant/DEPS b/chromeos/ash/services/assistant/DEPS
index 9e86a36..b20d3c13 100644
--- a/chromeos/ash/services/assistant/DEPS
+++ b/chromeos/ash/services/assistant/DEPS
@@ -12,7 +12,6 @@
   "+services/device/public",
   "+services/media_session/public",
   "+services/network/public",
-  "+ui/accessibility/accessibility_switches.h",
   "+ui/accessibility/ax_assistant_structure.h",
   "+ui/accessibility/mojom",
   "+ui/base",
diff --git a/chromeos/crosapi/mojom/web_app_types.mojom b/chromeos/crosapi/mojom/web_app_types.mojom
index 9c430115..5df0347d 100644
--- a/chromeos/crosapi/mojom/web_app_types.mojom
+++ b/chromeos/crosapi/mojom/web_app_types.mojom
@@ -48,7 +48,8 @@
   kSuccess,
   kNoAppToUninstall,
   kCancelled,
-  kError,
+  [Default] kError,
+  [MinVersion=1] kShutdown,
 };
 
 // A subset of |WebAppInstallInfo| necessary to install a web app originated in
diff --git a/chromeos/crosapi/mojom/web_app_types_mojom_traits.cc b/chromeos/crosapi/mojom/web_app_types_mojom_traits.cc
index 466a938..91eede00 100644
--- a/chromeos/crosapi/mojom/web_app_types_mojom_traits.cc
+++ b/chromeos/crosapi/mojom/web_app_types_mojom_traits.cc
@@ -192,6 +192,8 @@
       return crosapi::mojom::WebAppUninstallResultCode::kCancelled;
     case webapps::UninstallResultCode::kError:
       return crosapi::mojom::WebAppUninstallResultCode::kError;
+    case webapps::UninstallResultCode::kShutdown:
+      return crosapi::mojom::WebAppUninstallResultCode::kShutdown;
   };
 }
 
@@ -212,6 +214,9 @@
     case crosapi::mojom::WebAppUninstallResultCode::kError:
       *output = webapps::UninstallResultCode::kError;
       return true;
+    case crosapi::mojom::WebAppUninstallResultCode::kShutdown:
+      *output = webapps::UninstallResultCode::kShutdown;
+      return true;
   };
 
   NOTREACHED();
diff --git a/components/exo/buffer_unittest.cc b/components/exo/buffer_unittest.cc
index b45c219..c46831a3 100644
--- a/components/exo/buffer_unittest.cc
+++ b/components/exo/buffer_unittest.cc
@@ -7,6 +7,7 @@
 #include <GLES2/gl2extchromium.h>
 
 #include "base/barrier_closure.h"
+#include "base/functional/callback_helpers.h"
 #include "base/run_loop.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
@@ -82,6 +83,29 @@
   ri->VerifySyncTokensCHROMIUM(sync_tokens.data(), sync_tokens.size());
 }
 
+viz::CompositorFrame CreateCompositorFrame(
+    SurfaceTreeHost* surface_tree_host,
+    const gfx::Rect& output_rect,
+    const gfx::Rect& damage_rect,
+    std::vector<viz::TransferableResource> resources) {
+  viz::CompositorFrame frame;
+  frame.metadata.begin_frame_ack.frame_id =
+      viz::BeginFrameId(viz::BeginFrameArgs::kManualSourceId,
+                        viz::BeginFrameArgs::kStartingFrameNumber);
+  frame.metadata.begin_frame_ack.has_damage = true;
+  frame.metadata.frame_token = surface_tree_host->GenerateNextFrameToken();
+  frame.metadata.device_scale_factor = 1;
+  auto pass = viz::CompositorRenderPass::Create();
+  pass->SetNew(viz::CompositorRenderPassId{1}, output_rect, damage_rect,
+               gfx::Transform());
+  frame.render_pass_list.push_back(std::move(pass));
+  frame.resource_list = std::move(resources);
+  if (!frame.resource_list.empty()) {
+    VerifySyncTokensInCompositorFrame(&frame);
+  }
+  return frame;
+}
+
 // Instantiate the values of disabling/enabling reactive frame submission in the
 // parameterized tests.
 INSTANTIATE_TEST_SUITE_P(All, BufferTest, testing::Values(false, true));
@@ -311,24 +335,10 @@
   ASSERT_TRUE(rv);
 
   // Submit frame with resource.
-  {
-    viz::CompositorFrame frame;
-    frame.metadata.begin_frame_ack.frame_id.source_id =
-        viz::BeginFrameArgs::kManualSourceId;
-    frame.metadata.begin_frame_ack.frame_id.sequence_number =
-        viz::BeginFrameArgs::kStartingFrameNumber;
-    frame.metadata.begin_frame_ack.has_damage = true;
-    frame.metadata.frame_token = shell_surface->GenerateNextFrameToken();
-    frame.metadata.device_scale_factor = 1;
-    auto pass = viz::CompositorRenderPass::Create();
-    pass->SetNew(viz::CompositorRenderPassId{1}, gfx::Rect(buffer_size),
-                 gfx::Rect(buffer_size), gfx::Transform());
-    frame.render_pass_list.push_back(std::move(pass));
-    frame.resource_list.push_back(resource);
-    VerifySyncTokensInCompositorFrame(&frame);
-    shell_surface->SubmitCompositorFrameForTesting(std::move(frame));
-    test::WaitForLastFrameAck(shell_surface.get());
-  }
+  shell_surface->SubmitCompositorFrameForTesting(
+      CreateCompositorFrame(shell_surface.get(), gfx::Rect(buffer_size),
+                            gfx::Rect(buffer_size), {resource}));
+  test::WaitForLastFrameAck(shell_surface.get());
 
   buffer->OnDetach();
 
@@ -386,20 +396,9 @@
 
   // Submit frame with resource.
   {
-    viz::CompositorFrame frame;
-    frame.metadata.begin_frame_ack.frame_id =
-        viz::BeginFrameId(viz::BeginFrameArgs::kManualSourceId,
-                          viz::BeginFrameArgs::kStartingFrameNumber);
-    frame.metadata.begin_frame_ack.has_damage = true;
-    frame.metadata.frame_token = shell_surface->GenerateNextFrameToken();
-    frame.metadata.device_scale_factor = 1;
-    auto pass = viz::CompositorRenderPass::Create();
-    pass->SetNew(viz::CompositorRenderPassId{1}, gfx::Rect(buffer_size),
-                 gfx::Rect(buffer_size), gfx::Transform());
-    frame.render_pass_list.push_back(std::move(pass));
-    frame.resource_list.push_back(resource);
-    VerifySyncTokensInCompositorFrame(&frame);
-    shell_surface->SubmitCompositorFrameForTesting(std::move(frame));
+    shell_surface->SubmitCompositorFrameForTesting(
+        CreateCompositorFrame(shell_surface.get(), gfx::Rect(buffer_size),
+                              gfx::Rect(buffer_size), {resource}));
     test::WaitForLastFrameAck(shell_surface.get());
 
     // Try to release buffer in last frame. This can happen during a resize
@@ -423,20 +422,8 @@
   ASSERT_EQ(release_resource_count, 0);
 
   // Submit frame without resource. This should cause buffer to be released.
-  {
-    viz::CompositorFrame frame;
-    frame.metadata.begin_frame_ack.frame_id =
-        viz::BeginFrameId(viz::BeginFrameArgs::kManualSourceId,
-                          viz::BeginFrameArgs::kStartingFrameNumber);
-    frame.metadata.begin_frame_ack.has_damage = true;
-    frame.metadata.frame_token = shell_surface->GenerateNextFrameToken();
-    frame.metadata.device_scale_factor = 1;
-    auto pass = viz::CompositorRenderPass::Create();
-    pass->SetNew(viz::CompositorRenderPassId{1}, gfx::Rect(buffer_size),
-                 gfx::Rect(buffer_size), gfx::Transform());
-    frame.render_pass_list.push_back(std::move(pass));
-    shell_surface->SubmitCompositorFrameForTesting(std::move(frame));
-  }
+  shell_surface->SubmitCompositorFrameForTesting(CreateCompositorFrame(
+      shell_surface.get(), gfx::Rect(buffer_size), gfx::Rect(buffer_size), {}));
 
   run_loop.Run();
   // Release() should have been called exactly once.
@@ -444,5 +431,321 @@
   ASSERT_EQ(release_resource_count, 1);
 }
 
+// Tests that only apply if ExoReactiveFrameSubmission is enabled.
+class ReactiveFrameSubmissionBufferTest : public test::ExoTestBase {
+ public:
+  ReactiveFrameSubmissionBufferTest()
+      : test::ExoTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
+    feature_list_.InitAndEnableFeature(kExoReactiveFrameSubmission);
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+class TestLayerTreeFrameSinkHolder : public LayerTreeFrameSinkHolder {
+ public:
+  TestLayerTreeFrameSinkHolder(
+      SurfaceTreeHost* surface_tree_host,
+      std::unique_ptr<cc::LayerTreeFrameSink> frame_sink)
+      : LayerTreeFrameSinkHolder(surface_tree_host, std::move(frame_sink)) {}
+  ~TestLayerTreeFrameSinkHolder() override = default;
+
+  using PreReclaimCallback =
+      base::RepeatingCallback<void(const std::vector<viz::ReturnedResource>&)>;
+  void set_pre_reclaim_callback(PreReclaimCallback callback) {
+    pre_reclaim_callback_ = std::move(callback);
+  }
+
+  void set_post_reclaim_callback(base::RepeatingClosure callback) {
+    post_reclaim_callback_ = std::move(callback);
+  }
+
+  void ReclaimResources(std::vector<viz::ReturnedResource> resources) override {
+    if (pre_reclaim_callback_) {
+      pre_reclaim_callback_.Run(resources);
+    }
+
+    LayerTreeFrameSinkHolder::ReclaimResources(std::move(resources));
+
+    if (post_reclaim_callback_) {
+      post_reclaim_callback_.Run();
+    }
+  }
+
+ private:
+  PreReclaimCallback pre_reclaim_callback_;
+  base::RepeatingClosure post_reclaim_callback_;
+};
+
+TEST_F(ReactiveFrameSubmissionBufferTest,
+       SurfaceTreeHostNotReclaimCachedFrameResources) {
+  gfx::Size buffer_size(256, 256);
+
+  auto shell_surface =
+      test::ShellSurfaceBuilder(buffer_size).SetNoCommit().BuildShellSurface();
+  test::SetLayerTreeFrameSinkHolderFactory<TestLayerTreeFrameSinkHolder>(
+      shell_surface.get());
+  shell_surface->root_surface()->Commit();
+  test::WaitForLastFrameAck(shell_surface.get());
+
+  TestLayerTreeFrameSinkHolder* frame_sink_holder =
+      static_cast<TestLayerTreeFrameSinkHolder*>(
+          shell_surface->layer_tree_frame_sink_holder());
+
+  auto buffer = std::make_unique<Buffer>(
+      exo_test_helper()->CreateGpuMemoryBuffer(buffer_size));
+
+  // Remove wait time for efficiency.
+  buffer->set_wait_for_release_delay_for_testing(base::TimeDelta());
+
+  int release_call_count = 0;
+
+  base::RunLoop run_loop1;
+  auto combined_quit_closure = BarrierClosure(2, run_loop1.QuitClosure());
+
+  buffer->set_release_callback(
+      CreateReleaseBufferClosure(&release_call_count, combined_quit_closure));
+
+  buffer->OnAttach();
+  viz::TransferableResource resource;
+  // Produce a transferable resource for the contents of the buffer.
+  int release_resource_count = 0;
+  bool rv = buffer->ProduceTransferableResource(
+      frame_sink_holder->resource_manager(), nullptr, false, &resource,
+      gfx::ColorSpace::CreateSRGB(), nullptr,
+      CreateExplicitReleaseCallback(&release_resource_count,
+                                    combined_quit_closure));
+  ASSERT_TRUE(rv);
+
+  // Submit frame with `resource`.
+  shell_surface->SubmitCompositorFrameForTesting(
+      CreateCompositorFrame(shell_surface.get(), gfx::Rect(buffer_size),
+                            gfx::Rect(buffer_size), {resource}));
+  test::WaitForLastFrameAck(shell_surface.get());
+
+  base::RunLoop run_loop2;
+  // Set a callback that will be called when the remote side notify
+  // ReclaimResources for `resource`.
+  frame_sink_holder->set_pre_reclaim_callback(base::BindLambdaForTesting(
+      [&](const std::vector<viz::ReturnedResource>& resources) {
+        // Skip if it is not a notification for reclaiming `resource`.
+        if (!base::Contains(
+                resources, resource.id,
+                [](const viz::ReturnedResource& r) { return r.id; })) {
+          return;
+        }
+
+        run_loop2.Quit();
+        // Make sure that this callback is only used once.
+        frame_sink_holder->set_pre_reclaim_callback({});
+
+        frame_sink_holder->ClearPendingBeginFramesForTesting();
+        // Cause a frame with `resource` is cached. This should hold off
+        // reclaming `resource`.
+        shell_surface->SubmitCompositorFrameForTesting(
+            CreateCompositorFrame(shell_surface.get(), gfx::Rect(buffer_size),
+                                  gfx::Rect(buffer_size), {resource}));
+      }));
+
+  // Submit a new frame without resource to cause the remote side to stop using
+  // `resource` and notify ReclaimResources. The callback set by
+  // set_pre_reclaim_callback() above should be run as a result.
+  shell_surface->SubmitCompositorFrameForTesting(CreateCompositorFrame(
+      shell_surface.get(), gfx::Rect(buffer_size), gfx::Rect(buffer_size), {}));
+  run_loop2.Run();
+
+  // Ensure the cached frame is submitted.
+  test::WaitForLastFrameAck(shell_surface.get());
+
+  // We expect that Release() is not called, no matter whether we have a wait
+  // here or how long the wait is. An arbitrary time period is added here so
+  // that if the event mistakenly happens, it is more likely to find out.
+  task_environment()->FastForwardBy(base::Seconds(1));
+
+  // Release() should not have been called.
+  ASSERT_EQ(release_call_count, 0);
+  ASSERT_EQ(release_resource_count, 0);
+
+  buffer->OnDetach();
+
+  // Submit a new frame without resource. This will cause buffer to be released.
+  shell_surface->SubmitCompositorFrameForTesting(CreateCompositorFrame(
+      shell_surface.get(), gfx::Rect(buffer_size), gfx::Rect(buffer_size), {}));
+
+  run_loop1.Run();
+  // Release() should have been called exactly once.
+  ASSERT_EQ(release_call_count, 1);
+  ASSERT_EQ(release_resource_count, 1);
+}
+
+TEST_F(ReactiveFrameSubmissionBufferTest,
+       SurfaceTreeHostDiscardFrameNotReclaimNewFrameResources) {
+  gfx::Size buffer_size(256, 256);
+
+  auto shell_surface =
+      test::ShellSurfaceBuilder(buffer_size).BuildShellSurface();
+  test::WaitForLastFrameAck(shell_surface.get());
+
+  LayerTreeFrameSinkHolder* frame_sink_holder =
+      shell_surface->layer_tree_frame_sink_holder();
+
+  auto buffer = std::make_unique<Buffer>(
+      exo_test_helper()->CreateGpuMemoryBuffer(buffer_size));
+
+  // Remove wait time for efficiency.
+  buffer->set_wait_for_release_delay_for_testing(base::TimeDelta());
+
+  int release_call_count = 0;
+
+  base::RunLoop run_loop;
+  auto combined_quit_closure = BarrierClosure(2, run_loop.QuitClosure());
+
+  buffer->set_release_callback(
+      CreateReleaseBufferClosure(&release_call_count, combined_quit_closure));
+
+  buffer->OnAttach();
+  viz::TransferableResource resource;
+  // Produce a transferable resource for the contents of the buffer.
+  int release_resource_count = 0;
+  bool rv = buffer->ProduceTransferableResource(
+      frame_sink_holder->resource_manager(), nullptr, false, &resource,
+      gfx::ColorSpace::CreateSRGB(), nullptr,
+      CreateExplicitReleaseCallback(&release_resource_count,
+                                    combined_quit_closure));
+  ASSERT_TRUE(rv);
+
+  frame_sink_holder->ClearPendingBeginFramesForTesting();
+
+  // Submit a frame with `resource`, which will be cached.
+  shell_surface->SubmitCompositorFrameForTesting(
+      CreateCompositorFrame(shell_surface.get(), gfx::Rect(buffer_size),
+                            gfx::Rect(buffer_size), {resource}));
+
+  // Submit another frame with `resource`. It will cause the previously cached
+  // frame to be evicted.
+  shell_surface->SubmitCompositorFrameForTesting(
+      CreateCompositorFrame(shell_surface.get(), gfx::Rect(buffer_size),
+                            gfx::Rect(buffer_size), {resource}));
+
+  buffer->OnDetach();
+
+  // Ensure the cached frame is submitted.
+  test::WaitForLastFrameAck(shell_surface.get());
+
+  // We expect that Release() is not called, no matter whether we have a wait
+  // here or how long the wait is. An arbitrary time period is added here so
+  // that if the event mistakenly happens, it is more likely to find out.
+  task_environment()->FastForwardBy(base::Seconds(1));
+
+  // Release() should not have been called.
+  ASSERT_EQ(release_call_count, 0);
+  ASSERT_EQ(release_resource_count, 0);
+
+  // Submit another frame without resource.
+  shell_surface->SubmitCompositorFrameForTesting(CreateCompositorFrame(
+      shell_surface.get(), gfx::Rect(buffer_size), gfx::Rect(buffer_size), {}));
+  test::WaitForLastFrameAck(shell_surface.get());
+
+  run_loop.Run();
+
+  // Release() should have been called exactly once.
+  ASSERT_EQ(release_call_count, 1);
+  ASSERT_EQ(release_resource_count, 1);
+}
+
+TEST_F(ReactiveFrameSubmissionBufferTest,
+       SurfaceTreeHostDiscardFrameNotReclaimInUseResources) {
+  gfx::Size buffer_size(256, 256);
+
+  auto shell_surface =
+      test::ShellSurfaceBuilder(buffer_size).SetNoCommit().BuildShellSurface();
+  test::SetLayerTreeFrameSinkHolderFactory<TestLayerTreeFrameSinkHolder>(
+      shell_surface.get());
+  shell_surface->root_surface()->Commit();
+  test::WaitForLastFrameAck(shell_surface.get());
+
+  TestLayerTreeFrameSinkHolder* frame_sink_holder =
+      static_cast<TestLayerTreeFrameSinkHolder*>(
+          shell_surface->layer_tree_frame_sink_holder());
+
+  auto buffer = std::make_unique<Buffer>(
+      exo_test_helper()->CreateGpuMemoryBuffer(buffer_size));
+
+  // Remove wait time for efficiency.
+  buffer->set_wait_for_release_delay_for_testing(base::TimeDelta());
+
+  int release_call_count = 0;
+
+  base::RunLoop run_loop1;
+  auto combined_quit_closure = BarrierClosure(2, run_loop1.QuitClosure());
+
+  buffer->set_release_callback(
+      CreateReleaseBufferClosure(&release_call_count, combined_quit_closure));
+
+  buffer->OnAttach();
+  viz::TransferableResource resource;
+  // Produce a transferable resource for the contents of the buffer.
+  int release_resource_count = 0;
+  bool rv = buffer->ProduceTransferableResource(
+      frame_sink_holder->resource_manager(), nullptr, false, &resource,
+      gfx::ColorSpace::CreateSRGB(), nullptr,
+      CreateExplicitReleaseCallback(&release_resource_count,
+                                    combined_quit_closure));
+  ASSERT_TRUE(rv);
+
+  // Submit frame with `resource`.
+  shell_surface->SubmitCompositorFrameForTesting(
+      CreateCompositorFrame(shell_surface.get(), gfx::Rect(buffer_size),
+                            gfx::Rect(buffer_size), {resource}));
+  test::WaitForLastFrameAck(shell_surface.get());
+
+  base::RunLoop run_loop2;
+  // Set a callback that will be called when the remote side notify
+  // ReclaimResources for `resource`.
+  frame_sink_holder->set_pre_reclaim_callback(base::BindLambdaForTesting(
+      [&](const std::vector<viz::ReturnedResource>& resources) {
+        // Skip if it is not a notification for reclaiming `resource`.
+        if (!base::Contains(
+                resources, resource.id,
+                [](const viz::ReturnedResource& r) { return r.id; })) {
+          return;
+        }
+
+        run_loop2.Quit();
+        // Make sure that this callback is only used once.
+        frame_sink_holder->set_pre_reclaim_callback({});
+
+        // The evicted cached frame with `resource` shouldn't have caused
+        // Release() to be called.
+        ASSERT_EQ(release_call_count, 0);
+        ASSERT_EQ(release_resource_count, 0);
+      }));
+
+  frame_sink_holder->ClearPendingBeginFramesForTesting();
+
+  // Cause a frame with `resource` is cached.
+  shell_surface->SubmitCompositorFrameForTesting(
+      CreateCompositorFrame(shell_surface.get(), gfx::Rect(buffer_size),
+                            gfx::Rect(buffer_size), {resource}));
+
+  buffer->OnDetach();
+
+  // Submit a new frame without resource to evict the previously cached frame.
+  // It shouldn't cause `resource` to be reclaimed because it is still in use at
+  // the remote side.
+  shell_surface->SubmitCompositorFrameForTesting(CreateCompositorFrame(
+      shell_surface.get(), gfx::Rect(buffer_size), gfx::Rect(buffer_size), {}));
+
+  // Wait for the remote site to notify ReclaimResources for `resource`.
+  run_loop2.Run();
+
+  run_loop1.Run();
+
+  // Release() should have been called exactly once.
+  ASSERT_EQ(release_call_count, 1);
+  ASSERT_EQ(release_resource_count, 1);
+}
+
 }  // namespace
 }  // namespace exo
diff --git a/components/exo/layer_tree_frame_sink_holder.cc b/components/exo/layer_tree_frame_sink_holder.cc
index 40a2cc2..cea0bf6 100644
--- a/components/exo/layer_tree_frame_sink_holder.cc
+++ b/components/exo/layer_tree_frame_sink_holder.cc
@@ -36,7 +36,7 @@
 }
 
 LayerTreeFrameSinkHolder::~LayerTreeFrameSinkHolder() {
-  DiscardCachedFrame();
+  DiscardCachedFrame(nullptr);
 
   if (frame_sink_)
     frame_sink_->DetachFromClient();
@@ -103,7 +103,7 @@
 
   frame_timing_history_->MayRecordDidNotProduceToFrameArrvial(/*valid=*/true);
 
-  DiscardCachedFrame();
+  DiscardCachedFrame(&frame);
 
   if (!ShouldSubmitFrameNow()) {
     cached_frame_ = std::move(frame);
@@ -148,6 +148,17 @@
     if (base::Contains(last_frame_resources_, resource.id)) {
       continue;
     }
+    in_use_resources_.erase(resource.id);
+
+    // Skip resources that are also in the cached frame.
+    if (cached_frame_ &&
+        base::Contains(cached_frame_->resource_list, resource.id,
+                       [](const viz::TransferableResource& resource) {
+                         return resource.id;
+                       })) {
+      continue;
+    }
+
     resource_manager_.ReclaimResource(std::move(resource));
   }
 
@@ -204,6 +215,7 @@
   StopProcessingPendingFrames();
 
   last_frame_resources_.clear();
+  in_use_resources_.clear();
   resource_manager_.ClearAllCallbacks();
   is_lost_ = true;
 
@@ -211,6 +223,12 @@
     ScheduleDelete();
 }
 
+void LayerTreeFrameSinkHolder::ClearPendingBeginFramesForTesting() {
+  while (!pending_begin_frames_.empty()) {
+    OnSendDeadlineExpired(/*update_timer=*/false);
+  };
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // LayerTreeFrameSinkHolder, private:
 
@@ -281,6 +299,7 @@
   last_frame_resources_.clear();
   for (auto& resource : frame->resource_list) {
     last_frame_resources_.push_back(resource.id);
+    in_use_resources_.insert(resource.id);
   }
   frame_sink_->SubmitCompositorFrame(std::move(*frame),
                                      /*hit_test_data_changed=*/true);
@@ -288,7 +307,8 @@
   pending_submit_frames_++;
 }
 
-void LayerTreeFrameSinkHolder::DiscardCachedFrame() {
+void LayerTreeFrameSinkHolder::DiscardCachedFrame(
+    const viz::CompositorFrame* new_frame) {
   if (!cached_frame_) {
     return;
   }
@@ -296,6 +316,19 @@
   DCHECK(reactive_frame_submission_);
 
   for (const auto& resource : cached_frame_->resource_list) {
+    // Skip if the resource is still in use by the remote side.
+    if (in_use_resources_.contains(resource.id)) {
+      continue;
+    }
+
+    // Skip if the resource is also in `new_frame`.
+    if (new_frame &&
+        base::Contains(new_frame->resource_list, resource.id,
+                       [](const viz::TransferableResource& resource) {
+                         return resource.id;
+                       })) {
+      continue;
+    }
     resource_manager_.ReclaimResource(resource.ToReturnedResource());
   }
 
@@ -328,7 +361,7 @@
 }
 
 void LayerTreeFrameSinkHolder::StopProcessingPendingFrames() {
-  DiscardCachedFrame();
+  DiscardCachedFrame(nullptr);
   pending_begin_frames_ = {};
   UpdateSubmitFrameTimer();
 }
diff --git a/components/exo/layer_tree_frame_sink_holder.h b/components/exo/layer_tree_frame_sink_holder.h
index 829279d..d4dc50c 100644
--- a/components/exo/layer_tree_frame_sink_holder.h
+++ b/components/exo/layer_tree_frame_sink_holder.h
@@ -89,6 +89,8 @@
       const gfx::Rect& viewport_rect,
       const gfx::Transform& transform) override {}
 
+  void ClearPendingBeginFramesForTesting();
+
  private:
   struct PendingBeginFrame {
     viz::BeginFrameAck begin_frame_ack;
@@ -108,7 +110,7 @@
 
   // Discards `cached_frame_`, reclaims resources and returns failure
   // presentation feedback.
-  void DiscardCachedFrame();
+  void DiscardCachedFrame(const viz::CompositorFrame* new_frame);
   void SendDiscardedFrameNotifications(uint32_t frame_token);
 
   void StopProcessingPendingFrames();
@@ -134,6 +136,9 @@
 
   absl::optional<viz::CompositorFrame> cached_frame_;
 
+  // Resources that are submitted and still in use by the remote side.
+  std::set<viz::ResourceId> in_use_resources_;
+
   bool is_lost_ = false;
   bool delete_pending_ = false;
 
diff --git a/components/exo/surface_tree_host.cc b/components/exo/surface_tree_host.cc
index dcfbffdd..6359e44 100644
--- a/components/exo/surface_tree_host.cc
+++ b/components/exo/surface_tree_host.cc
@@ -103,7 +103,10 @@
 SurfaceTreeHost::SurfaceTreeHost(const std::string& window_name)
     : host_window_(
           std::make_unique<aura::Window>(nullptr,
-                                         aura::client::WINDOW_TYPE_CONTROL)) {
+                                         aura::client::WINDOW_TYPE_CONTROL)),
+      frame_sink_holder_factory_(
+          base::BindRepeating(&SurfaceTreeHost::CreateLayerTreeFrameSinkHolder,
+                              base::Unretained(this))) {
   InitHostWindow(window_name);
   context_provider_ = aura::Env::GetInstance()
                           ->context_factory()
@@ -208,6 +211,14 @@
   InitHostWindow(window_name);
 }
 
+void SurfaceTreeHost::SetLayerTreeFrameSinkHolderFactoryForTesting(
+    LayerTreeFrameSinkHolderFactory frame_sink_holder_factory) {
+  DCHECK(frame_callbacks_.empty() && active_presentation_callbacks_.empty());
+
+  frame_sink_holder_factory_ = std::move(frame_sink_holder_factory);
+  layer_tree_frame_sink_holder_ = frame_sink_holder_factory_.Run();
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // SurfaceDelegate overrides:
 
@@ -423,8 +434,7 @@
   host_window_->SetEventTargetingPolicy(
       aura::EventTargetingPolicy::kDescendantsOnly);
   host_window_->SetEventTargeter(std::make_unique<CustomWindowTargeter>(this));
-  layer_tree_frame_sink_holder_ = std::make_unique<LayerTreeFrameSinkHolder>(
-      this, host_window_->CreateLayerTreeFrameSink());
+  layer_tree_frame_sink_holder_ = frame_sink_holder_factory_.Run();
 }
 
 viz::CompositorFrame SurfaceTreeHost::PrepareToSubmitCompositorFrame() {
@@ -433,8 +443,7 @@
   if (layer_tree_frame_sink_holder_->is_lost()) {
     // We can immediately delete the old LayerTreeFrameSinkHolder because all of
     // it's resources are lost anyways.
-    layer_tree_frame_sink_holder_ = std::make_unique<LayerTreeFrameSinkHolder>(
-        this, host_window_->CreateLayerTreeFrameSink());
+    layer_tree_frame_sink_holder_ = frame_sink_holder_factory_.Run();
     CleanUpCallbacks();
   }
 
@@ -526,4 +535,10 @@
   active_presentation_callbacks_.clear();
 }
 
+std::unique_ptr<LayerTreeFrameSinkHolder>
+SurfaceTreeHost::CreateLayerTreeFrameSinkHolder() {
+  return std::make_unique<LayerTreeFrameSinkHolder>(
+      this, host_window_->CreateLayerTreeFrameSink());
+}
+
 }  // namespace exo
diff --git a/components/exo/surface_tree_host.h b/components/exo/surface_tree_host.h
index 3fd1035..be6b6635 100644
--- a/components/exo/surface_tree_host.h
+++ b/components/exo/surface_tree_host.h
@@ -147,6 +147,14 @@
   void SetHostWindowForTesting(std::unique_ptr<aura::Window> test_host_window,
                                const std::string& window_name);
 
+  using LayerTreeFrameSinkHolderFactory =
+      base::RepeatingCallback<std::unique_ptr<LayerTreeFrameSinkHolder>()>;
+
+  // It should only be used at initialization time before any frames are
+  // submitted.
+  void SetLayerTreeFrameSinkHolderFactoryForTesting(
+      LayerTreeFrameSinkHolderFactory frame_sink_holder_factory);
+
  protected:
   void UpdateDisplayOnTree();
 
@@ -189,6 +197,8 @@
 
   void CleanUpCallbacks();
 
+  std::unique_ptr<LayerTreeFrameSinkHolder> CreateLayerTreeFrameSinkHolder();
+
   raw_ptr<Surface, ExperimentalAsh> root_surface_ = nullptr;
 
   // Position of root surface relative to topmost, leftmost sub-surface. The
@@ -197,6 +207,7 @@
 
   std::unique_ptr<aura::Window> host_window_;
   std::unique_ptr<LayerTreeFrameSinkHolder> layer_tree_frame_sink_holder_;
+  LayerTreeFrameSinkHolderFactory frame_sink_holder_factory_;
 
   // This queue contains lists the callbacks to notify the client when it is a
   // good time to start producing a new frame. Each list corresponds to a
diff --git a/components/exo/surface_unittest.cc b/components/exo/surface_unittest.cc
index e7f0b63..43ab364 100644
--- a/components/exo/surface_unittest.cc
+++ b/components/exo/surface_unittest.cc
@@ -1822,10 +1822,8 @@
 
   surface->Attach(buffer.get());
 
-  // This commit will ensure that if there is already a pending BeginFrame
-  // request, it is responded and cleared.
-  surface->Damage(gfx::Rect(0, 0, 10, 10));
-  surface->Commit();
+  shell_surface->layer_tree_frame_sink_holder()
+      ->ClearPendingBeginFramesForTesting();
 
   // This will result in a cached frame in LayerTreeFrameSinkHolder.
   surface->Damage(gfx::Rect(10, 10, 10, 10));
diff --git a/components/exo/test/surface_tree_host_test_util.h b/components/exo/test/surface_tree_host_test_util.h
index 90a8ca14..f888508 100644
--- a/components/exo/test/surface_tree_host_test_util.h
+++ b/components/exo/test/surface_tree_host_test_util.h
@@ -5,6 +5,7 @@
 #ifndef COMPONENTS_EXO_TEST_SURFACE_TREE_HOST_TEST_UTIL_H_
 #define COMPONENTS_EXO_TEST_SURFACE_TREE_HOST_TEST_UTIL_H_
 
+#include "base/test/bind.h"
 #include "components/exo/surface_tree_host.h"
 
 namespace exo::test {
@@ -17,6 +18,17 @@
 // presented.
 void WaitForLastFramePresentation(SurfaceTreeHost* surface_tree_host);
 
+template <class LayerTreeFrameSinkHolderType>
+void SetLayerTreeFrameSinkHolderFactory(SurfaceTreeHost* surface_tree_host) {
+  surface_tree_host->SetLayerTreeFrameSinkHolderFactoryForTesting(
+      base::BindLambdaForTesting(
+          [surface_tree_host]() -> std::unique_ptr<LayerTreeFrameSinkHolder> {
+            return std::make_unique<LayerTreeFrameSinkHolderType>(
+                surface_tree_host,
+                surface_tree_host->host_window()->CreateLayerTreeFrameSink());
+          }));
+}
+
 }  // namespace exo::test
 
 #endif  // COMPONENTS_EXO_TEST_SURFACE_TREE_HOST_TEST_UTIL_H_
diff --git a/components/optimization_guide/core/model_util.cc b/components/optimization_guide/core/model_util.cc
index a5489bc..f805170 100644
--- a/components/optimization_guide/core/model_util.cc
+++ b/components/optimization_guide/core/model_util.cc
@@ -96,6 +96,8 @@
       return "SegmentationAdaptiveToolbar";
     case proto::OPTIMIZATION_TARGET_SEGMENTATION_TABLET_PRODUCTIVITY_USER:
       return "SegmentationTabletProductivityUser";
+    case proto::OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING_IMAGE_EMBEDDER:
+      return "ClientSidePhishingImageEmbedder";
     case proto::
         OPTIMIZATION_TARGET_NEW_TAB_PAGE_HISTORY_CLUSTERS_MODULE_RANKING:
       return "NewTabPageHistoryClustersModuleRanking";
diff --git a/components/optimization_guide/core/prediction_manager.cc b/components/optimization_guide/core/prediction_manager.cc
index daeabf6..38dfdad 100644
--- a/components/optimization_guide/core/prediction_manager.cc
+++ b/components/optimization_guide/core/prediction_manager.cc
@@ -171,7 +171,11 @@
          model_metadata.type_url() ==
              "type.googleapis.com/"
              "google.privacy.webpermissionpredictions.v1."
-             "WebPermissionPredictionsModelMetadata";
+             "WebPermissionPredictionsModelMetadata" ||
+         model_metadata.type_url() ==
+             "type.googleapis.com/"
+             "google.internal.chrome.optimizationguide.v1."
+             "ClientSidePhishingModelMetadata";
 }
 
 void RecordModelAvailableAtRegistration(
diff --git a/components/optimization_guide/proto/BUILD.gn b/components/optimization_guide/proto/BUILD.gn
index 2eb2d11..a77dbeb 100644
--- a/components/optimization_guide/proto/BUILD.gn
+++ b/components/optimization_guide/proto/BUILD.gn
@@ -12,6 +12,7 @@
   proto_in_dir = "//"
   sources = [
     "autocomplete_scoring_model_metadata.proto",
+    "client_side_phishing_model_metadata.proto",
     "common_types.proto",
     "hint_cache.proto",
     "hints.proto",
diff --git a/components/optimization_guide/proto/client_side_phishing_model_metadata.proto b/components/optimization_guide/proto/client_side_phishing_model_metadata.proto
new file mode 100644
index 0000000..c20a5bf
--- /dev/null
+++ b/components/optimization_guide/proto/client_side_phishing_model_metadata.proto
@@ -0,0 +1,17 @@
+// 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.
+
+syntax = "proto3";
+
+option optimize_for = LITE_RUNTIME;
+option java_package = "org.chromium.components.optimization_guide.proto";
+option java_outer_classname = "ClientSidePhishingModelMetadataProto";
+
+package optimization_guide.proto;
+
+// The message contains an integer to pair with a client side phishing image
+// embedding model which will also have an integer to match.
+message ClientSidePhishingModelMetadata {
+  optional int32 image_embedding_model_version = 1;
+}
diff --git a/components/optimization_guide/proto/models.proto b/components/optimization_guide/proto/models.proto
index 0036d8e2..7b469171 100644
--- a/components/optimization_guide/proto/models.proto
+++ b/components/optimization_guide/proto/models.proto
@@ -122,7 +122,7 @@
 
 // The scenarios for which the optimization guide has models for.
 enum OptimizationTarget {
-  reserved 14, 30;
+  reserved 14;
 
   OPTIMIZATION_TARGET_UNKNOWN = 0;
   // Should only be applied when the page load is predicted to be painful.
@@ -183,6 +183,8 @@
   OPTIMIZATION_TARGET_SEGMENTATION_ADAPTIVE_TOOLBAR = 28;
   // Target for segmentation: Determine users who are tabletproductivity users.
   OPTIMIZATION_TARGET_SEGMENTATION_TABLET_PRODUCTIVITY_USER = 29;
+  // Target for client side phishing image embedding model.
+  OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING_IMAGE_EMBEDDER = 30;
   // Target for ranking clusters that have passed minimal filtering for the New
   // Tab Page History Clusters module.
   OPTIMIZATION_TARGET_NEW_TAB_PAGE_HISTORY_CLUSTERS_MODULE_RANKING = 31;
diff --git a/components/safe_browsing/content/browser/client_side_detection_service.cc b/components/safe_browsing/content/browser/client_side_detection_service.cc
index 89abbe4..5b276342 100644
--- a/components/safe_browsing/content/browser/client_side_detection_service.cc
+++ b/components/safe_browsing/content/browser/client_side_detection_service.cc
@@ -158,6 +158,16 @@
                 base::BindRepeating(
                     &ClientSideDetectionService::SendModelToRenderers,
                     weak_factory_.GetWeakPtr()));
+        if (base::FeatureList::IsEnabled(
+                kClientSideDetectionModelImageEmbedder)) {
+          if (IsEnhancedProtectionEnabled(*delegate_->GetPrefs())) {
+            client_side_phishing_model_optimization_guide_
+                ->SubscribeToImageEmbedderOptimizationGuide();
+            send_image_embedding_model_to_renderer_ = true;
+          } else {
+            send_image_embedding_model_to_renderer_ = false;
+          }
+        }
       }
     }
   } else {
@@ -509,6 +519,24 @@
   return ClientSidePhishingModel::GetInstance()->GetVisualTfLiteModel();
 }
 
+const base::File& ClientSideDetectionService::GetImageEmbeddingModel() {
+  // At launch, we will only deploy the Image Embedding Model through
+  // OptimizationGuide
+  return client_side_phishing_model_optimization_guide_
+      ->GetImageEmbeddingModel();
+}
+
+bool ClientSideDetectionService::HasImageEmbeddingModel() {
+  return client_side_phishing_model_optimization_guide_
+      ->HasImageEmbeddingModel();
+}
+
+bool ClientSideDetectionService::
+    IsModelMetadataImageEmbeddingVersionMatching() {
+  return client_side_phishing_model_optimization_guide_
+      ->IsModelMetadataImageEmbeddingVersionMatching();
+}
+
 void ClientSideDetectionService::SetURLLoaderFactoryForTesting(
     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
   url_loader_factory_ = url_loader_factory;
@@ -533,8 +561,28 @@
                                      GetVisualTfLiteModel().Duplicate());
       return;
     case CSDModelType::kFlatbuffer:
-      model_setter->SetPhishingFlatBufferModel(
-          GetModelSharedMemoryRegion(), GetVisualTfLiteModel().Duplicate());
+      if (delegate_ && delegate_->GetPrefs() &&
+          IsEnhancedProtectionEnabled(*delegate_->GetPrefs()) &&
+          base::FeatureList::IsEnabled(
+              kClientSideDetectionModelImageEmbedder) &&
+          ShouldSendImageEmbeddingModelToRenderer() &&
+          HasImageEmbeddingModel()) {
+        if (IsModelMetadataImageEmbeddingVersionMatching()) {
+          base::UmaHistogramBoolean(
+              "SBClientPhishing.ImageEmbeddingModelVersionMatch", true);
+          model_setter->SetImageEmbeddingAndPhishingFlatBufferModel(
+              GetModelSharedMemoryRegion(), GetVisualTfLiteModel().Duplicate(),
+              GetImageEmbeddingModel().Duplicate());
+        } else {
+          base::UmaHistogramBoolean(
+              "SBClientPhishing.ImageEmbeddingModelVersionMatch", false);
+          model_setter->SetPhishingFlatBufferModel(
+              GetModelSharedMemoryRegion(), GetVisualTfLiteModel().Duplicate());
+        }
+      } else {
+        model_setter->SetPhishingFlatBufferModel(
+            GetModelSharedMemoryRegion(), GetVisualTfLiteModel().Duplicate());
+      }
       return;
   }
 }
@@ -629,6 +677,21 @@
   }
 }
 
+bool ClientSideDetectionService::IsSubscribedToImageEmbeddingModelUpdates() {
+  if (base::FeatureList::IsEnabled(
+          kClientSideDetectionModelOptimizationGuide) &&
+      base::FeatureList::IsEnabled(kClientSideDetectionModelImageEmbedder)) {
+    return client_side_phishing_model_optimization_guide_ &&
+           client_side_phishing_model_optimization_guide_
+               ->IsSubscribedToImageEmbeddingModelUpdates();
+  }
+  return false;
+}
+
+bool ClientSideDetectionService::ShouldSendImageEmbeddingModelToRenderer() {
+  return send_image_embedding_model_to_renderer_;
+}
+
 // IN-TEST
 void ClientSideDetectionService::SetModelAndVisualTfLiteForTesting(
     const base::FilePath& model,
diff --git a/components/safe_browsing/content/browser/client_side_detection_service.h b/components/safe_browsing/content/browser/client_side_detection_service.h
index 8afba77..1be0276 100644
--- a/components/safe_browsing/content/browser/client_side_detection_service.h
+++ b/components/safe_browsing/content/browser/client_side_detection_service.h
@@ -163,6 +163,14 @@
   // override it.
   virtual const base::File& GetVisualTfLiteModel();
 
+  // Returns the Image Embedding model file. Virtual so that mock implementation
+  // can override it.
+  virtual const base::File& GetImageEmbeddingModel();
+
+  virtual bool HasImageEmbeddingModel();
+
+  virtual bool IsModelMetadataImageEmbeddingVersionMatching();
+
   // Returns the visual TFLite model thresholds from the model class
   virtual const base::flat_map<std::string, TfLiteModelMetadata::Threshold>&
   GetVisualTfLiteModelThresholds();
@@ -182,10 +190,13 @@
 
   bool IsModelAvailable();
 
-  // For testing the model in browser test
+  // For testing the model in browser test.
   void SetModelAndVisualTfLiteForTesting(const base::FilePath& model,
                                          const base::FilePath& visual_tf_lite);
 
+  bool IsSubscribedToImageEmbeddingModelUpdates();
+  bool ShouldSendImageEmbeddingModelToRenderer();
+
  private:
   friend class ClientSideDetectionServiceTest;
   FRIEND_TEST_ALL_PREFIXES(ClientSideDetectionServiceTest,
@@ -298,6 +309,16 @@
 
   SEQUENCE_CHECKER(sequence_checker_);
 
+  // Used to note whether the model update should follow with sending the image
+  // embedding model to renderer, because removing the observer from
+  // OptimizationGuide service does not remove the observer instantaneously,
+  // making a user quick resubscription scenario fail a DCHECK in their service.
+  // We will always update the model on the disc if the user has subscribed to
+  // image embedding model once in their current session, but the state of this
+  // boolean value indicate whether the model updates will be sent to the
+  // renderer or not.
+  bool send_image_embedding_model_to_renderer_ = false;
+
   // Used to asynchronously call the callbacks for
   // SendClientReportPhishingRequest.
   base::WeakPtrFactory<ClientSideDetectionService> weak_factory_{this};
diff --git a/components/safe_browsing/content/browser/client_side_phishing_model_optimization_guide.cc b/components/safe_browsing/content/browser/client_side_phishing_model_optimization_guide.cc
index 5e513958..97d6ca0 100644
--- a/components/safe_browsing/content/browser/client_side_phishing_model_optimization_guide.cc
+++ b/components/safe_browsing/content/browser/client_side_phishing_model_optimization_guide.cc
@@ -21,6 +21,8 @@
 #include "base/task/thread_pool.h"
 #include "build/build_config.h"
 #include "components/optimization_guide/core/optimization_guide_model_provider.h"
+#include "components/optimization_guide/core/optimization_guide_util.h"
+#include "components/optimization_guide/proto/client_side_phishing_model_metadata.pb.h"
 #include "components/optimization_guide/proto/models.pb.h"
 #include "components/safe_browsing/core/common/fbs/client_model_generated.h"
 #include "components/safe_browsing/core/common/features.h"
@@ -28,6 +30,7 @@
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
 #include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace safe_browsing {
 
@@ -70,6 +73,32 @@
                      std::make_pair(contents, std::move(tflite_model))));
 }
 
+base::File LoadImageEmbeddingModelFile(const base::FilePath& model_file_path) {
+  if (!base::PathExists(model_file_path)) {
+    VLOG(0)
+        << "Model path does not exist. Returning empty file. Given path is: "
+        << model_file_path;
+    return base::File();
+  }
+
+  base::File image_embedding_model_file(
+      model_file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
+
+  bool image_embedding_model_valid = image_embedding_model_file.IsValid();
+
+  base::UmaHistogramBoolean(
+      "SBClientPhishing.ModelDynamicUpdateSuccess.ImageEmbedding",
+      image_embedding_model_valid);
+
+  if (!image_embedding_model_valid) {
+    VLOG(2)
+        << "Failed to receive image embedding model file. File is not valid";
+    return base::File();
+  }
+
+  return image_embedding_model_file;
+}
+
 // Load the model file at the provided file path.
 std::pair<std::string, base::File> LoadModelAndVisualTfLiteFile(
     const base::FilePath& model_file_path,
@@ -144,20 +173,56 @@
     const optimization_guide::ModelInfo& model_info) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (optimization_target !=
-      optimization_guide::proto::OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING) {
+          optimization_guide::proto::OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING &&
+      optimization_target !=
+          optimization_guide::proto::
+              OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING_IMAGE_EMBEDDER) {
     return;
   }
-  background_task_runner_->PostTaskAndReplyWithResult(
-      FROM_HERE,
-      base::BindOnce(&LoadModelAndVisualTfLiteFile,
-                     model_info.GetModelFilePath(),
-                     model_info.GetAdditionalFiles()),
-      base::BindOnce(&ClientSidePhishingModelOptimizationGuide::
-                         OnModelAndVisualTfLiteFileLoaded,
-                     weak_ptr_factory_.GetWeakPtr()));
+
+  if (optimization_target ==
+      optimization_guide::proto::OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING) {
+    background_task_runner_->PostTaskAndReplyWithResult(
+        FROM_HERE,
+        base::BindOnce(&LoadModelAndVisualTfLiteFile,
+                       model_info.GetModelFilePath(),
+                       model_info.GetAdditionalFiles()),
+        base::BindOnce(&ClientSidePhishingModelOptimizationGuide::
+                           OnModelAndVisualTfLiteFileLoaded,
+                       weak_ptr_factory_.GetWeakPtr(),
+                       model_info.GetModelMetadata()));
+  } else if (optimization_target ==
+             optimization_guide::proto::
+                 OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING_IMAGE_EMBEDDER) {
+    background_task_runner_->PostTaskAndReplyWithResult(
+        FROM_HERE,
+        base::BindOnce(&LoadImageEmbeddingModelFile,
+                       model_info.GetModelFilePath()),
+        base::BindOnce(&ClientSidePhishingModelOptimizationGuide::
+                           OnImageEmbeddingModelLoaded,
+                       weak_ptr_factory_.GetWeakPtr(),
+                       model_info.GetModelMetadata()));
+  }
+}
+
+void ClientSidePhishingModelOptimizationGuide::
+    SubscribeToImageEmbedderOptimizationGuide() {
+  if (!subscribed_to_image_embedder_ && opt_guide_) {
+    subscribed_to_image_embedder_ = true;
+    opt_guide_->AddObserverForOptimizationTargetModel(
+        optimization_guide::proto::
+            OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING_IMAGE_EMBEDDER,
+        /*model_metadata=*/absl::nullopt, this);
+  }
+}
+
+bool ClientSidePhishingModelOptimizationGuide::
+    IsSubscribedToImageEmbeddingModelUpdates() {
+  return subscribed_to_image_embedder_;
 }
 
 void ClientSidePhishingModelOptimizationGuide::OnModelAndVisualTfLiteFileLoaded(
+    absl::optional<optimization_guide::proto::Any> model_metadata,
     std::pair<std::string, base::File> model_and_tflite) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
@@ -243,6 +308,25 @@
     base::UmaHistogramMediumTimes(
         "SBClientPhishing.OptimizationGuide.ModelFetchTime",
         base::TimeTicks::Now() - beginning_time_);
+
+    absl::optional<optimization_guide::proto::ClientSidePhishingModelMetadata>
+        client_side_phishing_model_metadata = absl::nullopt;
+
+    if (model_metadata.has_value()) {
+      client_side_phishing_model_metadata =
+          optimization_guide::ParsedAnyMetadata<
+              optimization_guide::proto::ClientSidePhishingModelMetadata>(
+              model_metadata.value());
+    }
+
+    if (client_side_phishing_model_metadata.has_value()) {
+      trigger_model_version_ =
+          client_side_phishing_model_metadata->image_embedding_model_version();
+    } else {
+      VLOG(1) << "Client side phishing model metadata is missing an image "
+                 "embedding model version value";
+    }
+
     content::GetUIThreadTaskRunner({})->PostTask(
         FROM_HERE,
         base::BindOnce(
@@ -251,6 +335,47 @@
   }
 }
 
+void ClientSidePhishingModelOptimizationGuide::OnImageEmbeddingModelLoaded(
+    absl::optional<optimization_guide::proto::Any> model_metadata,
+    base::File image_embedding_model) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  image_embedding_model_ = std::move(image_embedding_model);
+
+  absl::optional<optimization_guide::proto::ClientSidePhishingModelMetadata>
+      image_embedding_model_metadata = absl::nullopt;
+
+  if (model_metadata.has_value()) {
+    image_embedding_model_metadata = optimization_guide::ParsedAnyMetadata<
+        optimization_guide::proto::ClientSidePhishingModelMetadata>(
+        model_metadata.value());
+  }
+
+  if (image_embedding_model_metadata.has_value()) {
+    embedding_model_version_ =
+        image_embedding_model_metadata->image_embedding_model_version();
+  } else {
+    VLOG(1) << "Image embedding model metadata is missing a version value";
+  }
+
+  // There is no use of the image embedding model if the visual trigger model is
+  // not present, so we will only send to the renderer when that is the case.
+  if (visual_tflite_model_ && image_embedding_model_) {
+    content::GetUIThreadTaskRunner({})->PostTask(
+        FROM_HERE,
+        base::BindOnce(
+            &ClientSidePhishingModelOptimizationGuide::NotifyCallbacksOnUI,
+            weak_ptr_factory_.GetWeakPtr()));
+  }
+}
+
+bool ClientSidePhishingModelOptimizationGuide::
+    IsModelMetadataImageEmbeddingVersionMatching() {
+  return trigger_model_version_.has_value() &&
+         embedding_model_version_.has_value() &&
+         trigger_model_version_.value() == embedding_model_version_.value();
+}
+
 ClientSidePhishingModelOptimizationGuide::
     ~ClientSidePhishingModelOptimizationGuide() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@@ -258,13 +383,27 @@
   opt_guide_->RemoveObserverForOptimizationTargetModel(
       optimization_guide::proto::OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING,
       this);
-  opt_guide_ = nullptr;
+
+  if (subscribed_to_image_embedder_) {
+    opt_guide_->RemoveObserverForOptimizationTargetModel(
+        optimization_guide::proto::
+            OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING_IMAGE_EMBEDDER,
+        this);
+  }
 
   if (visual_tflite_model_) {
     background_task_runner_->PostTask(
         FROM_HERE,
         base::BindOnce(&CloseModelFile, std::move(*visual_tflite_model_)));
   }
+
+  if (image_embedding_model_) {
+    background_task_runner_->PostTask(
+        FROM_HERE,
+        base::BindOnce(&CloseModelFile, std::move(*image_embedding_model_)));
+  }
+
+  opt_guide_ = nullptr;
 }
 
 base::CallbackListSubscription
@@ -363,6 +502,18 @@
   return *visual_tflite_model_;
 }
 
+const base::File&
+ClientSidePhishingModelOptimizationGuide::GetImageEmbeddingModel() const {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  DCHECK(image_embedding_model_ && image_embedding_model_->IsValid());
+  return *image_embedding_model_;
+}
+
+bool ClientSidePhishingModelOptimizationGuide::HasImageEmbeddingModel() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  return !!image_embedding_model_;
+}
+
 CSDModelTypeOptimizationGuide
 ClientSidePhishingModelOptimizationGuide::GetModelType() const {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@@ -590,7 +741,7 @@
                      additional_files),
       base::BindOnce(&ClientSidePhishingModelOptimizationGuide::
                          OnModelAndVisualTfLiteFileLoaded,
-                     weak_ptr_factory_.GetWeakPtr()));
+                     weak_ptr_factory_.GetWeakPtr(), absl::nullopt));
 }
 
 }  // namespace safe_browsing
diff --git a/components/safe_browsing/content/browser/client_side_phishing_model_optimization_guide.h b/components/safe_browsing/content/browser/client_side_phishing_model_optimization_guide.h
index f2abfa4..055008d5 100644
--- a/components/safe_browsing/content/browser/client_side_phishing_model_optimization_guide.h
+++ b/components/safe_browsing/content/browser/client_side_phishing_model_optimization_guide.h
@@ -59,6 +59,12 @@
       optimization_guide::proto::OptimizationTarget optimization_target,
       const optimization_guide::ModelInfo& model_info) override;
 
+  // Enhanced Safe Browsing users receive an additional image embedding model to
+  // be attached to CSD-Phishing ping to better train the models.
+  void SubscribeToImageEmbedderOptimizationGuide();
+
+  void UnsubscribeToImageEmbedderOptimizationGuide();
+
   // Register a callback to be notified whenever the model changes. All
   // notifications will occur on the UI thread.
   base::CallbackListSubscription RegisterCallback(
@@ -81,6 +87,12 @@
 
   const base::File& GetVisualTfLiteModel() const;
 
+  const base::File& GetImageEmbeddingModel() const;
+
+  bool HasImageEmbeddingModel();
+
+  bool IsModelMetadataImageEmbeddingVersionMatching();
+
   // Overrides the model string for use in tests.
   void SetModelStrForTesting(const std::string& model_str);
   void SetVisualTfLiteModelForTesting(base::File file);
@@ -101,8 +113,13 @@
   void MaybeOverrideModel();
 
   void OnModelAndVisualTfLiteFileLoaded(
+      absl::optional<optimization_guide::proto::Any> model_metadata,
       std::pair<std::string, base::File> model_and_tflite);
 
+  void OnImageEmbeddingModelLoaded(
+      absl::optional<optimization_guide::proto::Any> model_metadata,
+      base::File image_embedding_model_data);
+
   void SetModelAndVisualTfLiteForTesting(
       const base::FilePath& model_file_path,
       const base::FilePath& visual_tf_lite_model_path);
@@ -112,6 +129,8 @@
   void SetModelStringForTesting(const std::string& model_str,
                                 base::File visual_tflite_model);
 
+  bool IsSubscribedToImageEmbeddingModelUpdates();
+
  private:
   static const int kInitialClientModelFetchDelayMs;
 
@@ -135,6 +154,10 @@
   absl::optional<base::File> visual_tflite_model_
       GUARDED_BY_CONTEXT(sequence_checker_);
 
+  // Image Embedding TfLite model file. Guarded by sequence_checker_.
+  absl::optional<base::File> image_embedding_model_
+      GUARDED_BY_CONTEXT(sequence_checker_);
+
   // Thresholds in visual TFLite model file to be used for comparison after
   // visual classification
   base::flat_map<std::string, TfLiteModelMetadata::Threshold> thresholds_;
@@ -156,8 +179,24 @@
   // BrowserContextKeyedServiceFactory and should not be used after Shutdown
   raw_ptr<optimization_guide::OptimizationGuideModelProvider> opt_guide_;
 
+  // These two integer values will be set from reading the metadata specified
+  // under each optimization target. These two are used to match the model
+  // pairings properly. If the two values match, then the image embedding model
+  // will be sent to the renderer process along with the trigger models.
+  absl::optional<int> trigger_model_version_;
+  absl::optional<int> embedding_model_version_;
+
   scoped_refptr<base::SequencedTaskRunner> background_task_runner_;
 
+  // If the users subscribe to ESB, the code will add an observer to the
+  // OptimizationGuide service for the image embedder model. We can choose to
+  // remove the observer, but it will be on the list to be removed, and not
+  // removed instantly. Therefore, if the user subscribes, unsubscribes, and
+  // re-subscribes again in very quick succession, the code will crash because
+  // the DCHECK fails, indicating that the observer is added already. Therefore,
+  // this will be a one time use flag.
+  bool subscribed_to_image_embedder_ = false;
+
   SEQUENCE_CHECKER(sequence_checker_);
 
   base::TimeTicks beginning_time_;
diff --git a/components/safe_browsing/content/browser/client_side_phishing_model_unittest.cc b/components/safe_browsing/content/browser/client_side_phishing_model_unittest.cc
index fc873f83..ec7901a 100644
--- a/components/safe_browsing/content/browser/client_side_phishing_model_unittest.cc
+++ b/components/safe_browsing/content/browser/client_side_phishing_model_unittest.cc
@@ -75,6 +75,13 @@
                                 .SetAdditionalFiles(additional_files_path)
                                 .Build();
       model_observer_->OnModelUpdated(optimization_target, *model_metadata);
+    } else if (optimization_target ==
+               optimization_guide::proto::
+                   OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING_IMAGE_EMBEDDER) {
+      auto model_metadata = optimization_guide::TestModelInfoBuilder()
+                                .SetModelFilePath(model_file_path)
+                                .Build();
+      model_observer_->OnModelUpdated(optimization_target, *model_metadata);
     }
   }
 
@@ -84,14 +91,21 @@
   raw_ptr<optimization_guide::OptimizationTargetModelObserver> model_observer_;
 };
 
-class ClientSidePhishingModelTest : public content::RenderViewHostTestHarness,
-                                    public testing::WithParamInterface<bool> {
+class ClientSidePhishingModelTest
+    : public content::RenderViewHostTestHarness,
+      public testing::WithParamInterface<std::tuple<bool, bool>> {
  public:
   ClientSidePhishingModelTest() {
+    std::vector<base::test::FeatureRef> enabled_features = {};
     if (ShouldEnableCacao()) {
-      feature_list_.InitAndEnableFeature(
-          kClientSideDetectionModelOptimizationGuide);
+      enabled_features.push_back(kClientSideDetectionModelOptimizationGuide);
     }
+
+    if (ShouldEnableImageEmbedder()) {
+      enabled_features.push_back(kClientSideDetectionModelImageEmbedder);
+    }
+
+    feature_list_.InitWithFeatures(enabled_features, {});
   }
 
   void SetUp() override {
@@ -125,13 +139,24 @@
     task_environment()->RunUntilIdle();
   }
 
+  void ValidateImageEmbeddingModel(
+      const base::FilePath& image_embedding_model_file_path) {
+    model_observer_tracker_->NotifyModelFileUpdate(
+        optimization_guide::proto::
+            OPTIMIZATION_TARGET_CLIENT_SIDE_PHISHING_IMAGE_EMBEDDER,
+        image_embedding_model_file_path, {});
+    task_environment()->RunUntilIdle();
+  }
+
   ClientSidePhishingModelOptimizationGuide* service() {
     return client_side_phishing_model_.get();
   }
 
   base::HistogramTester& histogram_tester() { return histogram_tester_; }
 
-  bool ShouldEnableCacao() { return GetParam(); }
+  bool ShouldEnableCacao() { return get<0>(GetParam()); }
+
+  bool ShouldEnableImageEmbedder() { return get<1>(GetParam()); }
 
  protected:
   base::test::ScopedFeatureList feature_list_;
@@ -168,7 +193,9 @@
 
 }  // namespace
 
-INSTANTIATE_TEST_SUITE_P(All, ClientSidePhishingModelTest, testing::Bool());
+INSTANTIATE_TEST_SUITE_P(All,
+                         ClientSidePhishingModelTest,
+                         testing::Combine(testing::Bool(), testing::Bool()));
 
 TEST_P(ClientSidePhishingModelTest, ValidModel) {
   if (!base::FeatureList::IsEnabled(
@@ -203,6 +230,20 @@
   histogram_tester().ExpectUniqueSample(
       "SBClientPhishing.ModelDynamicUpdateSuccess", true, 1);
   EXPECT_TRUE(service()->IsEnabled());
+  if (base::FeatureList::IsEnabled(kClientSideDetectionModelImageEmbedder)) {
+    base::FilePath image_embedding_model_file_path;
+    base::PathService::Get(base::DIR_SOURCE_ROOT,
+                           &image_embedding_model_file_path);
+    image_embedding_model_file_path =
+        image_embedding_model_file_path.AppendASCII("components")
+            .AppendASCII("test")
+            .AppendASCII("data")
+            .AppendASCII("safe_browsing")
+            .AppendASCII("image_embedding.tflite");
+    ValidateImageEmbeddingModel(image_embedding_model_file_path);
+    histogram_tester().ExpectUniqueSample(
+        "SBClientPhishing.ModelDynamicUpdateSuccess.ImageEmbedding", true, 1);
+  }
 }
 
 TEST_P(ClientSidePhishingModelTest, InvalidModelDueToInvalidPath) {
diff --git a/components/safe_browsing/content/browser/web_ui/safe_browsing_ui.cc b/components/safe_browsing/content/browser/web_ui/safe_browsing_ui.cc
index 585ea4e..a8b105d4 100644
--- a/components/safe_browsing/content/browser/web_ui/safe_browsing_ui.cc
+++ b/components/safe_browsing/content/browser/web_ui/safe_browsing_ui.cc
@@ -642,6 +642,19 @@
   return "UNKNOWN_ENUM_SPECIFIED";
 }
 
+base::Value::Dict SerializeImageFeatureEmbedding(
+    ImageFeatureEmbedding image_feature_embedding) {
+  base::Value::Dict dict;
+  base::Value::List embedding_values;
+  for (const auto& value : image_feature_embedding.embedding_value()) {
+    embedding_values.Append(value);
+  }
+  dict.Set("embedding_model_version",
+           image_feature_embedding.embedding_model_version());
+  dict.Set("embedding_value", std::move(embedding_values));
+  return dict;
+}
+
 base::Value::Dict SerializeChromeUserPopulation(
     const ChromeUserPopulation& population) {
   base::Value::Dict population_dict;
@@ -1082,6 +1095,11 @@
         SerializeClientSideDetectionType(cpr.client_side_detection_type()));
   }
 
+  if (cpr.has_image_feature_embedding()) {
+    dict.Set("image_feature_embedding",
+             SerializeImageFeatureEmbedding(cpr.image_feature_embedding()));
+  }
+
   base::Value::List features;
   for (const auto& feature : cpr.feature_map()) {
     base::Value::Dict dict_features;
diff --git a/components/safe_browsing/content/common/safe_browsing.mojom b/components/safe_browsing/content/common/safe_browsing.mojom
index 5a783c7..94871774 100644
--- a/components/safe_browsing/content/common/safe_browsing.mojom
+++ b/components/safe_browsing/content/common/safe_browsing.mojom
@@ -142,6 +142,20 @@
 // Interface for setting a phishing model. This is scoped to an entire
 // RenderProcess.
 interface PhishingModelSetter {
+  // A classification model for client-side phishing detection in addition to
+  // the image embedding model. This call sends the model and the image
+  // embedding model from browser process to the renderer process. The image
+  // embedding model is converted from a TfLite file to a string in the browser
+  // process to be used to create an ImageEmbedder in the renderer process. This
+  // function call should only be called for Enhanced Safe Browsing enabled
+  // users who will allow the ImageEmbedding output as part of the CSD-Phishing
+  // ping to be sent to CSPP for better model training. All else is the same as
+  // the SetPhishingFlatBufferModel function.
+  SetImageEmbeddingAndPhishingFlatBufferModel(
+                              mojo_base.mojom.ReadOnlySharedMemoryRegion region,
+                              mojo_base.mojom.ReadOnlyFile? tflite_model,
+                              mojo_base.mojom.ReadOnlyFile?
+                              image_embedding_model_);
   // A classification model for client-side phishing detection.
   // The string is an encoded safe_browsing::ClientSideModel protocol buffer, or
   // empty to disable client-side phishing detection for this renderer. The
diff --git a/components/safe_browsing/content/renderer/phishing_classifier/flatbuffer_scorer.cc b/components/safe_browsing/content/renderer/phishing_classifier/flatbuffer_scorer.cc
index fa62e46..06961435 100644
--- a/components/safe_browsing/content/renderer/phishing_classifier/flatbuffer_scorer.cc
+++ b/components/safe_browsing/content/renderer/phishing_classifier/flatbuffer_scorer.cc
@@ -93,6 +93,26 @@
   return scorer;
 }
 
+std::unique_ptr<FlatBufferModelScorer>
+FlatBufferModelScorer::CreateFlatBufferModelWithImageEmbeddingScorer(
+    base::ReadOnlySharedMemoryRegion region,
+    base::File visual_tflite_model,
+    base::File image_embedding_model) {
+  std::unique_ptr<FlatBufferModelScorer> scorer =
+      Create(std::move(region), std::move(visual_tflite_model));
+
+  if (image_embedding_model.IsValid()) {
+    if (scorer && !scorer->image_embedding_model_.Initialize(
+                      std::move(image_embedding_model))) {
+      RecordScorerCreationStatus(
+          SCORER_FAIL_FLATBUFFER_INVALID_IMAGE_EMBEDDING_TFLITE_MODEL);
+      return nullptr;
+    }
+  }
+
+  return scorer;
+}
+
 double FlatBufferModelScorer::ComputeRuleScore(
     const flat::ClientSideModel_::Rule* rule,
     const FeatureMap& features) const {
diff --git a/components/safe_browsing/content/renderer/phishing_classifier/flatbuffer_scorer.h b/components/safe_browsing/content/renderer/phishing_classifier/flatbuffer_scorer.h
index 5a76abba..1f88fde 100644
--- a/components/safe_browsing/content/renderer/phishing_classifier/flatbuffer_scorer.h
+++ b/components/safe_browsing/content/renderer/phishing_classifier/flatbuffer_scorer.h
@@ -47,6 +47,12 @@
       base::ReadOnlySharedMemoryRegion region,
       base::File visual_tflite_model);
 
+  static std::unique_ptr<FlatBufferModelScorer>
+  CreateFlatBufferModelWithImageEmbeddingScorer(
+      base::ReadOnlySharedMemoryRegion region,
+      base::File visual_tflite_model,
+      base::File image_embedding_model);
+
   double ComputeScore(const FeatureMap& features) const override;
 
 #if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
@@ -87,6 +93,7 @@
   base::ReadOnlySharedMemoryMapping flatbuffer_mapping_;
   google::protobuf::RepeatedPtrField<TfLiteModelMetadata::Threshold>
       thresholds_;
+  base::MemoryMappedFile image_embedding_model_;
 };
 
 }  // namespace safe_browsing
diff --git a/components/safe_browsing/content/renderer/phishing_classifier/phishing_model_setter_impl.cc b/components/safe_browsing/content/renderer/phishing_classifier/phishing_model_setter_impl.cc
index 2da039412..146517af 100644
--- a/components/safe_browsing/content/renderer/phishing_classifier/phishing_model_setter_impl.cc
+++ b/components/safe_browsing/content/renderer/phishing_classifier/phishing_model_setter_impl.cc
@@ -11,6 +11,34 @@
 
 namespace safe_browsing {
 
+std::unique_ptr<FlatBufferModelScorer> CreateFlatBufferModelScorer(
+    base::ReadOnlySharedMemoryRegion flatbuffer_region,
+    base::File tflite_visual_model) {
+  std::unique_ptr<FlatBufferModelScorer> scorer;
+  // An invalid region means we should disable client-side phishing detection.
+  if (flatbuffer_region.IsValid()) {
+    scorer = safe_browsing::FlatBufferModelScorer::Create(
+        std::move(flatbuffer_region), std::move(tflite_visual_model));
+  }
+  return scorer;
+}
+
+std::unique_ptr<FlatBufferModelScorer>
+CreateFlatBufferModelWithImageEmbeddingScorer(
+    base::ReadOnlySharedMemoryRegion flatbuffer_region,
+    base::File tflite_visual_model,
+    base::File image_embedding_model) {
+  std::unique_ptr<FlatBufferModelScorer> scorer;
+  // An invalid region means we should disable client-side phishing detection.
+  if (flatbuffer_region.IsValid()) {
+    scorer = safe_browsing::FlatBufferModelScorer::
+        CreateFlatBufferModelWithImageEmbeddingScorer(
+            std::move(flatbuffer_region), std::move(tflite_visual_model),
+            std::move(image_embedding_model));
+  }
+  return scorer;
+}
+
 PhishingModelSetterImpl::PhishingModelSetterImpl() = default;
 PhishingModelSetterImpl::~PhishingModelSetterImpl() = default;
 
@@ -26,6 +54,27 @@
   associated_interfaces->RemoveInterface(mojom::PhishingModelSetter::Name_);
 }
 
+void PhishingModelSetterImpl::SetImageEmbeddingAndPhishingFlatBufferModel(
+    base::ReadOnlySharedMemoryRegion flatbuffer_region,
+    base::File tflite_visual_model,
+    base::File image_embedding_model) {
+  std::unique_ptr<FlatBufferModelScorer> scorer =
+      CreateFlatBufferModelWithImageEmbeddingScorer(
+          std::move(flatbuffer_region), std::move(tflite_visual_model),
+          std::move(image_embedding_model));
+
+  if (!scorer) {
+    // Log here that the image embedder creation has failed.
+    return;
+  }
+
+  ScorerStorage::GetInstance()->SetScorer(std::move(scorer));
+
+  if (observer_for_testing_.is_bound()) {
+    observer_for_testing_->PhishingModelUpdated();
+  }
+}
+
 void PhishingModelSetterImpl::SetPhishingModel(const std::string& model,
                                                base::File tflite_visual_model) {
   std::unique_ptr<Scorer> scorer;
@@ -48,13 +97,10 @@
 void PhishingModelSetterImpl::SetPhishingFlatBufferModel(
     base::ReadOnlySharedMemoryRegion flatbuffer_region,
     base::File tflite_visual_model) {
-  std::unique_ptr<Scorer> scorer;
-  // An invalid region means we should disable client-side phishing detection.
-  if (flatbuffer_region.IsValid()) {
-    scorer = safe_browsing::FlatBufferModelScorer::Create(
-        std::move(flatbuffer_region), std::move(tflite_visual_model));
-    if (!scorer)
-      return;
+  std::unique_ptr<Scorer> scorer = CreateFlatBufferModelScorer(
+      std::move(flatbuffer_region), std::move(tflite_visual_model));
+  if (!scorer) {
+    return;
   }
   ScorerStorage::GetInstance()->SetScorer(std::move(scorer));
 
diff --git a/components/safe_browsing/content/renderer/phishing_classifier/phishing_model_setter_impl.h b/components/safe_browsing/content/renderer/phishing_classifier/phishing_model_setter_impl.h
index 77a93c8..32ace65 100644
--- a/components/safe_browsing/content/renderer/phishing_classifier/phishing_model_setter_impl.h
+++ b/components/safe_browsing/content/renderer/phishing_classifier/phishing_model_setter_impl.h
@@ -30,6 +30,10 @@
       blink::AssociatedInterfaceRegistry* associated_interfaces) override;
 
   // mojom::PhishingModelSetter overrides:
+  void SetImageEmbeddingAndPhishingFlatBufferModel(
+      base::ReadOnlySharedMemoryRegion flatbuffer_region,
+      base::File tflite_visual_model,
+      base::File image_embedding_model_data) override;
   void SetPhishingModel(const std::string& model,
                         base::File tflite_visual_model) override;
   void SetPhishingFlatBufferModel(
diff --git a/components/safe_browsing/content/renderer/phishing_classifier/scorer.h b/components/safe_browsing/content/renderer/phishing_classifier/scorer.h
index 2f950a1..5eeb34c 100644
--- a/components/safe_browsing/content/renderer/phishing_classifier/scorer.h
+++ b/components/safe_browsing/content/renderer/phishing_classifier/scorer.h
@@ -53,6 +53,7 @@
   SCORER_FAIL_FLATBUFFER_INVALID_MAPPING = 8,
   SCORER_FAIL_FLATBUFFER_FAILED_VERIFY = 9,
   SCORER_FAIL_FLATBUFFER_BAD_INDICES_OR_FIELDS = 10,  // Not used anymore
+  SCORER_FAIL_FLATBUFFER_INVALID_IMAGE_EMBEDDING_TFLITE_MODEL = 11,
   SCORER_STATUS_MAX  // Always add new values before this one.
 };
 
diff --git a/components/safe_browsing/core/common/fbs/client_model.fbs b/components/safe_browsing/core/common/fbs/client_model.fbs
index 813f132..0a2a675 100644
--- a/components/safe_browsing/core/common/fbs/client_model.fbs
+++ b/components/safe_browsing/core/common/fbs/client_model.fbs
@@ -39,6 +39,7 @@
   tflite_model_input_width: int (deprecated);
   tflite_model_input_height: int (deprecated);
   tflite_metadata:safe_browsing.flat.TfLiteModelMetadata;
+  img_embedding_metadata:safe_browsing.flat.TfLiteModelMetadata;
   dom_model_version:int;
 }
 
diff --git a/components/safe_browsing/core/common/features.cc b/components/safe_browsing/core/common/features.cc
index 558342a2..fb5ebed 100644
--- a/components/safe_browsing/core/common/features.cc
+++ b/components/safe_browsing/core/common/features.cc
@@ -299,6 +299,10 @@
              "ClientSideDetectionModelOptimizationGuide",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+BASE_FEATURE(kClientSideDetectionModelImageEmbedder,
+             "ClientSideDetectionModelImageEmbedder",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 BASE_FEATURE(kSafeBrowsingPhishingClassificationESBThreshold,
              "SafeBrowsingPhishingClassificationESBThreshold",
              base::FEATURE_ENABLED_BY_DEFAULT);
diff --git a/components/safe_browsing/core/common/features.h b/components/safe_browsing/core/common/features.h
index e5114ce..512a1c6c 100644
--- a/components/safe_browsing/core/common/features.h
+++ b/components/safe_browsing/core/common/features.h
@@ -325,5 +325,7 @@
 // Specifies the CSD-Phishing daily reports limit for ESB users
 extern const base::FeatureParam<int> kSafeBrowsingDailyPhishingReportsLimitESB;
 
+BASE_DECLARE_FEATURE(kClientSideDetectionModelImageEmbedder);
+
 }  // namespace safe_browsing
 #endif  // COMPONENTS_SAFE_BROWSING_CORE_COMMON_FEATURES_H_
diff --git a/components/safe_browsing/core/common/proto/client_model.proto b/components/safe_browsing/core/common/proto/client_model.proto
index 81e94e1c..9b9e58af 100644
--- a/components/safe_browsing/core/common/proto/client_model.proto
+++ b/components/safe_browsing/core/common/proto/client_model.proto
@@ -109,7 +109,9 @@
 
   optional TfLiteModelMetadata tflite_metadata = 17;
 
-  // next available tag number: 19
+  optional TfLiteModelMetadata img_embedding_metadata = 19;
+
+  // next available tag number: 20
 }
 
 message TfLiteModelMetadata {
diff --git a/components/safe_browsing/core/common/proto/csd.proto b/components/safe_browsing/core/common/proto/csd.proto
index a3bd0b2..1c98f0e 100644
--- a/components/safe_browsing/core/common/proto/csd.proto
+++ b/components/safe_browsing/core/common/proto/csd.proto
@@ -131,6 +131,14 @@
   optional bool is_signed_in = 18;
 }
 
+message ImageFeatureEmbedding {
+  // Tracking embedding-model's version from iteration to iteration
+  optional int32 embedding_model_version = 1;
+
+  // Raw tensor output of the embedding layer.
+  repeated float embedding_value = 2;
+}
+
 message ClientPhishingRequest {
   // URL that the client visited.  The CGI parameters are stripped by the
   // client.
@@ -242,7 +250,10 @@
   // where the scoring from protego requested this request.
   optional ClientSideDetectionType client_side_detection_type = 28;
 
-  // next available tag number: 29.
+  // Image embedding from model output tensor
+  optional ImageFeatureEmbedding image_feature_embedding = 29;
+
+  // next available tag number: 30.
 }
 
 message ClientPhishingResponse {
diff --git a/components/test/data/safe_browsing/image_embedding.tflite b/components/test/data/safe_browsing/image_embedding.tflite
new file mode 100644
index 0000000..fe0d15c5
--- /dev/null
+++ b/components/test/data/safe_browsing/image_embedding.tflite
Binary files differ
diff --git a/components/viz/service/display/renderer_pixeltest.cc b/components/viz/service/display/renderer_pixeltest.cc
index d70a812..ee17fa9 100644
--- a/components/viz/service/display/renderer_pixeltest.cc
+++ b/components/viz/service/display/renderer_pixeltest.cc
@@ -1749,14 +1749,12 @@
     constexpr int kMaxResourceSize = 10000;
 
     video_resource_updater_ = std::make_unique<media::VideoResourceUpdater>(
-        nullptr,
-        /*raster_context_provider=*/this->child_context_provider_.get(),
-        nullptr, this->child_resource_provider_.get(), kUseStreamVideoDrawQuad,
+        this->child_context_provider_.get(), nullptr,
+        this->child_resource_provider_.get(), kUseStreamVideoDrawQuad,
         kUseGpuMemoryBufferResources, kUseR16Texture, kMaxResourceSize);
     video_resource_updater2_ = std::make_unique<media::VideoResourceUpdater>(
-        nullptr,
-        /*raster_context_provider=*/this->child_context_provider_.get(),
-        nullptr, this->child_resource_provider_.get(), kUseStreamVideoDrawQuad,
+        this->child_context_provider_.get(), nullptr,
+        this->child_resource_provider_.get(), kUseStreamVideoDrawQuad,
         kUseGpuMemoryBufferResources, kUseR16Texture, kMaxResourceSize);
   }
 
@@ -2166,9 +2164,9 @@
     constexpr bool kUseR16Texture = false;
     constexpr int kMaxResourceSize = 10000;
     video_resource_updater_ = std::make_unique<media::VideoResourceUpdater>(
-        nullptr, child_context_provider_.get(), nullptr,
-        child_resource_provider_.get(), kUseStreamVideoDrawQuad,
-        kUseGpuMemoryBufferResources, kUseR16Texture, kMaxResourceSize);
+        child_context_provider_.get(), nullptr, child_resource_provider_.get(),
+        kUseStreamVideoDrawQuad, kUseGpuMemoryBufferResources, kUseR16Texture,
+        kMaxResourceSize);
   }
 
   void TearDown() override {
diff --git a/components/webapps/browser/uninstall_result_code.cc b/components/webapps/browser/uninstall_result_code.cc
index 19dcc30e..fa6f2c3 100644
--- a/components/webapps/browser/uninstall_result_code.cc
+++ b/components/webapps/browser/uninstall_result_code.cc
@@ -8,6 +8,18 @@
 
 namespace webapps {
 
+bool UninstallSucceeded(UninstallResultCode code) {
+  switch (code) {
+    case UninstallResultCode::kSuccess:
+    case UninstallResultCode::kNoAppToUninstall:
+      return true;
+    case UninstallResultCode::kCancelled:
+    case UninstallResultCode::kError:
+    case UninstallResultCode::kShutdown:
+      return false;
+  }
+}
+
 std::string ConvertUninstallResultCodeToString(UninstallResultCode code) {
   switch (code) {
     case UninstallResultCode::kSuccess:
@@ -18,6 +30,8 @@
       return "Uninstall cancelled";
     case UninstallResultCode::kError:
       return "Error";
+    case UninstallResultCode::kShutdown:
+      return "Shutdown";
   }
 }
 
diff --git a/components/webapps/browser/uninstall_result_code.h b/components/webapps/browser/uninstall_result_code.h
index 635a555..2a8b81d 100644
--- a/components/webapps/browser/uninstall_result_code.h
+++ b/components/webapps/browser/uninstall_result_code.h
@@ -14,8 +14,11 @@
   kNoAppToUninstall,
   kCancelled,
   kError,
+  kShutdown,
 };
 
+bool UninstallSucceeded(UninstallResultCode code);
+
 std::string ConvertUninstallResultCodeToString(UninstallResultCode code);
 
 }  // namespace webapps
diff --git a/content/browser/browser_interface_binders.cc b/content/browser/browser_interface_binders.cc
index f84c373..bab129df 100644
--- a/content/browser/browser_interface_binders.cc
+++ b/content/browser/browser_interface_binders.cc
@@ -90,7 +90,9 @@
 #include "media/mojo/mojom/media_player.mojom.h"
 #include "media/mojo/mojom/remoting.mojom.h"
 #include "media/mojo/mojom/video_decode_perf_history.mojom.h"
+#include "media/mojo/mojom/video_encoder_metrics_provider.mojom.h"
 #include "media/mojo/mojom/webrtc_video_perf.mojom.h"
+#include "media/mojo/services/video_encoder_metrics_provider.h"
 #include "media/mojo/services/webrtc_video_perf_recorder.h"
 #include "mojo/public/cpp/bindings/message.h"
 #include "services/device/public/mojom/battery_monitor.mojom.h"
@@ -988,6 +990,10 @@
       &RenderFrameHostImpl::BindMediaMetricsProviderReceiver,
       base::Unretained(host)));
 
+  map->Add<media::mojom::VideoEncoderMetricsProvider>(base::BindRepeating(
+      &RenderFrameHostImpl::BindVideoEncoderMetricsProviderReceiver,
+      base::Unretained(host)));
+
   map->Add<media::mojom::WebrtcVideoPerfRecorder>(base::BindRepeating(
       [](RenderFrameHostImpl* host,
          mojo::PendingReceiver<media::mojom::WebrtcVideoPerfRecorder>
diff --git a/content/browser/geolocation/geolocation_service_impl.cc b/content/browser/geolocation/geolocation_service_impl.cc
index e0fc994f..033ab0b 100644
--- a/content/browser/geolocation/geolocation_service_impl.cc
+++ b/content/browser/geolocation/geolocation_service_impl.cc
@@ -64,7 +64,7 @@
           device::GeolocationManager::GetInstance();
       geolocation_manager) {
     // One call here is enough as the calls are grouped by app name
-    geolocation_manager->AppCeasesToUseGeolocation();
+    geolocation_manager->TrackGeolocationRelinquished();
   }
 }
 
@@ -75,7 +75,7 @@
   if (device::GeolocationManager* geolocation_manager =
           device::GeolocationManager::GetInstance();
       geolocation_manager) {
-    geolocation_manager->AppAttemptsToUseGeolocation();
+    geolocation_manager->TrackGeolocationAttempted();
   }
 }
 
diff --git a/content/browser/loader/navigation_early_hints_browsertest.cc b/content/browser/loader/navigation_early_hints_browsertest.cc
index 21f4950..25963881 100644
--- a/content/browser/loader/navigation_early_hints_browsertest.cc
+++ b/content/browser/loader/navigation_early_hints_browsertest.cc
@@ -5,6 +5,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/run_loop.h"
 #include "base/strings/strcat.h"
+#include "base/strings/string_split.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/sequenced_task_runner.h"
@@ -962,4 +963,87 @@
             nullptr);
 }
 
+namespace {
+
+const char kHttp1EarlyHintsPath[] = "/early-hints";
+
+class Http1EarlyHintsResponse : public net::test_server::HttpResponse {
+ public:
+  Http1EarlyHintsResponse() = default;
+  ~Http1EarlyHintsResponse() override = default;
+
+  void SendResponse(
+      base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) override {
+    base::StringPairs early_hints_headers = {
+        {"Link", "</cacheable.js>; rel=preload; as=script"}};
+    delegate->SendResponseHeaders(net::HTTP_EARLY_HINTS, "Early Hints",
+                                  early_hints_headers);
+
+    base::StringPairs final_response_headers = {
+        {"Content-Type", "text/html"},
+        {"Link", "</cacheable.js>; rel=preload; as=script"}};
+    delegate->SendResponseHeaders(net::HTTP_OK, "OK", final_response_headers);
+
+    delegate->SendContentsAndFinish("<script src=\"cacheable.js\"></script>");
+  }
+};
+
+std::unique_ptr<net::test_server::HttpResponse> HandleHttpEarlyHintsRequest(
+    const net::test_server::HttpRequest& request) {
+  const GURL relative_url = request.base_url.Resolve(request.relative_url);
+  if (relative_url.path() == kHttp1EarlyHintsPath) {
+    return std::make_unique<Http1EarlyHintsResponse>();
+  }
+  return nullptr;
+}
+
+}  // namespace
+
+class NavigationEarlyHintsHttp1Test : public ContentBrowserTest,
+                                      public testing::WithParamInterface<bool> {
+ public:
+  NavigationEarlyHintsHttp1Test() {
+    if (EnableEarlyHintsForHttp1()) {
+      scoped_feature_list_.InitAndEnableFeature(
+          net::features::kEnableEarlyHintsOnHttp11);
+    } else {
+      scoped_feature_list_.InitAndDisableFeature(
+          net::features::kEnableEarlyHintsOnHttp11);
+    }
+  }
+
+  void SetUpOnMainThread() override {
+    ContentBrowserTest::SetUpOnMainThread();
+    host_resolver()->AddRule("*", "127.0.0.1");
+    embedded_test_server()->AddDefaultHandlers();
+    embedded_test_server()->RegisterRequestHandler(
+        base::BindRepeating(&HandleHttpEarlyHintsRequest));
+    ASSERT_TRUE(embedded_test_server()->Start());
+  }
+
+  bool EnableEarlyHintsForHttp1() { return GetParam(); }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+INSTANTIATE_TEST_SUITE_P(All, NavigationEarlyHintsHttp1Test, testing::Bool());
+
+// Tests that Early Hints are allowed or disallowed on HTTP/1.1 based on a
+// feature flag.
+IN_PROC_BROWSER_TEST_P(NavigationEarlyHintsHttp1Test, AllowEarlyHints) {
+  const GURL url = embedded_test_server()->GetURL(kHttp1EarlyHintsPath);
+  ASSERT_TRUE(NavigateToURL(shell(), url));
+
+  NavigationEarlyHintsManager* early_hints_manager =
+      static_cast<RenderFrameHostImpl*>(
+          shell()->web_contents()->GetPrimaryMainFrame())
+          ->early_hints_manager();
+  if (EnableEarlyHintsForHttp1()) {
+    ASSERT_TRUE(early_hints_manager->WasResourceHintsReceived());
+  } else {
+    ASSERT_TRUE(early_hints_manager == nullptr);
+  }
+}
+
 }  // namespace content
diff --git a/content/browser/preloading/prerender/prerender_browsertest.cc b/content/browser/preloading/prerender/prerender_browsertest.cc
index 364c497..fd6b2c7 100644
--- a/content/browser/preloading/prerender/prerender_browsertest.cc
+++ b/content/browser/preloading/prerender/prerender_browsertest.cc
@@ -230,7 +230,10 @@
             &PrerenderBrowserTest::web_contents, base::Unretained(this)));
     feature_list_.InitWithFeatures(
         {blink::features::kPrerender2InNewTab,
-         blink::features::kPrerender2MainFrameNavigation},
+         blink::features::kPrerender2MainFrameNavigation,
+
+         // To enable Content-Security-Policy navigate-to support.
+         features::kExperimentalContentSecurityPolicyFeatures},
         {});
   }
   ~PrerenderBrowserTest() override = default;
@@ -467,9 +470,6 @@
   }
 
   void SetUpCommandLine(base::CommandLine* command_line) override {
-    // Useful for testing CSP:prefetch-src
-    base::CommandLine::ForCurrentProcess()->AppendSwitch(
-        switches::kEnableExperimentalWebPlatformFeatures);
     // The viewport meta tag is only enabled on Android.
 #if BUILDFLAG(IS_ANDROID)
     command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
@@ -9049,6 +9049,41 @@
   ExpectFinalStatusForSpeculationRule(PrerenderFinalStatus::kMixedContent);
 }
 
+IN_PROC_BROWSER_TEST_F(PrerenderBrowserTest, PrerenderBlockedByCspNavigateTo) {
+  const GURL initial_url = GetUrl("/empty.html");
+  ASSERT_TRUE(NavigateToURL(shell(), initial_url));
+
+  // Add a Content-Security-Policy meta tag that restricts prerendering
+  // navigation URLs.
+  EXPECT_TRUE(ExecJs(current_frame_host(), R"(
+    const meta = document.createElement('meta');
+    meta.httpEquiv = "Content-Security-Policy";
+    meta.content = "navigate-to https://a.test:*/empty.html";
+    document.getElementsByTagName('head')[0].appendChild(meta);
+  )"));
+
+  const char* console_pattern =
+      "Refused to navigate to 'https://a.test:*/title1.html' because it "
+      "violates the following Content Security Policy directive: \"navigate-to "
+      "https://a.test:*/empty.html\".\n";
+
+  // Prerender a URL that will be blocked by the navigate-to. As navigation
+  // fails, AddPrerender() doesn't finish. We need to wait its failure by
+  // monitoring a console error for the navigation blocking.
+  WebContentsConsoleObserver console_observer(web_contents());
+  console_observer.SetPattern(console_pattern);
+  const GURL disallowed_url = GetUrl("/title1.html");
+  AddPrerenderAsync(disallowed_url);
+  ASSERT_TRUE(console_observer.Wait());
+
+  EXPECT_FALSE(
+      web_contents_impl()->GetPrerenderHostRegistry()->FindHostByUrlForTesting(
+          disallowed_url));
+  ASSERT_EQ(1u, console_observer.messages().size());
+  // Just in case, no server access should be observed.
+  EXPECT_EQ(GetRequestCount(disallowed_url), 0);
+}
+
 // Check that the Content-Security-Policy set via HTTP header applies after the
 // activation. This test verifies that that the web sandbox flags value is none.
 IN_PROC_BROWSER_TEST_F(PrerenderBrowserTest,
diff --git a/content/browser/preloading/prerender/prerender_host.cc b/content/browser/preloading/prerender/prerender_host.cc
index 10f655c..a708334 100644
--- a/content/browser/preloading/prerender/prerender_host.cc
+++ b/content/browser/preloading/prerender/prerender_host.cc
@@ -4,6 +4,7 @@
 
 #include "content/browser/preloading/prerender/prerender_host.h"
 
+#include "base/debug/alias.h"
 #include "base/feature_list.h"
 #include "base/notreached.h"
 #include "base/observer_list.h"
@@ -353,6 +354,17 @@
     // `begin_params_` and `common_params_` is null here, but it doesn't matter
     // as this branch is reached only when the initial navigation fails,
     // so this PrerenderHost can't be activated.
+
+    // Original code assumes the case CSP prefetch-src blocks prerendering, but
+    // prefetch-src was already deprecated, but this code path seems still
+    // reachable. To be clarify the actual scenario, let's have the dump code.
+    // We may eventually return false for this code path to make things simple.
+    // TODO(https://crbug.com/1394486): Monitor reports and decide if we
+    // continue to have the `is_ready_for_activation_` check in
+    // AreInitialPrerenderNavigationParamsCompatibleWithNavigation().
+    net::Error net_error = created_navigation_handle->GetNetErrorCode();
+    base::debug::Alias(&net_error);
+    base::debug::DumpWithoutCrashing();
   }
 
   NavigationRequest* navigation_request =
@@ -624,6 +636,12 @@
   // defence-in-depth measure.
   CHECK(navigation_request.IsInPrimaryMainFrame());
 
+  // Check `common_params_` and `begin_params_` here as these can be nullptr
+  // if LoadURLWithParams failed without running PrerenderNavigationThrottle.
+  if (!common_params_ || !begin_params_) {
+    return false;
+  }
+
   // Compare BeginNavigationParams.
   ActivationNavigationParamsMatch result =
       AreBeginNavigationParamsCompatibleWithNavigation(
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index f205062..b8fe56a 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -11293,6 +11293,12 @@
       std::move(is_shutting_down_cb), std::move(receiver));
 }
 
+void RenderFrameHostImpl::BindVideoEncoderMetricsProviderReceiver(
+    mojo::PendingReceiver<media::mojom::VideoEncoderMetricsProvider> receiver) {
+  media::VideoEncoderMetricsProvider::Create(GetPageUkmSourceId(),
+                                             std::move(receiver));
+}
+
 #if BUILDFLAG(ENABLE_MEDIA_REMOTING)
 void RenderFrameHostImpl::BindMediaRemoterFactoryReceiver(
     mojo::PendingReceiver<media::mojom::RemoterFactory> receiver) {
diff --git a/content/browser/renderer_host/render_frame_host_impl.h b/content/browser/renderer_host/render_frame_host_impl.h
index 09ddb217..2a287fb 100644
--- a/content/browser/renderer_host/render_frame_host_impl.h
+++ b/content/browser/renderer_host/render_frame_host_impl.h
@@ -89,7 +89,9 @@
 #include "media/mojo/mojom/interface_factory.mojom-forward.h"
 #include "media/mojo/mojom/media_metrics_provider.mojom-forward.h"
 #include "media/mojo/mojom/media_player.mojom-forward.h"
+#include "media/mojo/mojom/video_encoder_metrics_provider.mojom-forward.h"
 #include "media/mojo/services/media_metrics_provider.h"
+#include "media/mojo/services/video_encoder_metrics_provider.h"
 #include "mojo/public/cpp/bindings/associated_receiver.h"
 #include "mojo/public/cpp/bindings/associated_remote.h"
 #include "mojo/public/cpp/bindings/pending_associated_remote.h"
@@ -2077,6 +2079,10 @@
   void BindMediaMetricsProviderReceiver(
       mojo::PendingReceiver<media::mojom::MediaMetricsProvider> receiver);
 
+  void BindVideoEncoderMetricsProviderReceiver(
+      mojo::PendingReceiver<media::mojom::VideoEncoderMetricsProvider>
+          receiver);
+
 #if BUILDFLAG(ENABLE_MEDIA_REMOTING)
   void BindMediaRemoterFactoryReceiver(
       mojo::PendingReceiver<media::mojom::RemoterFactory> receiver);
diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc
index a122bd21..0e521106 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura.cc
@@ -59,7 +59,6 @@
 #include "third_party/blink/public/common/input/web_input_event.h"
 #include "third_party/blink/public/mojom/input/input_handler.mojom-forward.h"
 #include "third_party/blink/public/mojom/widget/record_content_to_visible_time_request.mojom.h"
-#include "ui/accessibility/accessibility_switches.h"
 #include "ui/accessibility/aura/aura_window_properties.h"
 #include "ui/accessibility/platform/ax_platform_node.h"
 #include "ui/aura/client/aura_constants.h"
diff --git a/content/browser/service_worker/service_worker_registry.cc b/content/browser/service_worker/service_worker_registry.cc
index 4a23cee..788b5dea 100644
--- a/content/browser/service_worker/service_worker_registry.cc
+++ b/content/browser/service_worker/service_worker_registry.cc
@@ -572,6 +572,9 @@
   for (const blink::mojom::WebFeature feature : version->used_features())
     data->used_features.push_back(feature);
   data->ancestor_frame_type = registration->ancestor_frame_type();
+  if (version->router_evaluator()) {
+    data->router_rules = version->router_evaluator()->rules();
+  }
 
   // The ServiceWorkerVersion's policy container host might be null if it is
   // stored before loading the main script. This happens in many unittests.
@@ -1101,6 +1104,11 @@
           base::MakeRefCounted<PolicyContainerHost>(
               PolicyContainerPolicies(*data.policy_container_policies)));
     }
+    if (data.router_rules) {
+      bool status = version->SetupRouterEvaluator(*data.router_rules);
+      DCHECK(status) << "Failed to setup RouterEvaluator from the provided "
+                     << "rules. Possibly the database is corrupted.";
+    }
   }
   version->set_script_response_time_for_devtools(data.script_response_time);
 
diff --git a/content/browser/service_worker/service_worker_version.cc b/content/browser/service_worker/service_worker_version.cc
index db4223ca..5945fb8c 100644
--- a/content/browser/service_worker/service_worker_version.cc
+++ b/content/browser/service_worker/service_worker_version.cc
@@ -1813,6 +1813,25 @@
     registration->ActivateWaitingVersionWhenReady();
 }
 
+void ServiceWorkerVersion::RegisterRouter(
+    const blink::ServiceWorkerRouterRules& rules,
+    RegisterRouterCallback callback) {
+  if (router_evaluator()) {
+    // The renderer should have denied calling this twice.
+    receiver_.ReportBadMessage("The ServiceWorker router rules are set twice.");
+    return;
+  }
+  if (!SetupRouterEvaluator(rules)) {
+    // The renderer should have denied calling this method while the setup
+    // fails.
+    // TODO(crbug.com/1371756): revisit this to confirm no case for this error.
+    receiver_.ReportBadMessage(
+        "Failed to configure a router. Possibly a syntax error");
+    return;
+  }
+  std::move(callback).Run();
+}
+
 void ServiceWorkerVersion::OnSetCachedMetadataFinished(int64_t callback_id,
                                                        size_t size,
                                                        int result) {
diff --git a/content/browser/service_worker/service_worker_version.h b/content/browser/service_worker/service_worker_version.h
index 7c749bbb..3fca259 100644
--- a/content/browser/service_worker/service_worker_version.h
+++ b/content/browser/service_worker/service_worker_version.h
@@ -915,6 +915,8 @@
                       const GURL& url,
                       NavigateClientCallback callback) override;
   void SkipWaiting(SkipWaitingCallback callback) override;
+  void RegisterRouter(const blink::ServiceWorkerRouterRules& rules,
+                      RegisterRouterCallback callback) override;
 
   void OnSetCachedMetadataFinished(int64_t callback_id,
                                    size_t size,
diff --git a/content/child/runtime_features.cc b/content/child/runtime_features.cc
index 611f926..65b930f 100644
--- a/content/child/runtime_features.cc
+++ b/content/child/runtime_features.cc
@@ -323,6 +323,8 @@
      raw_ref(features::kRemoveMobileViewportDoubleTap)},
     {wf::EnableServiceWorkerBypassFetchHandler,
      raw_ref(features::kServiceWorkerBypassFetchHandler)},
+    {wf::EnableServiceWorkerStaticRouter,
+     raw_ref(features::kServiceWorkerStaticRouter)},
   };
   for (const auto& mapping : blinkFeatureToBaseFeatureMapping) {
     SetRuntimeFeatureFromChromiumFeature(
diff --git a/content/public/common/content_features.cc b/content/public/common/content_features.cc
index 40ec406..2f63633 100644
--- a/content/public/common/content_features.cc
+++ b/content/public/common/content_features.cc
@@ -1096,6 +1096,12 @@
         0,
     };
 
+// Enables ServiceWorker static routing API.
+// https://github.com/yoshisatoyanagisawa/service-worker-static-routing-api
+BASE_FEATURE(kServiceWorkerStaticRouter,
+             "ServiceWorkerStaticRouter",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 // Run video capture service in the Browser process as opposed to a dedicated
 // utility process
 BASE_FEATURE(kRunVideoCaptureServiceInBrowserProcess,
diff --git a/content/public/common/content_features.h b/content/public/common/content_features.h
index ca4b212..e03e8dd 100644
--- a/content/public/common/content_features.h
+++ b/content/public/common/content_features.h
@@ -281,6 +281,7 @@
     kAsyncStartServiceWorkerForEmptyFetchHandler;
 CONTENT_EXPORT extern const base::FeatureParam<int>
     kAsyncStartServiceWorkerForEmptyFetchHandlerDurationInMs;
+CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerStaticRouter);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kUserMediaCaptureOnFocus);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kWebLockScreenApi);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kWebOTP);
diff --git a/gpu/command_buffer/service/abstract_texture.h b/gpu/command_buffer/service/abstract_texture.h
index fd85cb9..d5bc607f 100644
--- a/gpu/command_buffer/service/abstract_texture.h
+++ b/gpu/command_buffer/service/abstract_texture.h
@@ -67,11 +67,6 @@
   virtual void SetBoundImage(gl::GLImage* image) = 0;
 #endif
 
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_APPLE)
-  // Return the image, if any, for testing purposes.
-  virtual gl::GLImage* GetImageForTesting() const = 0;
-#endif
-
   // Marks the texture as cleared, to help prevent sending an uninitialized
   // texture to the (untrusted) renderer.  One should call this only when one
   // has actually initialized the texture.
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest.cc
index 889310f..390eb6b 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest.cc
@@ -329,18 +329,11 @@
 
   // Binding an image should make the texture renderable.
   EXPECT_EQ(texture->SafeToRenderFrom(), true);
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
-  EXPECT_EQ(abstract_texture->GetImageForTesting(), image.get());
-  EXPECT_EQ(texture->GetLevelImage(target, 0), image.get());
-#endif
 
   // Unbinding should make it not renderable.
   abstract_texture->SetBoundImage(nullptr);
 
   EXPECT_EQ(texture->SafeToRenderFrom(), false);
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
-  EXPECT_EQ(abstract_texture->GetImageForTesting(), nullptr);
-#endif
 
   // Deleting |abstract_texture| should delete the platform texture as well,
   // since we haven't make a copy of the TextureRef.  Also make sure that the
diff --git a/gpu/command_buffer/service/passthrough_abstract_texture_impl.cc b/gpu/command_buffer/service/passthrough_abstract_texture_impl.cc
index 06ba0ab..db84e5b 100644
--- a/gpu/command_buffer/service/passthrough_abstract_texture_impl.cc
+++ b/gpu/command_buffer/service/passthrough_abstract_texture_impl.cc
@@ -58,17 +58,6 @@
 }
 #endif
 
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_APPLE)
-gl::GLImage* PassthroughAbstractTextureImpl::GetImageForTesting() const {
-  if (!texture_passthrough_)
-    return nullptr;
-
-  const GLint level = 0;
-  return texture_passthrough_->GetLevelImage(texture_passthrough_->target(),
-                                             level);
-}
-#endif
-
 void PassthroughAbstractTextureImpl::SetCleared() {
   // The passthrough decoder has no notion of 'cleared', so do nothing.
 }
diff --git a/gpu/command_buffer/service/passthrough_abstract_texture_impl.h b/gpu/command_buffer/service/passthrough_abstract_texture_impl.h
index 4128283..edc6e7e 100644
--- a/gpu/command_buffer/service/passthrough_abstract_texture_impl.h
+++ b/gpu/command_buffer/service/passthrough_abstract_texture_impl.h
@@ -35,9 +35,6 @@
   void SetBoundImage(gl::GLImage* image) override;
 #endif
 
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_APPLE)
-  gl::GLImage* GetImageForTesting() const override;
-#endif
   void SetCleared() override;
   void SetCleanupCallback(CleanupCallback cb) override;
   void NotifyOnContextLost() override;
diff --git a/gpu/command_buffer/service/texture_manager.cc b/gpu/command_buffer/service/texture_manager.cc
index f7552a3..eada05e6 100644
--- a/gpu/command_buffer/service/texture_manager.cc
+++ b/gpu/command_buffer/service/texture_manager.cc
@@ -572,22 +572,6 @@
   SetLevelImageInternal(target, level, image, owned_service_id_);
 }
 #endif
-
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_APPLE)
-gl::GLImage* TexturePassthrough::GetLevelImage(GLenum target,
-                                               GLint level) const {
-#if BUILDFLAG(IS_APPLE)
-  return nullptr;
-#else
-  size_t face_idx = 0;
-  if (!LevelInfoExists(target, level, &face_idx)) {
-    return nullptr;
-  }
-
-  return level_images_[face_idx][level].image.get();
-#endif
-}
-#endif
 #endif
 
 #if BUILDFLAG(IS_ANDROID)
@@ -1930,16 +1914,6 @@
   return nullptr;
 }
 
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_APPLE)
-gl::GLImage* Texture::GetLevelImage(GLint target, GLint level) const {
-  const LevelInfo* info = GetLevelInfo(target, level);
-  if (!info)
-    return nullptr;
-
-  return info->image.get();
-}
-#endif
-
 void Texture::DumpLevelMemory(base::trace_event::ProcessMemoryDump* pmd,
                               uint64_t client_tracing_id,
                               const std::string& dump_name) const {
diff --git a/gpu/command_buffer/service/texture_manager.h b/gpu/command_buffer/service/texture_manager.h
index 8d7c3c92..96eb5d2 100644
--- a/gpu/command_buffer/service/texture_manager.h
+++ b/gpu/command_buffer/service/texture_manager.h
@@ -79,14 +79,9 @@
   // native GL texture in the destructor
   void MarkContextLost();
 
-#if !BUILDFLAG(IS_ANDROID)
-#if !BUILDFLAG(IS_APPLE)
+#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_APPLE)
   void SetLevelImage(GLenum target, GLint level, gl::GLImage* image);
 #endif
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_APPLE)
-  gl::GLImage* GetLevelImage(GLenum target, GLint level) const;
-#endif
-#endif
 
 #if BUILDFLAG(IS_ANDROID)
   void BindToServiceId(GLuint service_id);
@@ -332,12 +327,6 @@
       GLenum type,
       const SamplerState& sampler_state) const;
 
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_APPLE)
-  // Get the image associated with a particular level. Returns NULL if level
-  // does not exist.
-  gl::GLImage* GetLevelImage(GLint target, GLint level) const;
-#endif
-
   bool HasImages() const {
     return has_images_;
   }
diff --git a/gpu/command_buffer/service/texture_manager_unittest.cc b/gpu/command_buffer/service/texture_manager_unittest.cc
index dce0cae..afe61b9 100644
--- a/gpu/command_buffer/service/texture_manager_unittest.cc
+++ b/gpu/command_buffer/service/texture_manager_unittest.cc
@@ -1679,32 +1679,6 @@
   texture_ref = nullptr;
 }
 
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_APPLE)
-TEST_F(TextureTest, GetLevelImage) {
-  manager_->SetTarget(texture_ref_.get(), GL_TEXTURE_2D);
-  manager_->SetLevelInfo(texture_ref_.get(), GL_TEXTURE_2D, 1, GL_RGBA, 2, 2, 1,
-                         0, GL_RGBA, GL_UNSIGNED_BYTE, gfx::Rect(2, 2));
-  Texture* texture = texture_ref_->texture();
-  EXPECT_TRUE(texture->GetLevelImage(GL_TEXTURE_2D, 1) == nullptr);
-  // Set image.
-  scoped_refptr<gl::GLImage> image(new GLImageStub);
-  manager_->SetBoundLevelImage(texture_ref_.get(), GL_TEXTURE_2D, 1,
-                               image.get());
-  EXPECT_FALSE(texture->GetLevelImage(GL_TEXTURE_2D, 1) == nullptr);
-  // Remove it.
-  manager_->UnsetLevelImage(texture_ref_.get(), GL_TEXTURE_2D, 1);
-  EXPECT_TRUE(texture->GetLevelImage(GL_TEXTURE_2D, 1) == nullptr);
-
-  // Re-add it, and check that it's reset when SetLevelInfo is called.
-  manager_->SetBoundLevelImage(texture_ref_.get(), GL_TEXTURE_2D, 1,
-                               image.get());
-  manager_->SetLevelInfo(texture_ref_.get(), GL_TEXTURE_2D, 1, GL_RGBA, 2, 2, 1,
-                         0, GL_RGBA, GL_UNSIGNED_BYTE, gfx::Rect(2, 2));
-  EXPECT_TRUE(texture->GetLevelImage(GL_TEXTURE_2D, 1) == nullptr);
-}
-
-#endif
-
 #if BUILDFLAG(IS_ANDROID)
 TEST_F(TextureTest, SetStreamTextureImageServiceID) {
   manager_->SetTarget(texture_ref_.get(), GL_TEXTURE_EXTERNAL_OES);
@@ -2098,9 +2072,6 @@
   scoped_refptr<TextureRef> restored_texture = manager_->GetTexture(client_id);
   EXPECT_EQ(produced_texture, restored_texture->texture());
   EXPECT_EQ(service_id, restored_texture->service_id());
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_APPLE)
-  EXPECT_EQ(image.get(), restored_texture->texture()->GetLevelImage(target, 0));
-#endif
 }
 
 static const GLenum kTextureTargets[] = {GL_TEXTURE_2D, GL_TEXTURE_EXTERNAL_OES,
diff --git a/gpu/command_buffer/service/validating_abstract_texture_impl.cc b/gpu/command_buffer/service/validating_abstract_texture_impl.cc
index 5e15cdf..2a63d2f 100644
--- a/gpu/command_buffer/service/validating_abstract_texture_impl.cc
+++ b/gpu/command_buffer/service/validating_abstract_texture_impl.cc
@@ -73,17 +73,6 @@
 }
 #endif
 
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_APPLE)
-gl::GLImage* ValidatingAbstractTextureImpl::GetImageForTesting() const {
-  if (!texture_ref_)
-    return nullptr;
-
-  const GLuint target = texture_ref_->texture()->target();
-  const GLint level = 0;
-  return texture_ref_->texture()->GetLevelImage(target, level);
-}
-#endif
-
 void ValidatingAbstractTextureImpl::SetCleared() {
   if (!texture_ref_)
     return;
diff --git a/gpu/command_buffer/service/validating_abstract_texture_impl.h b/gpu/command_buffer/service/validating_abstract_texture_impl.h
index a17abdc21..dd2732c4 100644
--- a/gpu/command_buffer/service/validating_abstract_texture_impl.h
+++ b/gpu/command_buffer/service/validating_abstract_texture_impl.h
@@ -40,9 +40,6 @@
   void SetBoundImage(gl::GLImage* image) override;
 #endif
 
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_APPLE)
-  gl::GLImage* GetImageForTesting() const override;
-#endif
   void SetCleared() override;
   void SetCleanupCallback(CleanupCallback cb) override;
   void NotifyOnContextLost() override;
diff --git a/infra/config/generated/luci/cr-buildbucket.cfg b/infra/config/generated/luci/cr-buildbucket.cfg
index 1c347b1..bc997a26 100644
--- a/infra/config/generated/luci/cr-buildbucket.cfg
+++ b/infra/config/generated/luci/cr-buildbucket.cfg
@@ -608,7 +608,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -3106,7 +3106,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -11113,7 +11113,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -11833,7 +11833,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -14001,7 +14001,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -17004,7 +17004,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -17178,7 +17178,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -17781,7 +17781,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -21215,7 +21215,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -25128,7 +25128,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -25558,7 +25558,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -26035,7 +26035,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -26710,7 +26710,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -26951,7 +26951,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -27203,7 +27203,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -27287,7 +27287,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -27539,7 +27539,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -27803,7 +27803,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -28234,7 +28234,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -28490,7 +28490,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -28574,7 +28574,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -28658,7 +28658,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -29047,7 +29047,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -31823,7 +31823,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -32163,7 +32163,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -36382,7 +36382,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -40932,7 +40932,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -44311,7 +44311,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -44563,7 +44563,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -44897,7 +44897,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -44984,7 +44984,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -45821,7 +45821,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -46240,7 +46240,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -48863,7 +48863,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -49849,7 +49849,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -52186,7 +52186,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -52440,7 +52440,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -53663,7 +53663,7 @@
       dimensions: "cores:8"
       dimensions: "cpu:x86-64"
       dimensions: "free_space:standard"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -55294,7 +55294,7 @@
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
       dimensions: "cores:8"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
@@ -55739,7 +55739,7 @@
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
       dimensions: "cores:8"
-      dimensions: "os:Ubuntu-18.04"
+      dimensions: "os:Ubuntu-22.04"
       dimensions: "pool:luci.chromium.ci"
       dimensions: "ssd:0"
       exe {
diff --git a/infra/config/lib/linux-default.json b/infra/config/lib/linux-default.json
index cc3aac5..bf8a953 100644
--- a/infra/config/lib/linux-default.json
+++ b/infra/config/lib/linux-default.json
@@ -1,6 +1,7 @@
 {
   "ci": {
     "3pp-linux-amd64-packager": "Ubuntu-22.04",
+    "ASan Release Media (32-bit x86 with V8-ARM)": "Ubuntu-22.04",
     "Android ASAN (dbg)": "Ubuntu-22.04",
     "Android FYI Release (NVIDIA Shield TV)": "Ubuntu-22.04",
     "Android FYI Release (Nexus 5X)": "Ubuntu-22.04",
@@ -25,6 +26,7 @@
     "Cast Linux": "Ubuntu-22.04",
     "Cast Linux ARM64": "Ubuntu-22.04",
     "Cast Linux Debug": "Ubuntu-22.04",
+    "Centipede Upload Linux ASan": "Ubuntu-22.04",
     "ChromeOS FYI Release (amd64-generic)": "Ubuntu-22.04",
     "ChromeOS FYI Release (kevin)": "Ubuntu-22.04",
     "ChromeOS FYI Release Skylab (kevin)": "Ubuntu-22.04",
@@ -80,8 +82,10 @@
     "Leak Detection Linux": "Ubuntu-22.04",
     "Libfuzzer Upload Linux ASan": "Ubuntu-22.04",
     "Libfuzzer Upload Linux ASan Debug": "Ubuntu-22.04",
+    "Libfuzzer Upload Linux V8-ARM64 ASan": "Ubuntu-22.04",
     "Libfuzzer Upload Linux32 V8-ARM ASan Debug": "Ubuntu-22.04",
     "Linux ASan LSan Builder": "Ubuntu-22.04",
+    "Linux ASan LSan Tests (1)": "Ubuntu-22.04",
     "Linux Builder": "Ubuntu-22.04",
     "Linux Builder (Wayland)": "Ubuntu-22.04",
     "Linux Builder (dbg)": "Ubuntu-22.04",
@@ -99,6 +103,7 @@
     "Linux FYI Release (Intel UHD 630)": "Ubuntu-22.04",
     "Linux FYI Release (NVIDIA)": "Ubuntu-22.04",
     "Linux Release (NVIDIA)": "Ubuntu-22.04",
+    "Linux TSan Tests": "Ubuntu-22.04",
     "Linux Tests": "Ubuntu-22.04",
     "Linux Tests (Wayland)": "Ubuntu-22.04",
     "Linux Tests (dbg)(1)": "Ubuntu-22.04",
@@ -121,7 +126,10 @@
     "Mac Release (Intel)": "Ubuntu-22.04",
     "Mac Retina Debug (AMD)": "Ubuntu-22.04",
     "Mac Retina Release (AMD)": "Ubuntu-22.04",
+    "Mac10.13 Tests": "Ubuntu-22.04",
+    "Mac10.15 Tests": "Ubuntu-22.04",
     "Network Service Linux": "Ubuntu-22.04",
+    "Oreo Phone Tester": "Ubuntu-22.04",
     "Site Isolation Android": "Ubuntu-22.04",
     "ToTAndroid": "Ubuntu-22.04",
     "ToTAndroid (dbg)": "Ubuntu-22.04",
@@ -143,6 +151,7 @@
     "ToTLinuxPGO": "Ubuntu-22.04",
     "ToTLinuxTSan": "Ubuntu-22.04",
     "ToTLinuxUBSanVptr": "Ubuntu-22.04",
+    "UBSan Release": "Ubuntu-22.04",
     "VR Linux": "Ubuntu-22.04",
     "WebKit Linux Leak": "Ubuntu-22.04",
     "Win10 FYI x64 DX12 Vulkan Debug (NVIDIA)": "Ubuntu-22.04",
@@ -160,22 +169,36 @@
     "android-12-x64-fyi-rel": "Ubuntu-22.04",
     "android-angle-chromium-arm64-builder": "Ubuntu-22.04",
     "android-angle-chromium-arm64-nexus5x": "Ubuntu-22.04",
+    "android-annotator-rel": "Ubuntu-22.04",
     "android-archive-dbg": "Ubuntu-22.04",
     "android-archive-rel": "Ubuntu-22.04",
     "android-arm64-archive-rel": "Ubuntu-22.04",
+    "android-asan": "Ubuntu-22.04",
     "android-bfcache-rel": "Ubuntu-22.04",
     "android-binary-size-generator": "Ubuntu-22.04",
     "android-build-perf-developer": "Ubuntu-22.04",
+    "android-chrome-pie-x86-wpt-fyi-rel": "Ubuntu-22.04",
     "android-code-coverage": "Ubuntu-22.04",
     "android-code-coverage-native": "Ubuntu-22.04",
     "android-cronet-arm-rel": "Ubuntu-22.04",
+    "android-cronet-asan-x86-rel": "Ubuntu-22.04",
     "android-cronet-x86-dbg": "Ubuntu-22.04",
+    "android-cronet-x86-dbg-10-tests": "Ubuntu-22.04",
     "android-cronet-x86-dbg-11-tests": "Ubuntu-22.04",
+    "android-cronet-x86-dbg-marshmallow-tests": "Ubuntu-22.04",
+    "android-cronet-x86-dbg-oreo-tests": "Ubuntu-22.04",
     "android-cronet-x86-rel": "Ubuntu-22.04",
+    "android-cronet-x86-rel-kitkat-tests": "Ubuntu-22.04",
+    "android-nougat-x86-rel": "Ubuntu-22.04",
     "android-official": "Ubuntu-22.04",
     "android-perfetto-rel": "Ubuntu-22.04",
+    "android-pie-x86-fyi-rel": "Ubuntu-22.04",
     "android-pie-x86-rel": "Ubuntu-22.04",
     "android-rust-arm32-rel": "Ubuntu-22.04",
+    "android-rust-arm64-dbg": "Ubuntu-22.04",
+    "android-rust-arm64-rel": "Ubuntu-22.04",
+    "android-sdk-packager": "Ubuntu-22.04",
+    "android-webview-13-x64-dbg-tests": "Ubuntu-22.04",
     "android-x86-code-coverage": "Ubuntu-22.04",
     "build-perf-android": "Ubuntu-22.04",
     "build-perf-android-siso": "Ubuntu-22.04",
@@ -197,8 +220,10 @@
     "fuchsia-angle-builder": "Ubuntu-22.04",
     "fuchsia-arm64-rel": "Ubuntu-22.04",
     "fuchsia-code-coverage": "Ubuntu-22.04",
+    "fuchsia-fyi-x64-dbg": "Ubuntu-22.04",
     "fuchsia-official": "Ubuntu-22.04",
     "fuchsia-x64-accessibility-rel": "Ubuntu-22.04",
+    "fuchsia-x64-cast-receiver-rel": "Ubuntu-22.04",
     "ios-angle-intel": "Ubuntu-22.04",
     "lacros-amd64-generic-binary-size-rel": "Ubuntu-22.04",
     "lacros-amd64-generic-rel": "Ubuntu-22.04",
@@ -219,6 +244,7 @@
     "linux-angle-chromium-intel": "Ubuntu-22.04",
     "linux-angle-chromium-nvidia": "Ubuntu-22.04",
     "linux-annotator-rel": "Ubuntu-22.04",
+    "linux-archive-dbg": "Ubuntu-22.04",
     "linux-archive-rel": "Ubuntu-22.04",
     "linux-ash-chromium-generator-rel": "Ubuntu-22.04",
     "linux-bfcache-rel": "Ubuntu-22.04",
@@ -264,6 +290,7 @@
     "linux-swangle-x64": "Ubuntu-22.04",
     "linux-ubsan-vptr": "Ubuntu-22.04",
     "linux-updater-builder-dbg": "Ubuntu-22.04",
+    "linux-updater-builder-rel": "Ubuntu-22.04",
     "linux-upload-perfetto": "Ubuntu-22.04",
     "linux-win_cross-rel": "Ubuntu-22.04",
     "linux-wpt-content-shell-asan-fyi-rel": "Ubuntu-22.04",
@@ -274,9 +301,15 @@
     "linux-wpt-input-fyi-rel": "Ubuntu-22.04",
     "mac-angle-chromium-amd": "Ubuntu-22.04",
     "mac-angle-chromium-intel": "Ubuntu-22.04",
+    "mac10.13-updater-tester-dbg": "Ubuntu-22.04",
+    "mac10.14-updater-tester-rel": "Ubuntu-22.04",
     "mac10.15-updater-tester-rel": "Ubuntu-22.04",
+    "mac11-arm64-rel-tests": "Ubuntu-22.04",
+    "mac11-arm64-updater-tester-dbg": "Ubuntu-22.04",
     "mac12-arm64-updater-tester-rel": "Ubuntu-22.04",
     "mac12-x64-updater-tester-asan-dbg": "Ubuntu-22.04",
+    "mac13-arm64-rel-tests": "Ubuntu-22.04",
+    "metadata-exporter": "Ubuntu-22.04",
     "rts-model-packager": "Ubuntu-22.04",
     "win-annotator-rel": "Ubuntu-22.04",
     "win-cr23-rel": "Ubuntu-22.04",
@@ -284,19 +317,28 @@
     "win-perfetto-rel": "Ubuntu-22.04",
     "win-upload-perfetto": "Ubuntu-22.04",
     "win10-32-on-64-updater-tester-dbg": "Ubuntu-22.04",
+    "win10-32-on-64-updater-tester-rel": "Ubuntu-22.04",
     "win10-angle-chromium-x64-intel": "Ubuntu-22.04",
     "win10-angle-chromium-x64-nvidia": "Ubuntu-22.04",
     "win10-updater-tester-dbg": "Ubuntu-22.04",
     "win10-updater-tester-dbg-uac": "Ubuntu-22.04",
     "win10-wpt-content-shell-fyi-rel": "Ubuntu-22.04",
+    "win11-updater-tester-dbg-uac": "Ubuntu-22.04",
     "win11-wpt-content-shell-fyi-rel": "Ubuntu-22.04"
   },
+  "goma": {
+    "android-archive-dbg-goma-rbe-ats-canary": "Ubuntu-22.04",
+    "linux-archive-rel-goma-rbe-ats-canary": "Ubuntu-22.04"
+  },
   "reclient": {
-    "Linux Builder (canonical wd) (reclient compare)": "Ubuntu-22.04"
+    "Linux Builder (canonical wd) (reclient compare)": "Ubuntu-22.04",
+    "Simple Chrome Builder reclient staging": "Ubuntu-22.04"
   },
   "reviver": {
+    "android-launcher": "Ubuntu-22.04",
     "fuchsia-coordinator": "Ubuntu-22.04",
-    "lacros-coordinator": "Ubuntu-22.04"
+    "lacros-coordinator": "Ubuntu-22.04",
+    "win-launcher": "Ubuntu-22.04"
   },
   "try": {
     "android-11-x86-rel": "Ubuntu-22.04",
diff --git a/ios/chrome/browser/promos_manager/promos_manager_impl.mm b/ios/chrome/browser/promos_manager/promos_manager_impl.mm
index 4ffb9be..a4890e17 100644
--- a/ios/chrome/browser/promos_manager/promos_manager_impl.mm
+++ b/ios/chrome/browser/promos_manager/promos_manager_impl.mm
@@ -548,7 +548,14 @@
   auto compare_promo = [&impression_history](
                            std::pair<promos_manager::Promo, PromoContext> lhs,
                            std::pair<promos_manager::Promo, PromoContext> rhs) {
-    // PostRestoreSignIn types are to be displayed first.
+    // Choice types are to be displayed first.
+    if (lhs.first == Promo::Choice) {
+      return true;
+    }
+    if (rhs.first == Promo::Choice) {
+      return false;
+    }
+    // PostRestoreSignIn types come next.
     if (lhs.first == Promo::PostRestoreSignInFullscreen ||
         lhs.first == Promo::PostRestoreSignInAlert) {
       return true;
diff --git a/ios/chrome/browser/promos_manager/promos_manager_impl_unittest.mm b/ios/chrome/browser/promos_manager/promos_manager_impl_unittest.mm
index ac093a1b..492e750 100644
--- a/ios/chrome/browser/promos_manager/promos_manager_impl_unittest.mm
+++ b/ios/chrome/browser/promos_manager/promos_manager_impl_unittest.mm
@@ -559,7 +559,8 @@
   EXPECT_EQ(promos_manager_->SortPromos(active_promos), expected);
 }
 
-// Tests `SortPromos` sorts `PostRestoreSignIn` promos before others.
+// Tests `SortPromos` sorts `Choice` promos before others and
+// `PostRestoreSignIn` next.
 TEST_F(PromosManagerImplTest, SortsPromosPreferCertainTypes) {
   CreatePromosManager();
 
@@ -568,6 +569,7 @@
       {promos_manager::Promo::DefaultBrowser, PromoContext{true}},
       {promos_manager::Promo::PostRestoreSignInFullscreen, PromoContext{false}},
       {promos_manager::Promo::PostRestoreSignInAlert, PromoContext{false}},
+      {promos_manager::Promo::Choice, PromoContext{false}},
   };
 
   int today = TodaysDay();
@@ -586,13 +588,15 @@
   promos_manager_->impression_history_ = impressions;
   std::vector<promos_manager::Promo> sorted =
       promos_manager_->SortPromos(active_promos);
-  EXPECT_EQ(sorted.size(), (size_t)4);
+  EXPECT_EQ(sorted.size(), (size_t)5);
+  // Choice comes first
+  EXPECT_TRUE(sorted[0] == promos_manager::Promo::Choice);
   // tied for the type.
-  EXPECT_TRUE(sorted[0] == promos_manager::Promo::PostRestoreSignInFullscreen ||
-              sorted[0] == promos_manager::Promo::PostRestoreSignInAlert);
+  EXPECT_TRUE(sorted[1] == promos_manager::Promo::PostRestoreSignInFullscreen ||
+              sorted[1] == promos_manager::Promo::PostRestoreSignInAlert);
   // with pending state, before the less recently shown promo (Test).
-  EXPECT_EQ(sorted[2], promos_manager::Promo::DefaultBrowser);
-  EXPECT_EQ(sorted[3], promos_manager::Promo::Test);
+  EXPECT_EQ(sorted[3], promos_manager::Promo::DefaultBrowser);
+  EXPECT_EQ(sorted[4], promos_manager::Promo::Test);
 }
 
 // Tests `SortPromos` sorts promos with pending state before others without.
diff --git a/ios/google_internal/frameworks/ChromeInternal.framework.dSYM.ios.zip.sha1 b/ios/google_internal/frameworks/ChromeInternal.framework.dSYM.ios.zip.sha1
index 94b375b3..b147729 100644
--- a/ios/google_internal/frameworks/ChromeInternal.framework.dSYM.ios.zip.sha1
+++ b/ios/google_internal/frameworks/ChromeInternal.framework.dSYM.ios.zip.sha1
@@ -1 +1 @@
-73923d5b84a13e968b6ade6c9ed6c9fdf797e3f0
\ No newline at end of file
+4e183621aef1d69bfbbc8e8ee6fcce894abbd21b
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/ChromeSSOInternal.framework.dSYM.ios.zip.sha1 b/ios/google_internal/frameworks/ChromeSSOInternal.framework.dSYM.ios.zip.sha1
index 46cbbf3..da5a13e 100644
--- a/ios/google_internal/frameworks/ChromeSSOInternal.framework.dSYM.ios.zip.sha1
+++ b/ios/google_internal/frameworks/ChromeSSOInternal.framework.dSYM.ios.zip.sha1
@@ -1 +1 @@
-7faa08ca66730d92203afcca6054f413c12e702d
\ No newline at end of file
+4dd3f4f38deeee69863d8c8bf925900122d22655
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.ios.zip.sha1 b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.ios.zip.sha1
index 787a2a1..21af408 100644
--- a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.ios.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.ios.zip.sha1
@@ -1 +1 @@
-4a0bb62f5261263ce5e66c7b60b9793ffd08f5a3
\ No newline at end of file
+6dbf4ce31ef69cba81d62cd1fe4a0e35a4a7c9f2
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.iossimulator.zip.sha1 b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.iossimulator.zip.sha1
index f39c789..425ba3b 100644
--- a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.iossimulator.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.iossimulator.zip.sha1
@@ -1 +1 @@
-03466c84504eccb0bb206930cb82a7b08fc0216a
\ No newline at end of file
+1170bb515efa06464c5fe912b504c240c51e4352
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.ios.zip.sha1 b/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.ios.zip.sha1
index 50efb87..9fab454 100644
--- a/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.ios.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.ios.zip.sha1
@@ -1 +1 @@
-f8e5f1423ad43c85b3aef6c91db69a232e02573e
\ No newline at end of file
+b176785bbae9c11e0ba069f571a68e956984dd95
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.iossimulator.zip.sha1 b/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.iossimulator.zip.sha1
index b171683e..e0e9cf2 100644
--- a/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.iossimulator.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.iossimulator.zip.sha1
@@ -1 +1 @@
-2389e204f10df680affd5bf7ce07b74fb3cddcac
\ No newline at end of file
+e8d416c8afea6d257ef1c852067757d18441b2d7
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/chrome_test_internal_dynamic_framework.ios.zip.sha1 b/ios/google_internal/frameworks/chrome_test_internal_dynamic_framework.ios.zip.sha1
index 7e46501..08ce54a5 100644
--- a/ios/google_internal/frameworks/chrome_test_internal_dynamic_framework.ios.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_test_internal_dynamic_framework.ios.zip.sha1
@@ -1 +1 @@
-acf3532718ab894f2800233da3c1cdfd6aa6dfb4
\ No newline at end of file
+deea15a31dfc629c977aef51f612a5afb0e9604d
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/chrome_test_internal_dynamic_framework.iossimulator.zip.sha1 b/ios/google_internal/frameworks/chrome_test_internal_dynamic_framework.iossimulator.zip.sha1
index ccae72b5..6c9c213 100644
--- a/ios/google_internal/frameworks/chrome_test_internal_dynamic_framework.iossimulator.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_test_internal_dynamic_framework.iossimulator.zip.sha1
@@ -1 +1 @@
-de3438c3415d671d88dee4a06cadc277780b2ad9
\ No newline at end of file
+1028d583855ca5cf15d2c24d036aab3e04f0442f
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.ios.zip.sha1 b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.ios.zip.sha1
index 1faf4038..c55c2fa 100644
--- a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.ios.zip.sha1
+++ b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.ios.zip.sha1
@@ -1 +1 @@
-017743e976a2fd2c527b91a79bfd8cb54012990f
\ No newline at end of file
+84accfe866937cf0d68ec859a2bb096d4e233427
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.iossimulator.zip.sha1 b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.iossimulator.zip.sha1
index e239c85b..0703be0 100644
--- a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.iossimulator.zip.sha1
+++ b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.iossimulator.zip.sha1
@@ -1 +1 @@
-50d757824a37fb5cbf188c7aa1010a7f5da2651d
\ No newline at end of file
+99cf5c91aabfedea4e2475a8382d10036b694c35
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.ios.zip.sha1 b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.ios.zip.sha1
index 1420a69..2bf6b72 100644
--- a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.ios.zip.sha1
+++ b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.ios.zip.sha1
@@ -1 +1 @@
-67da2651d1135db472eb4595984de2a1bebb2e6e
\ No newline at end of file
+4172beb5356cf2a07e977ed624734d0646991269
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.iossimulator.zip.sha1 b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.iossimulator.zip.sha1
index 4ec09a202..058adae1 100644
--- a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.iossimulator.zip.sha1
+++ b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.iossimulator.zip.sha1
@@ -1 +1 @@
-0cc5f2ef9c7e63b5e1050b7b2039c1e2fd74ba5d
\ No newline at end of file
+e1ea32b8932b82110363c2c3aa61ddbcd1e98e9c
\ No newline at end of file
diff --git a/media/base/svc_scalability_mode.h b/media/base/svc_scalability_mode.h
index f96a560..37f8e34 100644
--- a/media/base/svc_scalability_mode.h
+++ b/media/base/svc_scalability_mode.h
@@ -11,41 +11,46 @@
 
 // This enum class is the corresponding implementation with WebRTC-SVC.
 // See https://www.w3.org/TR/webrtc-svc/#scalabilitymodes* for the detail.
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused. Please keep the consistency with
+// VideoEncoderUseCase in tools/metrics/histograms/enums.xml.
 enum class SVCScalabilityMode {
-  kL1T1,
-  kL1T2,
-  kL1T3,
-  kL2T1,
-  kL2T2,
-  kL2T3,
-  kL3T1,
-  kL3T2,
-  kL3T3,
-  kL2T1h,
-  kL2T2h,
-  kL2T3h,
-  kS2T1,
-  kS2T2,
-  kS2T3,
-  kS2T1h,
-  kS2T2h,
-  kS2T3h,
-  kS3T1,
-  kS3T2,
-  kS3T3,
-  kS3T1h,
-  kS3T2h,
-  kS3T3h,
-  kL2T1Key,
-  kL2T2Key,
-  kL2T2KeyShift,
-  kL2T3Key,
-  kL2T3KeyShift,
-  kL3T1Key,
-  kL3T2Key,
-  kL3T2KeyShift,
-  kL3T3Key,
-  kL3T3KeyShift,
+  kL1T1 = 0,
+  kL1T2 = 1,
+  kL1T3 = 2,
+  kL2T1 = 3,
+  kL2T2 = 4,
+  kL2T3 = 5,
+  kL3T1 = 6,
+  kL3T2 = 7,
+  kL3T3 = 8,
+  kL2T1h = 9,
+  kL2T2h = 10,
+  kL2T3h = 11,
+  kS2T1 = 12,
+  kS2T2 = 13,
+  kS2T3 = 14,
+  kS2T1h = 15,
+  kS2T2h = 16,
+  kS2T3h = 17,
+  kS3T1 = 18,
+  kS3T2 = 19,
+  kS3T3 = 20,
+  kS3T1h = 21,
+  kS3T2h = 22,
+  kS3T3h = 23,
+  kL2T1Key = 24,
+  kL2T2Key = 25,
+  kL2T2KeyShift = 26,
+  kL2T3Key = 27,
+  kL2T3KeyShift = 28,
+  kL3T1Key = 29,
+  kL3T2Key = 30,
+  kL3T2KeyShift = 31,
+  kL3T3Key = 32,
+  kL3T3KeyShift = 33,
+
+  kMaxValue = kL3T3KeyShift,
 };
 
 // Gets the WebRTC-SVC Spec defined scalability mode name.
diff --git a/media/capture/video/mac/video_capture_device_avfoundation_mac.h b/media/capture/video/mac/video_capture_device_avfoundation_mac.h
index db968b1..4c1d3d24 100644
--- a/media/capture/video/mac/video_capture_device_avfoundation_mac.h
+++ b/media/capture/video/mac/video_capture_device_avfoundation_mac.h
@@ -54,6 +54,9 @@
 - (void)setFrameReceiver:
     (media::VideoCaptureDeviceAVFoundationFrameReceiver*)frameReceiver;
 
+// Whether to use a GPU memory for video frames or not.
+- (void)setUseGPUMemoryBuffer:(bool)useGPUMemoryBuffer;
+
 // Sets which capture device to use by name, retrieved via |deviceNames|.
 // Method -setCaptureDevice: must be called at least once with a device
 // identifier from GetVideoCaptureDeviceNames(). It creates all the necessary
diff --git a/media/capture/video/mac/video_capture_device_avfoundation_mac.mm b/media/capture/video/mac/video_capture_device_avfoundation_mac.mm
index 4ba1b20..e852f8591 100644
--- a/media/capture/video/mac/video_capture_device_avfoundation_mac.mm
+++ b/media/capture/video/mac/video_capture_device_avfoundation_mac.mm
@@ -159,6 +159,12 @@
   // The following attributes are set via -setCaptureHeight:width:frameRate:.
   float _frameRate;
 
+  // Usage of GPU memory buffer is controlled by
+  // `--disable-video-capture-use-gpu-memory-buffer` and
+  // `--video-capture-use-gpu-memory-buffer` commandline switches. This flag
+  // handles whether to use a GPU memoery for a video frame or not.
+  bool _useGPUMemoryBuffer;
+
   // The capture format that best matches the above attributes.
   AVCaptureDeviceFormat* __strong _bestCaptureFormat;
 
@@ -245,6 +251,7 @@
                               "SampleDeliveryDispatchQueue",
                               DISPATCH_QUEUE_SERIAL);
     DCHECK(frameReceiver);
+    _useGPUMemoryBuffer = true;
     _capturedFirstFrame = false;
     _weakPtrHolderForStallCheck.the_self = self;
     _weakPtrHolderForTakePhoto.the_self = self;
@@ -286,6 +293,10 @@
   _frameReceiver = frameReceiver;
 }
 
+- (void)setUseGPUMemoryBuffer:(bool)useGPUMemoryBuffer {
+  _useGPUMemoryBuffer = useGPUMemoryBuffer;
+}
+
 - (BOOL)setCaptureDevice:(NSString*)deviceId
             errorMessage:(NSString**)outMessage {
   DCHECK(_captureSession);
@@ -1026,7 +1037,7 @@
   // formats to an IOSurface-backed NV12 pixel buffer.
   // TODO(https://crbug.com/1175142): Refactor to not hijack the code paths
   // below the transformer code.
-  if (sampleHasPixelBufferOrIsMjpeg) {
+  if (_useGPUMemoryBuffer && sampleHasPixelBufferOrIsMjpeg) {
     _sampleBufferTransformer->Reconfigure(
         media::SampleBufferTransformer::GetBestTransformerForNv12Output(
             sampleBuffer),
@@ -1088,15 +1099,16 @@
     DCHECK_EQ(pixelBufferPixelFormat, sampleBufferPixelFormat);
 
     // First preference is to use an NV12 IOSurface as a GpuMemoryBuffer.
-    if (CVPixelBufferGetIOSurface(pixelBuffer) &&
-        videoPixelFormat == media::PIXEL_FORMAT_NV12) {
-      [self processPixelBufferNV12IOSurface:pixelBuffer
-                              captureFormat:captureFormat
-                                 colorSpace:colorSpace
-                                  timestamp:timestamp];
-      return;
+    if (_useGPUMemoryBuffer) {
+      if (CVPixelBufferGetIOSurface(pixelBuffer) &&
+          videoPixelFormat == media::PIXEL_FORMAT_NV12) {
+        [self processPixelBufferNV12IOSurface:pixelBuffer
+                                captureFormat:captureFormat
+                                   colorSpace:colorSpace
+                                    timestamp:timestamp];
+        return;
+      }
     }
-
     // Second preference is to read the CVPixelBuffer's planes.
     if ([self processPixelBufferPlanes:pixelBuffer
                          captureFormat:captureFormat
diff --git a/media/capture/video/mac/video_capture_device_mac.mm b/media/capture/video/mac/video_capture_device_mac.mm
index d34e9dc5..223e891 100644
--- a/media/capture/video/mac/video_capture_device_mac.mm
+++ b/media/capture/video/mac/video_capture_device_mac.mm
@@ -164,6 +164,12 @@
 
   [capture_device_ setFrameReceiver:this];
 
+  if (params.buffer_type == VideoCaptureBufferType::kGpuMemoryBuffer) {
+    [capture_device_ setUseGPUMemoryBuffer:true];
+  } else if (params.buffer_type == VideoCaptureBufferType::kSharedMemory) {
+    [capture_device_ setUseGPUMemoryBuffer:false];
+  }
+
   NSString* errorMessage = nil;
   if (![capture_device_ setCaptureDevice:deviceId errorMessage:&errorMessage]) {
     SetErrorState(VideoCaptureError::kMacSetCaptureDeviceFailed, FROM_HERE,
diff --git a/media/gpu/v4l2/test/av1_decoder.cc b/media/gpu/v4l2/test/av1_decoder.cc
index 651b66bf..b2b9c05 100644
--- a/media/gpu/v4l2/test/av1_decoder.cc
+++ b/media/gpu/v4l2/test/av1_decoder.cc
@@ -696,9 +696,6 @@
                               !frm_header.enable_frame_end_update_cdf,
                               V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF);
   conditionally_set_u32_flags(&v4l2_frame_params->flags,
-                              frm_header.tile_info.uniform_spacing,
-                              V4L2_AV1_FRAME_FLAG_UNIFORM_TILE_SPACING);
-  conditionally_set_u32_flags(&v4l2_frame_params->flags,
                               frm_header.allow_warped_motion,
                               V4L2_AV1_FRAME_FLAG_ALLOW_WARPED_MOTION);
   conditionally_set_u32_flags(&v4l2_frame_params->flags,
diff --git a/media/gpu/v4l2/v4l2_device.h b/media/gpu/v4l2/v4l2_device.h
index 53796883..45492ba 100644
--- a/media/gpu/v4l2/v4l2_device.h
+++ b/media/gpu/v4l2/v4l2_device.h
@@ -60,22 +60,6 @@
   v4l2_fourcc('S', '2', '6', '5') /* HEVC parsed slices */
 #endif
 
-#if BUILDFLAG(IS_CHROMEOS)
-#ifndef V4L2_CID_MPEG_VIDEO_AV1_PROFILE
-#define V4L2_CID_MPEG_VIDEO_AV1_PROFILE V4L2_CID_STATELESS_AV1_PROFILE
-#endif
-#ifndef V4L2_MPEG_VIDEO_AV1_PROFILE_MAIN
-#define V4L2_MPEG_VIDEO_AV1_PROFILE_MAIN V4L2_STATELESS_AV1_PROFILE_MAIN
-#endif
-#ifndef V4L2_MPEG_VIDEO_AV1_PROFILE_HIGH
-#define V4L2_MPEG_VIDEO_AV1_PROFILE_HIGH V4L2_STATELESS_AV1_PROFILE_HIGH
-#endif
-#ifndef V4L2_MPEG_VIDEO_AV1_PROFILE_PROFESSIONAL
-#define V4L2_MPEG_VIDEO_AV1_PROFILE_PROFESSIONAL \
-  V4L2_STATELESS_AV1_PROFILE_PROFESSIONAL
-#endif
-#endif
-
 // TODO(b/260863940): Remove this once V4L2 header is updated
 #ifndef V4L2_CID_MPEG_VIDEO_HEVC_PROFILE
 #define V4L2_CID_MPEG_VIDEO_HEVC_PROFILE (V4L2_CID_MPEG_BASE + 615)
diff --git a/media/gpu/v4l2/v4l2_utils.cc b/media/gpu/v4l2/v4l2_utils.cc
index 24604a20..9c784f17 100644
--- a/media/gpu/v4l2/v4l2_utils.cc
+++ b/media/gpu/v4l2/v4l2_utils.cc
@@ -21,22 +21,6 @@
 #include "media/gpu/macros.h"
 #include "ui/gfx/geometry/size.h"
 
-#if BUILDFLAG(IS_CHROMEOS)
-#ifndef V4L2_CID_MPEG_VIDEO_AV1_PROFILE
-#define V4L2_CID_MPEG_VIDEO_AV1_PROFILE V4L2_CID_STATELESS_AV1_PROFILE
-#endif
-#ifndef V4L2_MPEG_VIDEO_AV1_PROFILE_MAIN
-#define V4L2_MPEG_VIDEO_AV1_PROFILE_MAIN V4L2_STATELESS_AV1_PROFILE_MAIN
-#endif
-#ifndef V4L2_MPEG_VIDEO_AV1_PROFILE_HIGH
-#define V4L2_MPEG_VIDEO_AV1_PROFILE_HIGH V4L2_STATELESS_AV1_PROFILE_HIGH
-#endif
-#ifndef V4L2_MPEG_VIDEO_AV1_PROFILE_PROFESSIONAL
-#define V4L2_MPEG_VIDEO_AV1_PROFILE_PROFESSIONAL \
-  V4L2_STATELESS_AV1_PROFILE_PROFESSIONAL
-#endif
-#endif
-
 // TODO(b/255770680): Remove this once V4L2 header is updated.
 // https://patchwork.linuxtv.org/project/linux-media/patch/20210810220552.298140-2-daniel.almeida@collabora.com/
 #ifndef V4L2_PIX_FMT_AV1
diff --git a/media/gpu/v4l2/v4l2_video_decoder_delegate_av1.cc b/media/gpu/v4l2/v4l2_video_decoder_delegate_av1.cc
index fa08e35..fb05a18 100644
--- a/media/gpu/v4l2/v4l2_video_decoder_delegate_av1.cc
+++ b/media/gpu/v4l2/v4l2_video_decoder_delegate_av1.cc
@@ -522,8 +522,6 @@
     v4l2_frame_params.flags |= V4L2_AV1_FRAME_FLAG_USE_REF_FRAME_MVS;
   if (frame_header.enable_frame_end_update_cdf == false)
     v4l2_frame_params.flags |= V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF;
-  if (frame_header.tile_info.uniform_spacing)
-    v4l2_frame_params.flags |= V4L2_AV1_FRAME_FLAG_UNIFORM_TILE_SPACING;
   if (frame_header.allow_warped_motion)
     v4l2_frame_params.flags |= V4L2_AV1_FRAME_FLAG_ALLOW_WARPED_MOTION;
   if (frame_header.reference_mode_select)
diff --git a/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate.cc b/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate.cc
index b909580..6eb61d3 100644
--- a/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate.cc
+++ b/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate.cc
@@ -19,9 +19,6 @@
 #include "media/gpu/vaapi/vaapi_common.h"
 #include "media/gpu/vaapi/vaapi_wrapper.h"
 #include "media/gpu/vp9_svc_layers.h"
-
-// TODO(b/286163500): Some macro in the libvpx headers conflicts with a macro in
-// Chrome headers, so we always need to include this file last.
 #include "third_party/libvpx/source/libvpx/vp9/ratectrl_rtc.h"
 
 namespace media {
diff --git a/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate_unittest.cc b/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate_unittest.cc
index 8507fd5..d8618ec 100644
--- a/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate_unittest.cc
+++ b/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate_unittest.cc
@@ -25,9 +25,6 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/libvpx/source/libvpx/vp9/common/vp9_blockd.h"
-
-// Some macro in the libvpx headers conflicts with a macro in Chrome headers, so
-// we always need to include this file last.
 #include "third_party/libvpx/source/libvpx/vp9/ratectrl_rtc.h"
 
 using ::testing::_;
diff --git a/media/mojo/clients/BUILD.gn b/media/mojo/clients/BUILD.gn
index cbd8e90..b0f140074 100644
--- a/media/mojo/clients/BUILD.gn
+++ b/media/mojo/clients/BUILD.gn
@@ -53,6 +53,8 @@
     "mojo_video_decoder.h",
     "mojo_video_encode_accelerator.cc",
     "mojo_video_encode_accelerator.h",
+    "mojo_video_encoder_metrics_provider.cc",
+    "mojo_video_encoder_metrics_provider.h",
   ]
 
   if (is_android) {
@@ -114,6 +116,7 @@
     "mojo_decryptor_unittest.cc",
     "mojo_renderer_unittest.cc",
     "mojo_video_encode_accelerator_unittest.cc",
+    "mojo_video_encoder_metrics_provider_unittest.cc",
   ]
 
   deps = [
diff --git a/media/mojo/clients/mojo_video_encoder_metrics_provider.cc b/media/mojo/clients/mojo_video_encoder_metrics_provider.cc
new file mode 100644
index 0000000..f315cdf
--- /dev/null
+++ b/media/mojo/clients/mojo_video_encoder_metrics_provider.cc
@@ -0,0 +1,63 @@
+// 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 "media/mojo/clients/mojo_video_encoder_metrics_provider.h"
+
+namespace media {
+
+MojoVideoEncoderMetricsProvider::MojoVideoEncoderMetricsProvider(
+    mojom::VideoEncoderUseCase use_case,
+    mojo::PendingRemote<mojom::VideoEncoderMetricsProvider> pending_remote)
+    : use_case_(use_case), pending_remote_(std::move(pending_remote)) {
+  DETACH_FROM_SEQUENCE(sequence_checker_);
+}
+
+MojoVideoEncoderMetricsProvider::~MojoVideoEncoderMetricsProvider() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+}
+
+void MojoVideoEncoderMetricsProvider::Initialize(
+    VideoCodecProfile codec_profile,
+    const gfx::Size& encode_size,
+    bool is_hardware_encoder,
+    SVCScalabilityMode svc_mode) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (pending_remote_.is_valid()) {
+    remote_.Bind(std::move(pending_remote_));
+  }
+  CHECK(remote_.is_bound());
+
+  remote_->Initialize(use_case_, codec_profile, encode_size,
+                      is_hardware_encoder, svc_mode);
+}
+
+void MojoVideoEncoderMetricsProvider::IncrementEncodedFrameCount() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (pending_remote_.is_valid()) {
+    DLOG(WARNING) << __func__ << "is called before Initialize()";
+    return;
+  }
+  ++num_encoded_frames_;
+  constexpr size_t kEncodedFrameCountBucketSize = 100;
+  // Basically update the number of encoded frames every 100 seconds to avoid
+  // the frequent mojo call. The exception is the first encoded frames update as
+  // it is important to represent whether the encoding actually starts.
+  if (num_encoded_frames_ % kEncodedFrameCountBucketSize == 0 ||
+      num_encoded_frames_ == 1u) {
+    remote_->SetEncodedFrameCount(num_encoded_frames_);
+  }
+}
+
+void MojoVideoEncoderMetricsProvider::SetError(
+    const media::EncoderStatus& status) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (pending_remote_.is_valid()) {
+    DLOG(WARNING) << __func__ << "is called before Initialize()";
+    return;
+  }
+  CHECK(!status.is_ok());
+  remote_->SetError(status);
+}
+
+}  // namespace media
diff --git a/media/mojo/clients/mojo_video_encoder_metrics_provider.h b/media/mojo/clients/mojo_video_encoder_metrics_provider.h
new file mode 100644
index 0000000..f0b215d
--- /dev/null
+++ b/media/mojo/clients/mojo_video_encoder_metrics_provider.h
@@ -0,0 +1,60 @@
+// 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.
+
+#ifndef MEDIA_MOJO_CLIENTS_MOJO_VIDEO_ENCODER_METRICS_PROVIDER_H_
+#define MEDIA_MOJO_CLIENTS_MOJO_VIDEO_ENCODER_METRICS_PROVIDER_H_
+
+#include "base/sequence_checker.h"
+#include "base/thread_annotations.h"
+#include "media/base/encoder_status.h"
+#include "media/base/svc_scalability_mode.h"
+#include "media/mojo/mojom/video_encoder_metrics_provider.mojom.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace media {
+class MojoVideoEncoderMetricsProvider {
+ public:
+  // MojoVideoEncoderMetricsProvider ctro can be called on any sequence.
+  MojoVideoEncoderMetricsProvider(
+      mojom::VideoEncoderUseCase use_case,
+      mojo::PendingRemote<mojom::VideoEncoderMetricsProvider> pending_remote);
+  virtual ~MojoVideoEncoderMetricsProvider();
+
+  MojoVideoEncoderMetricsProvider(const MojoVideoEncoderMetricsProvider&) =
+      delete;
+  MojoVideoEncoderMetricsProvider& operator=(
+      const MojoVideoEncoderMetricsProvider&) = delete;
+  MojoVideoEncoderMetricsProvider(MojoVideoEncoderMetricsProvider&&) = delete;
+  MojoVideoEncoderMetricsProvider& operator=(
+      MojoVideoEncoderMetricsProvider&&) = delete;
+
+  // All of the function must be called on the same sequence.
+  void Initialize(VideoCodecProfile codec_profile,
+                  const gfx::Size& encode_size,
+                  bool is_hardware_encoder) {
+    Initialize(codec_profile, encode_size, is_hardware_encoder,
+               SVCScalabilityMode::kL1T1);
+  }
+  virtual void Initialize(VideoCodecProfile codec_profile,
+                          const gfx::Size& encode_size,
+                          bool is_hardware_encoder,
+                          SVCScalabilityMode svc_mode);
+  virtual void IncrementEncodedFrameCount();
+  virtual void SetError(const media::EncoderStatus& status);
+
+ protected:
+  // |sequence_checker_| is used in MockMojoVideoEncoderMetricsProvider.
+  SEQUENCE_CHECKER(sequence_checker_);
+
+ private:
+  const mojom::VideoEncoderUseCase use_case_;
+  mojo::PendingRemote<mojom::VideoEncoderMetricsProvider> pending_remote_;
+  mojo::Remote<mojom::VideoEncoderMetricsProvider> remote_
+      GUARDED_BY_CONTEXT(sequence_checker_);
+  uint64_t num_encoded_frames_ GUARDED_BY_CONTEXT(sequence_checker_){0};
+};
+}  // namespace media
+#endif  // MEDIA_MOJO_CLIENTS_MOJO_VIDEO_ENCODER_METRICS_PROVIDER_H_
diff --git a/media/mojo/clients/mojo_video_encoder_metrics_provider_unittest.cc b/media/mojo/clients/mojo_video_encoder_metrics_provider_unittest.cc
new file mode 100644
index 0000000..3290a79
--- /dev/null
+++ b/media/mojo/clients/mojo_video_encoder_metrics_provider_unittest.cc
@@ -0,0 +1,166 @@
+// 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 "media/mojo/clients/mojo_video_encoder_metrics_provider.h"
+
+#include "base/run_loop.h"
+#include "base/test/task_environment.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/bindings/self_owned_receiver.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::InSequence;
+
+namespace media {
+
+class MockMojomVideoEncoderMetricsProvider
+    : public mojom::VideoEncoderMetricsProvider {
+ public:
+  MockMojomVideoEncoderMetricsProvider() = default;
+  ~MockMojomVideoEncoderMetricsProvider() override = default;
+  // mojom::VideoEncoderMetricsProvider implementation.
+  MOCK_METHOD(void,
+              Initialize,
+              (mojom::VideoEncoderUseCase,
+               VideoCodecProfile,
+               const gfx::Size&,
+               bool,
+               SVCScalabilityMode),
+              (override));
+  MOCK_METHOD(void, SetEncodedFrameCount, (uint64_t), (override));
+  MOCK_METHOD(void, SetError, (const media::EncoderStatus&), (override));
+};
+
+class MojoVideoEncoderMetricsProviderTest : public ::testing::Test {
+ public:
+  MojoVideoEncoderMetricsProviderTest() = default;
+
+  static constexpr auto kUseCase = mojom::VideoEncoderUseCase::kMediaRecorder;
+  void SetUp() override {
+    mojo::PendingRemote<mojom::VideoEncoderMetricsProvider> pending_remote;
+    mojo_encoder_metrics_receiver_ = mojo::MakeSelfOwnedReceiver(
+        std::make_unique<MockMojomVideoEncoderMetricsProvider>(),
+        pending_remote.InitWithNewPipeAndPassReceiver());
+
+    mojo_encoder_metrics_provider_ =
+        std::make_unique<MojoVideoEncoderMetricsProvider>(
+            kUseCase, std::move(pending_remote));
+  }
+  void TearDown() override {
+    // The destruction of a mojo::SelfOwnedReceiver closes the bound message
+    // pipe but does not destroy the implementation object(s): this needs to
+    // happen manually by Close()ing it.
+    if (mojo_encoder_metrics_receiver_) {
+      mojo_encoder_metrics_receiver_->Close();
+    }
+  }
+
+  MockMojomVideoEncoderMetricsProvider* mock_mojo_receiver() {
+    return reinterpret_cast<MockMojomVideoEncoderMetricsProvider*>(
+        mojo_encoder_metrics_receiver_->impl());
+  }
+
+ protected:
+  base::test::SingleThreadTaskEnvironment task_environment_;
+  // This member holds on to the mock implementation of the "service" side.
+  mojo::SelfOwnedReceiverRef<mojom::VideoEncoderMetricsProvider>
+      mojo_encoder_metrics_receiver_;
+
+  std::unique_ptr<MojoVideoEncoderMetricsProvider>
+      mojo_encoder_metrics_provider_;
+};
+
+TEST_F(MojoVideoEncoderMetricsProviderTest, CreateAndDestroy) {}
+
+TEST_F(MojoVideoEncoderMetricsProviderTest, CreateAndBoundAndInitialize) {
+  constexpr auto kCodecProfile = media::VP9PROFILE_PROFILE0;
+  constexpr gfx::Size kEncodeSize(1920, 1080);
+  constexpr bool kIsHardwareEncoder = true;
+  constexpr auto kSVCMode = SVCScalabilityMode::kL1T3;
+
+  InSequence s;
+  EXPECT_CALL(*mock_mojo_receiver(),
+              Initialize(kUseCase, kCodecProfile, kEncodeSize,
+                         kIsHardwareEncoder, kSVCMode));
+  mojo_encoder_metrics_provider_->Initialize(kCodecProfile, kEncodeSize,
+                                             kIsHardwareEncoder, kSVCMode);
+  base::RunLoop().RunUntilIdle();
+}
+
+TEST_F(MojoVideoEncoderMetricsProviderTest,
+       CreateAndBoundAndInitializeSetEncodedFrameCount) {
+  constexpr auto kCodecProfile = media::VP9PROFILE_PROFILE0;
+  constexpr gfx::Size kEncodeSize(1920, 1080);
+  constexpr bool kIsHardwareEncoder = true;
+  constexpr auto kSVCMode = SVCScalabilityMode::kL1T3;
+
+  InSequence s;
+  EXPECT_CALL(*mock_mojo_receiver(),
+              Initialize(kUseCase, kCodecProfile, kEncodeSize,
+                         kIsHardwareEncoder, kSVCMode));
+  mojo_encoder_metrics_provider_->Initialize(kCodecProfile, kEncodeSize,
+                                             kIsHardwareEncoder, kSVCMode);
+  base::RunLoop().RunUntilIdle();
+  EXPECT_CALL(*mock_mojo_receiver(), SetEncodedFrameCount(1u));
+  mojo_encoder_metrics_provider_->IncrementEncodedFrameCount();
+  base::RunLoop().RunUntilIdle();
+  for (size_t i = 0; i < 98; i++) {
+    mojo_encoder_metrics_provider_->IncrementEncodedFrameCount();
+  }
+  base::RunLoop().RunUntilIdle();
+  EXPECT_CALL(*mock_mojo_receiver(), SetEncodedFrameCount(100u));
+  mojo_encoder_metrics_provider_->IncrementEncodedFrameCount();
+  base::RunLoop().RunUntilIdle();
+  for (size_t i = 0; i < 99; i++) {
+    mojo_encoder_metrics_provider_->IncrementEncodedFrameCount();
+  }
+  base::RunLoop().RunUntilIdle();
+  EXPECT_CALL(*mock_mojo_receiver(), SetEncodedFrameCount(200u));
+  mojo_encoder_metrics_provider_->IncrementEncodedFrameCount();
+  base::RunLoop().RunUntilIdle();
+}
+
+TEST_F(MojoVideoEncoderMetricsProviderTest,
+       CreateAndBoundAndInitializeAndSetError) {
+  constexpr auto kCodecProfile = media::VP9PROFILE_PROFILE0;
+  constexpr gfx::Size kEncodeSize(1920, 1080);
+  constexpr bool kIsHardwareEncoder = true;
+  constexpr auto kSVCMode = SVCScalabilityMode::kL1T3;
+
+  InSequence s;
+  EXPECT_CALL(*mock_mojo_receiver(),
+              Initialize(kUseCase, kCodecProfile, kEncodeSize,
+                         kIsHardwareEncoder, kSVCMode));
+  mojo_encoder_metrics_provider_->Initialize(kCodecProfile, kEncodeSize,
+                                             kIsHardwareEncoder, kSVCMode);
+  base::RunLoop().RunUntilIdle();
+  EXPECT_CALL(*mock_mojo_receiver(), SetEncodedFrameCount(1u));
+  mojo_encoder_metrics_provider_->IncrementEncodedFrameCount();
+  mojo_encoder_metrics_provider_->IncrementEncodedFrameCount();
+  mojo_encoder_metrics_provider_->IncrementEncodedFrameCount();
+  base::RunLoop().RunUntilIdle();
+  const media::EncoderStatus kErrorStatus(
+      media::EncoderStatus::Codes::kEncoderFailedEncode, "Encoder failed");
+  mojo_encoder_metrics_provider_->SetError(kErrorStatus);
+  EXPECT_CALL(*mock_mojo_receiver(), SetError(kErrorStatus));
+  base::RunLoop().RunUntilIdle();
+}
+
+TEST_F(MojoVideoEncoderMetricsProviderTest, CreateAndSetError_NoCall) {
+  InSequence s;
+  const media::EncoderStatus kErrorStatus(
+      media::EncoderStatus::Codes::kEncoderFailedEncode, "Encoder failed");
+  mojo_encoder_metrics_provider_->SetError(kErrorStatus);
+  base::RunLoop().RunUntilIdle();
+}
+
+TEST_F(MojoVideoEncoderMetricsProviderTest,
+       CreateAndIncrementEncodedFrameCount_NoCall) {
+  InSequence s;
+  mojo_encoder_metrics_provider_->IncrementEncodedFrameCount();
+  base::RunLoop().RunUntilIdle();
+}
+}  // namespace media
diff --git a/media/mojo/mojom/BUILD.gn b/media/mojo/mojom/BUILD.gn
index 238c0a1..d8d0c373 100644
--- a/media/mojo/mojom/BUILD.gn
+++ b/media/mojo/mojom/BUILD.gn
@@ -46,6 +46,7 @@
     "video_decoder.mojom",
     "video_encode_accelerator.mojom",
     "video_encoder_info.mojom",
+    "video_encoder_metrics_provider.mojom",
     "watch_time_recorder.mojom",
     "webrtc_video_perf.mojom",
   ]
diff --git a/media/mojo/mojom/video_encoder_metrics_provider.mojom b/media/mojo/mojom/video_encoder_metrics_provider.mojom
new file mode 100644
index 0000000..403cd79c
--- /dev/null
+++ b/media/mojo/mojom/video_encoder_metrics_provider.mojom
@@ -0,0 +1,38 @@
+// 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.
+
+module media.mojom;
+
+import "ui/gfx/geometry/mojom/geometry.mojom";
+import "media/mojo/mojom/media_types.mojom";
+
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused. Please keep the consistency with
+// VideoEncoderUseCase in tools/metrics/histograms/enums.xml.
+enum VideoEncoderUseCase {
+  kCastMirroring = 0,
+  kMediaRecorder = 1,
+  kWebCodecs = 2,
+  kWebRTC = 3,
+};
+
+// Provider interface to collect video encoder metrics.
+interface VideoEncoderMetricsProvider {
+  // Initialize() is called whenever the encoder configuration is changed.
+  // The UKM about the encoding represented by the previous Initialize() is
+  // reported if SetEncodedFrameCount() or SetError() is invoked. See
+  // Media.VideoEncoderMetrics in ukm.xml for the detail about the recorded UKM.
+  Initialize(VideoEncoderUseCase encoder_use_case, VideoCodecProfile profile,
+             gfx.mojom.Size encode_size, bool is_hardware_encoder,
+             SVCScalabilityMode svc_mode);
+
+  // SetEncodedFramesCount() updates the number of successfully encoded frames.
+  // |num_encoded_frames| can be any value, but it is bucket by 100 when UKM
+  // is recorded.
+  SetEncodedFrameCount(uint64 num_encoded_frames);
+
+  // SetError() should be called when the encoder becomes in the error state. In
+  // other words, |status| must not be kOk.
+  SetError(EncoderStatus status);
+};
diff --git a/media/mojo/services/BUILD.gn b/media/mojo/services/BUILD.gn
index b7e84ee..b9c858d 100644
--- a/media/mojo/services/BUILD.gn
+++ b/media/mojo/services/BUILD.gn
@@ -65,6 +65,8 @@
     "video_decode_perf_history.h",
     "video_decode_stats_recorder.cc",
     "video_decode_stats_recorder.h",
+    "video_encoder_metrics_provider.cc",
+    "video_encoder_metrics_provider.h",
     "watch_time_recorder.cc",
     "watch_time_recorder.h",
     "webrtc_video_perf_history.cc",
@@ -245,6 +247,7 @@
     "test_helpers.h",
     "video_decode_perf_history_unittest.cc",
     "video_decode_stats_recorder_unittest.cc",
+    "video_encoder_metrics_provider_unittest.cc",
     "watch_time_recorder_unittest.cc",
     "webrtc_video_perf_history_unittest.cc",
     "webrtc_video_perf_recorder_unittest.cc",
diff --git a/media/mojo/services/video_encoder_metrics_provider.cc b/media/mojo/services/video_encoder_metrics_provider.cc
new file mode 100644
index 0000000..ecddb73
--- /dev/null
+++ b/media/mojo/services/video_encoder_metrics_provider.cc
@@ -0,0 +1,99 @@
+// 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 "media/mojo/services/video_encoder_metrics_provider.h"
+
+#include <algorithm>
+
+#include "base/memory/ptr_util.h"
+#include "mojo/public/cpp/bindings/self_owned_receiver.h"
+#include "services/metrics/public/cpp/ukm_builders.h"
+#include "services/metrics/public/cpp/ukm_recorder.h"
+
+namespace media {
+
+// static
+void VideoEncoderMetricsProvider::Create(
+    ukm::SourceId source_id,
+    mojo::PendingReceiver<mojom::VideoEncoderMetricsProvider> receiver) {
+  mojo::MakeSelfOwnedReceiver(
+      base::WrapUnique(new VideoEncoderMetricsProvider(source_id)),
+      std::move(receiver));
+}
+
+VideoEncoderMetricsProvider::VideoEncoderMetricsProvider(
+    ukm::SourceId source_id)
+    : source_id_(source_id) {}
+
+VideoEncoderMetricsProvider::~VideoEncoderMetricsProvider() {
+  ReportUKMIfNeeded();
+}
+
+void VideoEncoderMetricsProvider::ReportUKMIfNeeded() const {
+  // If Initialize() is not called, no UKM is reported.
+  if (!initialized_) {
+    return;
+  }
+  ukm::UkmRecorder* ukm_recorder = ukm::UkmRecorder::Get();
+  if (!ukm_recorder) {
+    return;
+  }
+  ukm::builders::Media_VideoEncoderMetrics builder(source_id_);
+  builder.SetUseCase(static_cast<int>(encoder_use_case_));
+  builder.SetProfile(static_cast<int>(codec_profile_));
+  builder.SetSVCMode(static_cast<int>(svc_mode_));
+  builder.SetIsHardware(is_hardware_encoder_);
+  constexpr int kMaxResolutionBucket = 8200;
+  builder.SetWidth(
+      std::min(encode_size_.width() / 100 * 100, kMaxResolutionBucket));
+  builder.SetHeight(
+      std::min(encode_size_.height() / 100 * 100, kMaxResolutionBucket));
+  builder.SetStatus(static_cast<int>(encoder_status_.code()));
+
+  // We report UKM even if |num_encoded_frames_| is 0 so that we know how
+  // Initialize() is called. However, since the number of encoded frame is
+  // bucketed per 100, it disables to distinguish Initialize()-only case and
+  // the case of encoding a few frames. Therefore, we set the number of encoded
+  // frames to 1 if |num_encoded_frames_| is between 1 and 99.
+  if (num_encoded_frames_ == 0) {
+    builder.SetNumEncodedFrames(0);
+  } else {
+    builder.SetNumEncodedFrames(
+        std::max<uint64_t>(1, num_encoded_frames_ / 100 * 100));
+  }
+  builder.Record(ukm_recorder);
+
+  // TODO(b/275663480): Report UMAs.
+}
+
+void VideoEncoderMetricsProvider::Initialize(
+    mojom::VideoEncoderUseCase encoder_use_case,
+    VideoCodecProfile codec_profile,
+    const gfx::Size& encode_size,
+    bool is_hardware_encoder,
+    SVCScalabilityMode svc_mode) {
+  ReportUKMIfNeeded();
+  initialized_ = true;
+  num_encoded_frames_ = 0;
+  encoder_use_case_ = encoder_use_case;
+  codec_profile_ = codec_profile;
+  encode_size_ = encode_size;
+  is_hardware_encoder_ = is_hardware_encoder;
+  encoder_status_ = EncoderStatus::Codes::kOk;
+  svc_mode_ = svc_mode;
+}
+
+void VideoEncoderMetricsProvider::SetEncodedFrameCount(
+    uint64_t num_encoded_frames) {
+  num_encoded_frames_ = num_encoded_frames;
+}
+
+void VideoEncoderMetricsProvider::SetError(const EncoderStatus& status) {
+  CHECK(!status.is_ok());
+  if (encoder_status_.is_ok()) {
+    encoder_status_ = status;
+  }
+}
+
+}  // namespace media
diff --git a/media/mojo/services/video_encoder_metrics_provider.h b/media/mojo/services/video_encoder_metrics_provider.h
new file mode 100644
index 0000000..af1e1ed0
--- /dev/null
+++ b/media/mojo/services/video_encoder_metrics_provider.h
@@ -0,0 +1,54 @@
+// 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.
+
+#ifndef MEDIA_MOJO_SERVICES_VIDEO_ENCODER_METRICS_PROVIDER_H_
+#define MEDIA_MOJO_SERVICES_VIDEO_ENCODER_METRICS_PROVIDER_H_
+
+#include "media/base/encoder_status.h"
+#include "media/base/svc_scalability_mode.h"
+#include "media/base/video_codecs.h"
+#include "media/mojo/mojom/video_encoder_metrics_provider.mojom.h"
+#include "media/mojo/services/media_mojo_export.h"
+#include "services/metrics/public/cpp/ukm_source_id.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace media {
+
+// See mojom::VideoEncoderMetricsProvider for documentation.
+class MEDIA_MOJO_EXPORT VideoEncoderMetricsProvider
+    : public mojom::VideoEncoderMetricsProvider {
+ public:
+  static void Create(
+      ukm::SourceId source_id,
+      mojo::PendingReceiver<mojom::VideoEncoderMetricsProvider> receiver);
+
+  ~VideoEncoderMetricsProvider() override;
+
+  // mojom::VideoEncoderMetricsProvider implementation.
+  void Initialize(mojom::VideoEncoderUseCase encoder_use_case,
+                  VideoCodecProfile codec_profile,
+                  const gfx::Size& encode_size,
+                  bool is_hardware_encoder,
+                  SVCScalabilityMode svc_mode) override;
+  void SetEncodedFrameCount(uint64_t num_encoded_frames) override;
+  void SetError(const EncoderStatus& status) override;
+
+ private:
+  explicit VideoEncoderMetricsProvider(ukm::SourceId source_id);
+
+  void ReportUKMIfNeeded() const;
+
+  const ukm::SourceId source_id_;
+  bool initialized_ = false;
+  uint64_t num_encoded_frames_ = 0;
+
+  mojom::VideoEncoderUseCase encoder_use_case_;
+  VideoCodecProfile codec_profile_;
+  gfx::Size encode_size_;
+  bool is_hardware_encoder_;
+  SVCScalabilityMode svc_mode_;
+  EncoderStatus encoder_status_;
+};
+}  // namespace media
+#endif  // MEDIA_MOJO_SERVICES_VIDEO_ENCODER_METRICS_PROVIDER_H_
diff --git a/media/mojo/services/video_encoder_metrics_provider_unittest.cc b/media/mojo/services/video_encoder_metrics_provider_unittest.cc
new file mode 100644
index 0000000..647ab3b
--- /dev/null
+++ b/media/mojo/services/video_encoder_metrics_provider_unittest.cc
@@ -0,0 +1,405 @@
+// 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 <stddef.h>
+#include <memory>
+
+#include "media/mojo/services/video_encoder_metrics_provider.h"
+
+#include "base/run_loop.h"
+#include "base/test/test_message_loop.h"
+#include "components/ukm/test_ukm_recorder.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/metrics/public/cpp/ukm_builders.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+using UkmEntry = ukm::builders::Media_VideoEncoderMetrics;
+
+using ::testing::TestWithParam;
+using ::testing::ValuesIn;
+
+namespace media {
+namespace {
+constexpr char kTestURL[] = "https://test.google.com/";
+
+std::tuple<std::unique_ptr<ukm::TestAutoSetUkmRecorder>,
+           mojo::Remote<mojom::VideoEncoderMetricsProvider>>
+Create(const std::string& url) {
+  auto test_recorder = std::make_unique<ukm::TestAutoSetUkmRecorder>();
+  ukm::SourceId source_id = test_recorder->GetNewSourceID();
+  test_recorder->UpdateSourceURL(source_id, GURL(url));
+  mojo::Remote<mojom::VideoEncoderMetricsProvider> provider;
+  VideoEncoderMetricsProvider::Create(source_id,
+                                      provider.BindNewPipeAndPassReceiver());
+  return {std::move(test_recorder), std::move(provider)};
+}
+}  // namespace
+
+class VideoEncoderMetricsProviderTest
+    : public TestWithParam<testing::tuple<mojom::VideoEncoderUseCase,
+                                          VideoCodecProfile,
+                                          gfx::Size,
+                                          bool,
+                                          SVCScalabilityMode>> {
+ public:
+  VideoEncoderMetricsProviderTest() = default;
+  ~VideoEncoderMetricsProviderTest() override = default;
+
+ protected:
+  base::TestMessageLoop message_loop_;
+};
+
+TEST_F(VideoEncoderMetricsProviderTest, Create_NoUKMReport) {
+  auto [test_recorder, provider] = Create(kTestURL);
+  provider.reset();
+  base::RunLoop().RunUntilIdle();
+  const auto entries = test_recorder->GetEntriesByName(UkmEntry::kEntryName);
+  EXPECT_TRUE(entries.empty());
+}
+
+#define EXPECT_UKM(name, value) \
+  test_recorder->ExpectEntryMetric(entry, name, value)
+
+TEST_F(VideoEncoderMetricsProviderTest, CreateAndInitialize_ReportUKM) {
+  auto [test_recorder, provider] = Create(kTestURL);
+  constexpr auto kEncoderUseCase = mojom::VideoEncoderUseCase::kWebRTC;
+  constexpr auto kCodecProfile = VP9PROFILE_PROFILE0;
+  constexpr gfx::Size kEncodeSize(1200, 700);
+  constexpr bool kIsHardwareEncoder = true;
+  constexpr auto kSVCMode = SVCScalabilityMode::kL1T3;
+  provider->Initialize(kEncoderUseCase, kCodecProfile, kEncodeSize,
+                       kIsHardwareEncoder, kSVCMode);
+  provider.reset();
+  base::RunLoop().RunUntilIdle();
+
+  const auto entries = test_recorder->GetEntriesByName(UkmEntry::kEntryName);
+  ASSERT_EQ(1u, entries.size());
+  const auto* entry = entries[0];
+  EXPECT_UKM(UkmEntry::kHeightName, kEncodeSize.height());
+  EXPECT_UKM(UkmEntry::kIsHardwareName, kIsHardwareEncoder);
+  EXPECT_UKM(UkmEntry::kNumEncodedFramesName, 0);
+  EXPECT_UKM(UkmEntry::kProfileName, kCodecProfile);
+  EXPECT_UKM(UkmEntry::kStatusName,
+             static_cast<int64_t>(EncoderStatus::Codes::kOk));
+  EXPECT_UKM(UkmEntry::kSVCModeName, static_cast<int64_t>(kSVCMode));
+  EXPECT_UKM(UkmEntry::kUseCaseName, static_cast<int64_t>(kEncoderUseCase));
+  EXPECT_UKM(UkmEntry::kWidthName, kEncodeSize.width());
+}
+
+TEST_F(
+    VideoEncoderMetricsProviderTest,
+    CreateAndInitializeAndSetSmallNumberEncodedFrameCount_ReportUKMWithOneBucket) {
+  auto [test_recorder, provider] = Create(kTestURL);
+  constexpr auto kEncoderUseCase = mojom::VideoEncoderUseCase::kWebRTC;
+  constexpr auto kCodecProfile = VP9PROFILE_PROFILE0;
+  constexpr gfx::Size kEncodeSize(1200, 700);
+  constexpr bool kIsHardwareEncoder = true;
+  constexpr auto kSVCMode = SVCScalabilityMode::kL1T3;
+  provider->Initialize(kEncoderUseCase, kCodecProfile, kEncodeSize,
+                       kIsHardwareEncoder, kSVCMode);
+  provider->SetEncodedFrameCount(10);
+  provider.reset();
+  base::RunLoop().RunUntilIdle();
+
+  const auto entries = test_recorder->GetEntriesByName(UkmEntry::kEntryName);
+  ASSERT_EQ(1u, entries.size());
+  const auto* entry = entries[0];
+  EXPECT_UKM(UkmEntry::kHeightName, kEncodeSize.height());
+  EXPECT_UKM(UkmEntry::kIsHardwareName, kIsHardwareEncoder);
+  EXPECT_UKM(UkmEntry::kNumEncodedFramesName, 1u);
+  EXPECT_UKM(UkmEntry::kProfileName, kCodecProfile);
+  EXPECT_UKM(UkmEntry::kStatusName,
+             static_cast<int64_t>(EncoderStatus::Codes::kOk));
+  EXPECT_UKM(UkmEntry::kSVCModeName, static_cast<int64_t>(kSVCMode));
+  EXPECT_UKM(UkmEntry::kUseCaseName, static_cast<int64_t>(kEncoderUseCase));
+  EXPECT_UKM(UkmEntry::kWidthName, kEncodeSize.width());
+}
+
+TEST_P(VideoEncoderMetricsProviderTest,
+       CreateAndInitializeAndSetEncodedFrameCount_ReportUKM) {
+  auto [test_recorder, provider] = Create(kTestURL);
+  auto encoder_use_case = std::get<0>(GetParam());
+  auto codec_profile = std::get<1>(GetParam());
+  auto encode_size = std::get<2>(GetParam());
+  auto is_hardware_encoder = std::get<3>(GetParam());
+  auto svc_mode = std::get<4>(GetParam());
+  constexpr uint64_t kNumEncodedFrames = 100;
+  provider->Initialize(encoder_use_case, codec_profile, encode_size,
+                       is_hardware_encoder, svc_mode);
+  provider->SetEncodedFrameCount(kNumEncodedFrames);
+  provider.reset();
+  base::RunLoop().RunUntilIdle();
+
+  const auto entries = test_recorder->GetEntriesByName(UkmEntry::kEntryName);
+  ASSERT_EQ(1u, entries.size());
+  const auto* entry = entries[0];
+  const uint64_t expected_height = encode_size.height() / 100 * 100;
+  const uint64_t expected_width = encode_size.width() / 100 * 100;
+  EXPECT_UKM(UkmEntry::kHeightName, expected_height);
+  EXPECT_UKM(UkmEntry::kIsHardwareName, is_hardware_encoder);
+  EXPECT_UKM(UkmEntry::kNumEncodedFramesName, kNumEncodedFrames);
+  EXPECT_UKM(UkmEntry::kProfileName, codec_profile);
+  EXPECT_UKM(UkmEntry::kStatusName,
+             static_cast<int64_t>(EncoderStatus::Codes::kOk));
+  EXPECT_UKM(UkmEntry::kSVCModeName, static_cast<int64_t>(svc_mode));
+  EXPECT_UKM(UkmEntry::kUseCaseName, static_cast<int64_t>(encoder_use_case));
+  EXPECT_UKM(UkmEntry::kWidthName, expected_width);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    VideoEncoderMetricsProviderTest,
+    ::testing::Combine(ValuesIn({
+                           mojom::VideoEncoderUseCase::kCastMirroring,
+                           mojom::VideoEncoderUseCase::kMediaRecorder,
+                           mojom::VideoEncoderUseCase::kWebCodecs,
+                           mojom::VideoEncoderUseCase::kWebRTC,
+                       }),
+                       ValuesIn({
+                           H264PROFILE_MAIN,
+                           VP8PROFILE_ANY,
+                           VP9PROFILE_MIN,
+                           AV1PROFILE_PROFILE_HIGH,
+                       }),
+                       ValuesIn({
+                           gfx::Size(640, 360),
+                           gfx::Size(1280, 720),
+                       }),
+                       ::testing::Bool(),
+                       ValuesIn({
+                           SVCScalabilityMode::kL1T1,
+                           SVCScalabilityMode::kL3T3Key,
+                       })));
+
+TEST_F(VideoEncoderMetricsProviderTest,
+       InitializeWithVerfiyLargeResoloution_ReportCappedResolutionUKM) {
+  auto [test_recorder, provider] = Create(kTestURL);
+  constexpr auto kEncoderUseCase = mojom::VideoEncoderUseCase::kWebRTC;
+  constexpr auto kCodecProfile = VP9PROFILE_PROFILE0;
+  constexpr gfx::Size k16kEncodeSize(15360, 8640);
+  constexpr bool kIsHardwareEncoder = true;
+  constexpr auto kSVCMode = SVCScalabilityMode::kL1T3;
+  constexpr uint64_t kNumEncodedFrames = 100;
+  provider->Initialize(kEncoderUseCase, kCodecProfile, k16kEncodeSize,
+                       kIsHardwareEncoder, kSVCMode);
+  provider->SetEncodedFrameCount(kNumEncodedFrames);
+  provider.reset();
+  base::RunLoop().RunUntilIdle();
+
+  const auto entries = test_recorder->GetEntriesByName(UkmEntry::kEntryName);
+  ASSERT_EQ(1u, entries.size());
+  const auto* entry = entries[0];
+  constexpr uint64_t kHeight = 8200;
+  constexpr uint64_t kWidth = 8200;
+  EXPECT_UKM(UkmEntry::kHeightName, kHeight);
+  EXPECT_UKM(UkmEntry::kIsHardwareName, kIsHardwareEncoder);
+  EXPECT_UKM(UkmEntry::kNumEncodedFramesName, kNumEncodedFrames);
+  EXPECT_UKM(UkmEntry::kProfileName, kCodecProfile);
+  EXPECT_UKM(UkmEntry::kStatusName,
+             static_cast<int64_t>(EncoderStatus::Codes::kOk));
+  EXPECT_UKM(UkmEntry::kSVCModeName, static_cast<int64_t>(kSVCMode));
+  EXPECT_UKM(UkmEntry::kUseCaseName, static_cast<int64_t>(kEncoderUseCase));
+  EXPECT_UKM(UkmEntry::kWidthName, kWidth);
+}
+
+TEST_F(VideoEncoderMetricsProviderTest,
+       CallSetEncodedFrameCounts_ReportUKMWithTheLastEncodedFrameCount) {
+  auto [test_recorder, provider] = Create(kTestURL);
+  constexpr auto kEncoderUseCase = mojom::VideoEncoderUseCase::kWebRTC;
+  constexpr auto kCodecProfile = VP9PROFILE_PROFILE0;
+  constexpr gfx::Size kEncodeSize(1920, 1080);
+  constexpr bool kIsHardwareEncoder = true;
+  constexpr auto kSVCMode = SVCScalabilityMode::kL1T3;
+  provider->Initialize(kEncoderUseCase, kCodecProfile, kEncodeSize,
+                       kIsHardwareEncoder, kSVCMode);
+  provider->SetEncodedFrameCount(100);
+  provider->SetEncodedFrameCount(200);
+  provider->SetEncodedFrameCount(300);
+  provider.reset();
+  base::RunLoop().RunUntilIdle();
+
+  const auto entries = test_recorder->GetEntriesByName(UkmEntry::kEntryName);
+  ASSERT_EQ(1u, entries.size());
+  const auto* entry = entries[0];
+  constexpr uint64_t kHeight = kEncodeSize.height() / 100 * 100;
+  constexpr uint64_t kWidth = kEncodeSize.width() / 100 * 100;
+  EXPECT_UKM(UkmEntry::kHeightName, kHeight);
+  EXPECT_UKM(UkmEntry::kIsHardwareName, kIsHardwareEncoder);
+  EXPECT_UKM(UkmEntry::kNumEncodedFramesName, 300);
+  EXPECT_UKM(UkmEntry::kProfileName, kCodecProfile);
+  EXPECT_UKM(UkmEntry::kStatusName,
+             static_cast<int64_t>(EncoderStatus::Codes::kOk));
+  EXPECT_UKM(UkmEntry::kSVCModeName, static_cast<int64_t>(kSVCMode));
+  EXPECT_UKM(UkmEntry::kUseCaseName, static_cast<int64_t>(kEncoderUseCase));
+  EXPECT_UKM(UkmEntry::kWidthName, kWidth);
+}
+
+TEST_F(VideoEncoderMetricsProviderTest,
+       CreateAndInitializeAndCallSetErrors_ReportUKMWithTheFirstError) {
+  auto [test_recorder, provider] = Create(kTestURL);
+  constexpr auto kEncoderUseCase = mojom::VideoEncoderUseCase::kWebRTC;
+  constexpr auto kCodecProfile = VP9PROFILE_PROFILE0;
+  constexpr gfx::Size kEncodeSize(1920, 1080);
+  constexpr bool kIsHardwareEncoder = true;
+  constexpr auto kSVCMode = SVCScalabilityMode::kL1T3;
+  provider->Initialize(kEncoderUseCase, kCodecProfile, kEncodeSize,
+                       kIsHardwareEncoder, kSVCMode);
+  provider->SetError({EncoderStatus::Codes::kEncoderMojoConnectionError,
+                      "mojo connection is disclosed"});
+  provider->SetError(
+      {EncoderStatus::Codes::kEncoderFailedEncode, "Encoder failed"});
+  provider.reset();
+  base::RunLoop().RunUntilIdle();
+
+  const auto entries = test_recorder->GetEntriesByName(UkmEntry::kEntryName);
+  ASSERT_EQ(1u, entries.size());
+  const auto* entry = entries[0];
+  constexpr uint64_t kHeight = kEncodeSize.height() / 100 * 100;
+  constexpr uint64_t kWidth = kEncodeSize.width() / 100 * 100;
+  EXPECT_UKM(UkmEntry::kHeightName, kHeight);
+  EXPECT_UKM(UkmEntry::kIsHardwareName, kIsHardwareEncoder);
+  EXPECT_UKM(UkmEntry::kNumEncodedFramesName, 0u);
+  EXPECT_UKM(UkmEntry::kProfileName, kCodecProfile);
+  EXPECT_UKM(
+      UkmEntry::kStatusName,
+      static_cast<int64_t>(EncoderStatus::Codes::kEncoderMojoConnectionError));
+  EXPECT_UKM(UkmEntry::kSVCModeName, static_cast<int64_t>(kSVCMode));
+  EXPECT_UKM(UkmEntry::kUseCaseName, static_cast<int64_t>(kEncoderUseCase));
+  EXPECT_UKM(UkmEntry::kWidthName, kWidth);
+}
+
+TEST_F(VideoEncoderMetricsProviderTest,
+       CallErrorAndNoCallSetEncodedFramesCount_ReportUKMWithTheFirstError) {
+  auto [test_recorder, provider] = Create(kTestURL);
+  constexpr auto kEncoderUseCase = mojom::VideoEncoderUseCase::kWebRTC;
+  constexpr auto kCodecProfile = VP9PROFILE_PROFILE0;
+  constexpr gfx::Size kEncodeSize(1920, 1080);
+  constexpr bool kIsHardwareEncoder = true;
+  constexpr auto kSVCMode = SVCScalabilityMode::kL1T3;
+  provider->Initialize(kEncoderUseCase, kCodecProfile, kEncodeSize,
+                       kIsHardwareEncoder, kSVCMode);
+  provider->SetError({EncoderStatus::Codes::kEncoderMojoConnectionError,
+                      "mojo connection is disclosed"});
+  provider->SetError(
+      {EncoderStatus::Codes::kEncoderFailedEncode, "Encoder failed"});
+  provider.reset();
+  base::RunLoop().RunUntilIdle();
+
+  const auto entries = test_recorder->GetEntriesByName(UkmEntry::kEntryName);
+  ASSERT_EQ(1u, entries.size());
+  const auto* entry = entries[0];
+  constexpr uint64_t kHeight = kEncodeSize.height() / 100 * 100;
+  constexpr uint64_t kWidth = kEncodeSize.width() / 100 * 100;
+  EXPECT_UKM(UkmEntry::kHeightName, kHeight);
+  EXPECT_UKM(UkmEntry::kIsHardwareName, kIsHardwareEncoder);
+  EXPECT_UKM(UkmEntry::kNumEncodedFramesName, 0);
+  EXPECT_UKM(UkmEntry::kProfileName, kCodecProfile);
+  EXPECT_UKM(
+      UkmEntry::kStatusName,
+      static_cast<int64_t>(EncoderStatus::Codes::kEncoderMojoConnectionError));
+  EXPECT_UKM(UkmEntry::kSVCModeName, static_cast<int64_t>(kSVCMode));
+  EXPECT_UKM(UkmEntry::kUseCaseName, static_cast<int64_t>(kEncoderUseCase));
+  EXPECT_UKM(UkmEntry::kWidthName, kWidth);
+}
+
+TEST_F(
+    VideoEncoderMetricsProviderTest,
+    CallSetEncodedFrameCountsAndSetError_ReportUKMWithTheFirstErrorAndTheLastEncodedFrameCount) {
+  auto [test_recorder, provider] = Create(kTestURL);
+  constexpr auto kEncoderUseCase = mojom::VideoEncoderUseCase::kWebRTC;
+  constexpr auto kCodecProfile = VP9PROFILE_PROFILE0;
+  constexpr gfx::Size kEncodeSize(1920, 1080);
+  constexpr bool kIsHardwareEncoder = true;
+  constexpr auto kSVCMode = SVCScalabilityMode::kL1T3;
+  provider->Initialize(kEncoderUseCase, kCodecProfile, kEncodeSize,
+                       kIsHardwareEncoder, kSVCMode);
+  provider->SetEncodedFrameCount(100);
+  provider->SetEncodedFrameCount(200);
+  provider->SetEncodedFrameCount(300);
+  provider->SetError(
+      {EncoderStatus::Codes::kEncoderFailedEncode, "Encoder failed"});
+  provider->SetError(
+      {EncoderStatus::Codes::kEncoderIllegalState, "Encoder illegal state"});
+  provider->SetEncodedFrameCount(400);
+  provider.reset();
+  base::RunLoop().RunUntilIdle();
+
+  const auto entries = test_recorder->GetEntriesByName(UkmEntry::kEntryName);
+  ASSERT_EQ(1u, entries.size());
+  const auto* entry = entries[0];
+  constexpr uint64_t kHeight = kEncodeSize.height() / 100 * 100;
+  constexpr uint64_t kWidth = kEncodeSize.width() / 100 * 100;
+  EXPECT_UKM(UkmEntry::kHeightName, kHeight);
+  EXPECT_UKM(UkmEntry::kIsHardwareName, kIsHardwareEncoder);
+  EXPECT_UKM(UkmEntry::kNumEncodedFramesName, 400);
+  EXPECT_UKM(UkmEntry::kProfileName, kCodecProfile);
+  EXPECT_UKM(UkmEntry::kStatusName,
+             static_cast<int64_t>(EncoderStatus::Codes::kEncoderFailedEncode));
+  EXPECT_UKM(UkmEntry::kSVCModeName, static_cast<int64_t>(kSVCMode));
+  EXPECT_UKM(UkmEntry::kUseCaseName, static_cast<int64_t>(kEncoderUseCase));
+  EXPECT_UKM(UkmEntry::kWidthName, kWidth);
+}
+
+TEST_F(VideoEncoderMetricsProviderTest,
+       CreateAndTwoInitializeAndSetEncodedFrameCounts_ReportTwoUKMs) {
+  const struct {
+    mojom::VideoEncoderUseCase use_case;
+    VideoCodecProfile profile;
+    gfx::Size size;
+    bool is_hardware;
+    SVCScalabilityMode svc_mode;
+    EncoderStatus::Codes status;
+    uint64_t num_encoded_frames;
+  } kMetricsCases[] = {
+      {
+          mojom::VideoEncoderUseCase::kWebRTC,
+          VP9PROFILE_PROFILE0,
+          gfx::Size(600, 300),
+          true,
+          SVCScalabilityMode::kL2T3Key,
+          EncoderStatus::Codes::kEncoderIllegalState,
+          100,
+      },
+      {
+          mojom::VideoEncoderUseCase::kMediaRecorder,
+          H264PROFILE_HIGH,
+          gfx::Size(1200, 700),
+          /*is_hardware=*/true,
+          SVCScalabilityMode::kL2T3Key,
+          EncoderStatus::Codes::kOk,
+          300,
+      },
+  };
+  auto [test_recorder, provider] = Create(kTestURL);
+  for (const auto& metrics : kMetricsCases) {
+    provider->Initialize(metrics.use_case, metrics.profile, metrics.size,
+                         metrics.is_hardware, metrics.svc_mode);
+    provider->SetEncodedFrameCount(metrics.num_encoded_frames);
+    if (metrics.status != EncoderStatus::Codes::kOk) {
+      provider->SetError(metrics.status);
+    }
+  }
+  provider.reset();
+  base::RunLoop().RunUntilIdle();
+
+  const auto entries = test_recorder->GetEntriesByName(UkmEntry::kEntryName);
+  ASSERT_EQ(std::size(kMetricsCases), entries.size());
+  for (size_t i = 0; i < entries.size(); ++i) {
+    const auto* entry = entries[i];
+    const auto& metrics = kMetricsCases[i];
+    EXPECT_UKM(UkmEntry::kHeightName, metrics.size.height());
+    EXPECT_UKM(UkmEntry::kIsHardwareName, metrics.is_hardware);
+    EXPECT_UKM(UkmEntry::kNumEncodedFramesName, metrics.num_encoded_frames);
+    EXPECT_UKM(UkmEntry::kProfileName, metrics.profile);
+    EXPECT_UKM(UkmEntry::kStatusName, static_cast<int64_t>(metrics.status));
+    EXPECT_UKM(UkmEntry::kSVCModeName, static_cast<int64_t>(metrics.svc_mode));
+    EXPECT_UKM(UkmEntry::kUseCaseName, static_cast<int64_t>(metrics.use_case));
+    EXPECT_UKM(UkmEntry::kWidthName, metrics.size.width());
+  }
+}
+
+#undef EXPECT_UKM
+}  // namespace media
diff --git a/media/renderers/video_resource_updater.cc b/media/renderers/video_resource_updater.cc
index a23d02eb..0bc22fb 100644
--- a/media/renderers/video_resource_updater.cc
+++ b/media/renderers/video_resource_updater.cc
@@ -28,7 +28,6 @@
 #include "cc/paint/skia_paint_canvas.h"
 #include "components/viz/client/client_resource_provider.h"
 #include "components/viz/client/shared_bitmap_reporter.h"
-#include "components/viz/common/gpu/context_provider.h"
 #include "components/viz/common/gpu/raster_context_provider.h"
 #include "components/viz/common/quads/compositor_render_pass.h"
 #include "components/viz/common/quads/texture_draw_quad.h"
@@ -481,16 +480,11 @@
                         viz::SharedImageFormat format,
                         const gfx::ColorSpace& color_space,
                         bool use_gpu_memory_buffer_resources,
-                        viz::ContextProvider* context_provider,
-                        viz::RasterContextProvider* raster_context_provider)
+                        viz::RasterContextProvider* context_provider)
       : PlaneResource(plane_resource_id, size, format, /*is_software=*/false),
-        context_provider_(context_provider),
-        raster_context_provider_(raster_context_provider) {
-    DCHECK(context_provider_ || raster_context_provider_);
-    const gpu::Capabilities& caps =
-        raster_context_provider_
-            ? raster_context_provider_->ContextCapabilities()
-            : context_provider_->ContextCapabilities();
+        context_provider_(context_provider) {
+    DCHECK(context_provider_);
+    const gpu::Capabilities& caps = context_provider_->ContextCapabilities();
     DCHECK(format.is_single_plane());
     // TODO(hitawala): Add multiplanar support for software decode.
     overlay_candidate_ =
@@ -530,22 +524,18 @@
 
  private:
   gpu::SharedImageInterface* SharedImageInterface() {
-    auto* sii = raster_context_provider_
-                    ? raster_context_provider_->SharedImageInterface()
-                    : context_provider_->SharedImageInterface();
+    auto* sii = context_provider_->SharedImageInterface();
     DCHECK(sii);
     return sii;
   }
 
   gpu::gles2::GLES2Interface* ContextGL() {
-    auto* gl = raster_context_provider_ ? raster_context_provider_->ContextGL()
-                                        : context_provider_->ContextGL();
+    auto* gl = context_provider_->ContextGL();
     DCHECK(gl);
     return gl;
   }
 
-  const raw_ptr<viz::ContextProvider> context_provider_;
-  const raw_ptr<viz::RasterContextProvider> raster_context_provider_;
+  const raw_ptr<viz::RasterContextProvider> context_provider_;
   gpu::Mailbox mailbox_;
   GLenum texture_target_ = GL_TEXTURE_2D;
   bool overlay_candidate_ = false;
@@ -564,8 +554,7 @@
 }
 
 VideoResourceUpdater::VideoResourceUpdater(
-    viz::ContextProvider* context_provider,
-    viz::RasterContextProvider* raster_context_provider,
+    viz::RasterContextProvider* context_provider,
     viz::SharedBitmapReporter* shared_bitmap_reporter,
     viz::ClientResourceProvider* resource_provider,
     bool use_stream_video_draw_quad,
@@ -573,7 +562,6 @@
     bool use_r16_texture,
     int max_resource_size)
     : context_provider_(context_provider),
-      raster_context_provider_(raster_context_provider),
       shared_bitmap_reporter_(shared_bitmap_reporter),
       resource_provider_(resource_provider),
       use_stream_video_draw_quad_(use_stream_video_draw_quad),
@@ -581,8 +569,7 @@
       use_r16_texture_(use_r16_texture),
       max_resource_size_(max_resource_size),
       tracing_id_(g_next_video_resource_updater_id.GetNext()) {
-  DCHECK(context_provider_ || raster_context_provider_ ||
-         shared_bitmap_reporter_);
+  DCHECK(context_provider_ || shared_bitmap_reporter_);
 
   base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider(
       this, "media::VideoResourceUpdater",
@@ -776,10 +763,8 @@
 
 viz::SharedImageFormat VideoResourceUpdater::YuvSharedImageFormat(
     int bits_per_channel) {
-  DCHECK(raster_context_provider_ || context_provider_);
-  const auto& caps = raster_context_provider_
-                         ? raster_context_provider_->ContextCapabilities()
-                         : context_provider_->ContextCapabilities();
+  DCHECK(context_provider_);
+  const auto& caps = context_provider_->ContextCapabilities();
   if (caps.disable_one_component_textures)
     return PaintCanvasVideoRenderer::GetRGBPixelsOutputFormat();
   if (bits_per_channel <= 8)
@@ -861,8 +846,7 @@
   } else {
     all_resources_.push_back(std::make_unique<HardwarePlaneResource>(
         plane_resource_id, plane_size, format, color_space,
-        use_gpu_memory_buffer_resources_, context_provider_,
-        raster_context_provider_));
+        use_gpu_memory_buffer_resources_, context_provider_));
   }
   return all_resources_.back().get();
 }
@@ -936,8 +920,9 @@
     scoped_refptr<VideoFrame> video_frame) {
   TRACE_EVENT0("cc", "VideoResourceUpdater::CreateForHardwarePlanes");
   DCHECK(video_frame->HasTextures());
-  if (!context_provider_ && !raster_context_provider_)
+  if (!context_provider_) {
     return VideoFrameExternalResources();
+  }
 
   VideoFrameExternalResources external_resources;
   gfx::ColorSpace resource_color_space = video_frame->ColorSpace();
@@ -1456,8 +1441,7 @@
 }
 
 gpu::gles2::GLES2Interface* VideoResourceUpdater::ContextGL() {
-  auto* gl = raster_context_provider_ ? raster_context_provider_->ContextGL()
-                                      : context_provider_->ContextGL();
+  auto* gl = context_provider_->ContextGL();
   DCHECK(gl);
   return gl;
 }
@@ -1491,7 +1475,7 @@
   if (resource_it == all_resources_.end())
     return;
 
-  if ((raster_context_provider_ || context_provider_) && sync_token.HasData()) {
+  if (context_provider_ && sync_token.HasData()) {
     ContextGL()->WaitSyncTokenCHROMIUM(sync_token.GetConstData());
   }
 
diff --git a/media/renderers/video_resource_updater.h b/media/renderers/video_resource_updater.h
index b3414021..7d9ffe1 100644
--- a/media/renderers/video_resource_updater.h
+++ b/media/renderers/video_resource_updater.h
@@ -33,7 +33,6 @@
 
 namespace viz {
 class ClientResourceProvider;
-class ContextProvider;
 class RasterContextProvider;
 class CompositorRenderPass;
 class SharedBitmapReporter;
@@ -88,8 +87,7 @@
   // For GPU compositing |context_provider| should be provided and for software
   // compositing |shared_bitmap_reporter| should be provided. If there is a
   // non-null |context_provider| we assume GPU compositing.
-  VideoResourceUpdater(viz::ContextProvider* context_provider,
-                       viz::RasterContextProvider* raster_context_provider,
+  VideoResourceUpdater(viz::RasterContextProvider* context_provider,
                        viz::SharedBitmapReporter* shared_bitmap_reporter,
                        viz::ClientResourceProvider* resource_provider,
                        bool use_stream_video_draw_quad,
@@ -148,9 +146,7 @@
     gfx::Size size_in_pixels;
   };
 
-  bool software_compositor() const {
-    return context_provider_ == nullptr && raster_context_provider_ == nullptr;
-  }
+  bool software_compositor() const { return context_provider_ == nullptr; }
 
   // Reallocate |upload_pixels_| with the requested size.
   bool ReallocateUploadPixels(size_t needed_size);
@@ -207,8 +203,7 @@
   bool OnMemoryDump(const base::trace_event::MemoryDumpArgs& args,
                     base::trace_event::ProcessMemoryDump* pmd) override;
 
-  const raw_ptr<viz::ContextProvider, DanglingUntriaged> context_provider_;
-  const raw_ptr<viz::RasterContextProvider> raster_context_provider_;
+  const raw_ptr<viz::RasterContextProvider> context_provider_;
   const raw_ptr<viz::SharedBitmapReporter> shared_bitmap_reporter_;
   const raw_ptr<viz::ClientResourceProvider, DanglingUntriaged>
       resource_provider_;
diff --git a/media/renderers/video_resource_updater_unittest.cc b/media/renderers/video_resource_updater_unittest.cc
index ba4781dd..45d6b46 100644
--- a/media/renderers/video_resource_updater_unittest.cc
+++ b/media/renderers/video_resource_updater_unittest.cc
@@ -93,16 +93,16 @@
   std::unique_ptr<VideoResourceUpdater> CreateUpdaterForHardware(
       bool use_stream_video_draw_quad = false) {
     return std::make_unique<VideoResourceUpdater>(
-        context_provider_.get(), /*raster_context_provider=*/nullptr, nullptr,
-        resource_provider_.get(), use_stream_video_draw_quad,
+        context_provider_.get(), nullptr, resource_provider_.get(),
+        use_stream_video_draw_quad,
         /*use_gpu_memory_buffer_resources=*/false,
         /*use_r16_texture=*/use_r16_texture_, /*max_resource_size=*/10000);
   }
 
   std::unique_ptr<VideoResourceUpdater> CreateUpdaterForSoftware() {
     return std::make_unique<VideoResourceUpdater>(
-        /*context_provider=*/nullptr, /*raster_context_provider=*/nullptr,
-        &shared_bitmap_reporter_, resource_provider_.get(),
+        /*context_provider=*/nullptr, &shared_bitmap_reporter_,
+        resource_provider_.get(),
         /*use_stream_video_draw_quad=*/false,
         /*use_gpu_memory_buffer_resources=*/false,
         /*use_r16_texture=*/false,
diff --git a/net/base/features.cc b/net/base/features.cc
index 0750947..bdb72ff5 100644
--- a/net/base/features.cc
+++ b/net/base/features.cc
@@ -375,4 +375,14 @@
              "ForceThirdPartyCookieBlockingEnabled",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+// If the HTTP Cache Transaction write lock should be acquired async with
+// sending the HTTP request.
+BASE_FEATURE(kAsyncCacheLock,
+             "AsyncCacheLock",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
+BASE_FEATURE(kEnableEarlyHintsOnHttp11,
+             "EnableEarlyHintsOnHttp11",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 }  // namespace net::features
diff --git a/net/base/features.h b/net/base/features.h
index 809f858d..35f5c3091 100644
--- a/net/base/features.h
+++ b/net/base/features.h
@@ -395,6 +395,13 @@
 // Enables enabling third-party cookie blocking from the command line.
 NET_EXPORT BASE_DECLARE_FEATURE(kForceThirdPartyCookieBlocking);
 
+// If the HTTP Cache Transaction write lock should be acquired async with
+// sending the HTTP request.
+NET_EXPORT BASE_DECLARE_FEATURE(kAsyncCacheLock);
+
+// Enables Early Hints on HTTP/1.1.
+NET_EXPORT BASE_DECLARE_FEATURE(kEnableEarlyHintsOnHttp11);
+
 }  // namespace net::features
 
 #endif  // NET_BASE_FEATURES_H_
diff --git a/net/http/http_cache.cc b/net/http/http_cache.cc
index f6f9bf3..a4db692 100644
--- a/net/http/http_cache.cc
+++ b/net/http/http_cache.cc
@@ -194,7 +194,7 @@
     if (entry_)
       *entry_ = entry;
     if (transaction_)
-      transaction_->io_callback().Run(result);
+      transaction_->cache_io_callback().Run(result);
   }
 
   // Notifies the caller about the operation completion. Returns true if the
@@ -890,7 +890,10 @@
   DCHECK(entry->GetEntry());
   // Always add a new transaction to the queue to maintain FIFO order.
   entry->add_to_entry_queue.push_back(transaction);
-  ProcessQueuedTransactions(entry);
+  // Don't process the transaction if the lock timeout handling is being tested.
+  if (!bypass_lock_for_test_) {
+    ProcessQueuedTransactions(entry);
+  }
   return ERR_IO_PENDING;
 }
 
@@ -1045,7 +1048,7 @@
     transaction->ResetCachePendingState();
     base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
         FROM_HERE,
-        base::BindOnce(transaction->io_callback(), net::ERR_CACHE_RACE));
+        base::BindOnce(transaction->cache_io_callback(), net::ERR_CACHE_RACE));
   }
   entry->add_to_entry_queue.clear();
 }
@@ -1082,7 +1085,7 @@
   }
   // ERR_CACHE_RACE causes the transaction to restart the whole process.
   for (auto* queued_transaction : list)
-    queued_transaction->io_callback().Run(net::ERR_CACHE_RACE);
+    queued_transaction->cache_io_callback().Run(net::ERR_CACHE_RACE);
 }
 
 void HttpCache::RestartHeadersPhaseTransactions(ActiveEntry* entry) {
@@ -1093,7 +1096,7 @@
   while (it != entry->done_headers_queue.end()) {
     Transaction* done_headers_transaction = *it;
     it = entry->done_headers_queue.erase(it);
-    done_headers_transaction->io_callback().Run(net::ERR_CACHE_RACE);
+    done_headers_transaction->cache_io_callback().Run(net::ERR_CACHE_RACE);
   }
 }
 
@@ -1120,6 +1123,20 @@
 }
 
 void HttpCache::ProcessAddToEntryQueue(ActiveEntry* entry) {
+  if (delay_add_transaction_to_entry_for_test_) {
+    // Post a task to put the AddTransactionToEntry handling at the back of
+    // the task queue. This allows other tasks (like network IO) to jump
+    // ahead and simulate different callback ordering for testing.
+    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+        FROM_HERE,
+        base::BindOnce(&HttpCache::ProcessAddToEntryQueueImpl, GetWeakPtr(),
+                       base::UnsafeDanglingUntriaged(entry)));
+  } else {
+    ProcessAddToEntryQueueImpl(entry);
+  }
+}
+
+void HttpCache::ProcessAddToEntryQueueImpl(ActiveEntry* entry) {
   DCHECK(!entry->add_to_entry_queue.empty());
 
   // Note the entry may be new or may already have a response body written to
@@ -1132,7 +1149,7 @@
   entry->add_to_entry_queue.erase(entry->add_to_entry_queue.begin());
   entry->headers_transaction = transaction;
 
-  transaction->io_callback().Run(OK);
+  transaction->cache_io_callback().Run(OK);
 }
 
 HttpCache::ParallelWritingPattern HttpCache::CanTransactionJoinExistingWriters(
@@ -1198,7 +1215,7 @@
   ProcessQueuedTransactions(entry);
 
   entry->done_headers_queue.erase(entry->done_headers_queue.begin());
-  transaction->io_callback().Run(OK);
+  transaction->cache_io_callback().Run(OK);
 }
 
 void HttpCache::AddTransactionToWriters(
diff --git a/net/http/http_cache.h b/net/http/http_cache.h
index ddbcbab..8f4225d 100644
--- a/net/http/http_cache.h
+++ b/net/http/http_cache.h
@@ -227,6 +227,10 @@
     bypass_lock_after_headers_for_test_ = true;
   }
 
+  void DelayAddTransactionToEntryForTesting() {
+    delay_add_transaction_to_entry_for_test_ = true;
+  }
+
   // Causes all transactions created after this point to generate a failure
   // when attempting to conditionalize a network request.
   void FailConditionalizationForTest() {
@@ -421,9 +425,9 @@
 
   // Makes sure that the backend creation is complete before allowing the
   // provided transaction to use the object. Returns an error code.
-  // |transaction| will be notified via its IO callback if this method returns
-  // ERR_IO_PENDING. The transaction is free to use the backend directly at any
-  // time after receiving the notification.
+  // |transaction| will be notified via its Cache IO callback if this method
+  // returns ERR_IO_PENDING. The transaction is free to use the backend
+  // directly at any time after receiving the notification.
   int GetBackendForTransaction(Transaction* transaction);
 
   // Dooms the entry selected by |key|, if it is currently in the list of active
@@ -431,15 +435,15 @@
   void DoomActiveEntry(const std::string& key);
 
   // Dooms the entry selected by |key|. |transaction| will be notified via its
-  // IO callback if this method returns ERR_IO_PENDING. The entry can be
-  // currently in use or not. If entry is in use and the invoking transaction is
-  // associated with this entry and this entry is already doomed, this API
+  // Cache IO callback if this method returns ERR_IO_PENDING. The entry can be
+  // currently in use or not. If entry is in use and the invoking transaction
+  // is associated with this entry and this entry is already doomed, this API
   // should not be invoked.
   int DoomEntry(const std::string& key, Transaction* transaction);
 
   // Dooms the entry selected by |key|. |transaction| will be notified via its
-  // IO callback if this method returns ERR_IO_PENDING. The entry should not be
-  // currently in use.
+  // Cache IO callback if this method returns ERR_IO_PENDING. The entry should
+  // not be currently in use.
   int AsyncDoomEntry(const std::string& key, Transaction* transaction);
 
   // Dooms the entry associated with a GET for a given url and network
@@ -473,25 +477,25 @@
 
   // Opens the disk cache entry associated with |key|, creating the entry if it
   // does not already exist, returning an ActiveEntry in |*entry|. |transaction|
-  // will be notified via its IO callback if this method returns ERR_IO_PENDING.
-  // This should not be called if there already is an active entry associated
-  // with |key|, e.g. you should call FindActiveEntry first.
+  // will be notified via its Cache IO callback if this method returns
+  // ERR_IO_PENDING. This should not be called if there already is an active
+  // entry associated with |key|, e.g. you should call FindActiveEntry first.
   int OpenOrCreateEntry(const std::string& key,
                         ActiveEntry** entry,
                         Transaction* transaction);
 
   // Opens the disk cache entry associated with |key|, returning an ActiveEntry
-  // in |*entry|. |transaction| will be notified via its IO callback if this
-  // method returns ERR_IO_PENDING. This should not be called if there already
-  // is an active entry associated with |key|, e.g. you should call
+  // in |*entry|. |transaction| will be notified via its Cache IO callback if
+  // this method returns ERR_IO_PENDING. This should not be called if there
+  // already is an active entry associated with |key|, e.g. you should call
   // FindActiveEntry first.
   int OpenEntry(const std::string& key,
                 ActiveEntry** entry,
                 Transaction* transaction);
 
   // Creates the disk cache entry associated with |key|, returning an
-  // ActiveEntry in |*entry|. |transaction| will be notified via its IO callback
-  // if this method returns ERR_IO_PENDING.
+  // ActiveEntry in |*entry|. |transaction| will be notified via its Cache IO
+  // callback if this method returns ERR_IO_PENDING.
   int CreateEntry(const std::string& key,
                   ActiveEntry** entry,
                   Transaction* transaction);
@@ -501,7 +505,8 @@
   void DestroyEntry(ActiveEntry* entry);
 
   // Adds a transaction to an ActiveEntry. This method returns ERR_IO_PENDING
-  // and the transaction will be notified about completion via its IO callback.
+  // and the transaction will be notified about completion via a callback to
+  // cache_io_callback().
   // In a failure case, the callback will be invoked with ERR_CACHE_RACE.
   int AddTransactionToEntry(ActiveEntry* entry, Transaction* transaction);
 
@@ -509,7 +514,7 @@
   // If the transaction is responsible for writing the response body,
   // it becomes the writer and returns OK. In other cases ERR_IO_PENDING is
   // returned and the transaction will be notified about completion via its
-  // IO callback. In a failure case, the callback will be invoked with
+  // Cache IO callback. In a failure case, the callback will be invoked with
   // ERR_CACHE_RACE.
   int DoneWithResponseHeaders(ActiveEntry* entry,
                               Transaction* transaction,
@@ -559,19 +564,23 @@
 
   // Restarts the headers_transaction by setting its state. Since the
   // headers_transaction is awaiting an asynchronous operation completion,
-  // it will be restarted when it's IO callback is invoked.
+  // it will be restarted when it's Cache IO callback is invoked.
   void RestartHeadersTransaction(ActiveEntry* entry);
 
   // Resumes processing the queued transactions of |entry|.
   void ProcessQueuedTransactions(ActiveEntry* entry);
 
   // Checks if a transaction can be added to the entry. If yes, it will
-  // invoke the IO callback of the transaction. This is a helper function for
-  // OnProcessQueuedTransactions. It will take a transaction from
+  // invoke the Cache IO callback of the transaction. This is a helper function
+  // for OnProcessQueuedTransactions. It will take a transaction from
   // add_to_entry_queue and make it a headers_transaction, if one doesn't exist
   // already.
   void ProcessAddToEntryQueue(ActiveEntry* entry);
 
+  // The implementation is split into a separate function so that it can be
+  // called with a delay for testing.
+  void ProcessAddToEntryQueueImpl(ActiveEntry* entry);
+
   // Returns if the transaction can join other transactions for writing to
   // the cache simultaneously. It is only supported for non-Read only,
   // GET requests which are not range requests.
@@ -665,6 +674,7 @@
   bool building_backend_ = false;
   bool bypass_lock_for_test_ = false;
   bool bypass_lock_after_headers_for_test_ = false;
+  bool delay_add_transaction_to_entry_for_test_ = false;
   bool fail_conditionalization_for_test_ = false;
 
   Mode mode_ = NORMAL;
diff --git a/net/http/http_cache_transaction.cc b/net/http/http_cache_transaction.cc
index a48ac3aa..dc60fad 100644
--- a/net/http/http_cache_transaction.cc
+++ b/net/http/http_cache_transaction.cc
@@ -197,6 +197,8 @@
 
   io_callback_ = base::BindRepeating(&Transaction::OnIOComplete,
                                      weak_factory_.GetWeakPtr());
+  cache_io_callback_ = base::BindRepeating(&Transaction::OnCacheIOComplete,
+                                           weak_factory_.GetWeakPtr());
 }
 
 HttpCache::Transaction::~Transaction() {
@@ -1398,7 +1400,7 @@
   new_entry_->opened = true;
 
   int rv = cache_->AddTransactionToEntry(new_entry_, this);
-  DCHECK_EQ(rv, ERR_IO_PENDING);
+  CHECK_EQ(rv, ERR_IO_PENDING);
 
   // If headers phase is already done then we are here because of validation not
   // matching and creating a new entry. This transaction should be the
@@ -1412,14 +1414,25 @@
 
   TransitionToState(STATE_ADD_TO_ENTRY_COMPLETE);
 
+  // For a very-select case of creating a new non-range request entry, run the
+  // AddTransactionToEntry in parallel with sending the network request to
+  // hide the latency. This will run until the next ERR_IO_PENDING (or
+  // failure).
+  if (!partial_ && mode_ == WRITE &&
+      base::FeatureList::IsEnabled(features::kAsyncCacheLock)) {
+    CHECK(!waiting_for_cache_io_);
+    waiting_for_cache_io_ = true;
+    rv = OK;
+  }
+
   entry_lock_waiting_since_ = TimeTicks::Now();
   AddCacheLockTimeoutHandler(new_entry_);
   return rv;
 }
 
 void HttpCache::Transaction::AddCacheLockTimeoutHandler(ActiveEntry* entry) {
-  DCHECK(next_state_ == STATE_ADD_TO_ENTRY_COMPLETE ||
-         next_state_ == STATE_FINISH_HEADERS_COMPLETE);
+  CHECK(next_state_ == STATE_ADD_TO_ENTRY_COMPLETE ||
+        next_state_ == STATE_FINISH_HEADERS_COMPLETE);
   if ((bypass_lock_for_test_ && next_state_ == STATE_ADD_TO_ENTRY_COMPLETE) ||
       (bypass_lock_after_headers_for_test_ &&
        next_state_ == STATE_FINISH_HEADERS_COMPLETE)) {
@@ -1470,15 +1483,19 @@
     base::UmaHistogramTimes("HttpCache.AddTransactionToEntry", entry_lock_wait);
   }
 
-  entry_lock_waiting_since_ = TimeTicks();
   DCHECK(new_entry_);
-  cache_pending_ = false;
 
-  if (result == OK)
-    entry_ = new_entry_;
+  if (!waiting_for_cache_io_) {
+    entry_lock_waiting_since_ = TimeTicks();
+    cache_pending_ = false;
 
-  // If there is a failure, the cache should have taken care of new_entry_.
-  new_entry_ = nullptr;
+    if (result == OK) {
+      entry_ = new_entry_;
+    }
+
+    // If there is a failure, the cache should have taken care of new_entry_.
+    new_entry_ = nullptr;
+  }
 
   if (result == ERR_CACHE_RACE) {
     TransitionToState(STATE_HEADERS_PHASE_CANNOT_PROCEED);
@@ -1504,8 +1521,9 @@
   // TODO(crbug.com/713354) Access timestamp for histograms only if entry is
   // already written, to avoid data race since cache thread can also access
   // this.
-  if (!cache_->IsWritingInProgress(entry()))
+  if (entry_ && !cache_->IsWritingInProgress(entry())) {
     open_entry_last_used_ = entry_->GetEntry()->GetLastUsed();
+  }
 
   // TODO(jkarlin): We should either handle the case or DCHECK.
   if (result != OK) {
@@ -1864,6 +1882,12 @@
 
   TransitionToState(STATE_SEND_REQUEST_COMPLETE);
   rv = network_trans_->Start(request_, io_callback_, net_log_);
+  if (rv != ERR_IO_PENDING && waiting_for_cache_io_) {
+    // queue the state transition until the HttpCache transaction completes
+    DCHECK(!pending_io_result_);
+    pending_io_result_ = rv;
+    rv = ERR_IO_PENDING;
+  }
   return rv;
 }
 
@@ -2324,7 +2348,7 @@
 
   // If the transaction needs to wait because another transaction is still
   // writing the response body, it will return ERR_IO_PENDING now and the
-  // io_callback_ will be invoked when the wait is done.
+  // cache_io_callback_ will be invoked when the wait is done.
   int rv = cache_->DoneWithResponseHeaders(entry_, this, partial_ != nullptr);
   DCHECK(!reading_ || rv == OK) << "Expected OK, but got " << rv;
 
@@ -3644,16 +3668,17 @@
     return;
 
   DCHECK(next_state_ == STATE_ADD_TO_ENTRY_COMPLETE ||
-         next_state_ == STATE_FINISH_HEADERS_COMPLETE);
+         next_state_ == STATE_FINISH_HEADERS_COMPLETE || waiting_for_cache_io_);
 
   if (!cache_)
     return;
 
-  if (next_state_ == STATE_ADD_TO_ENTRY_COMPLETE)
+  if (next_state_ == STATE_ADD_TO_ENTRY_COMPLETE || waiting_for_cache_io_) {
     cache_->RemovePendingTransaction(this);
-  else
+  } else {
     DoneWithEntry(false /* entry_is_complete */);
-  OnIOComplete(ERR_CACHE_LOCK_TIMEOUT);
+  }
+  OnCacheIOComplete(ERR_CACHE_LOCK_TIMEOUT);
 }
 
 void HttpCache::Transaction::DoomPartialEntry(bool delete_object) {
@@ -4000,7 +4025,46 @@
 }
 
 void HttpCache::Transaction::OnIOComplete(int result) {
-  DoLoop(result);
+  if (waiting_for_cache_io_) {
+    CHECK_NE(result, ERR_CACHE_RACE);
+    // If the HttpCache IO hasn't completed yet, queue the IO result
+    // to be processed when the HttpCache IO completes (or times out).
+    pending_io_result_ = result;
+  } else {
+    DoLoop(result);
+  }
+}
+
+void HttpCache::Transaction::OnCacheIOComplete(int result) {
+  if (waiting_for_cache_io_) {
+    // Handle the case of parallel HttpCache transactions being run against
+    // network IO.
+    waiting_for_cache_io_ = false;
+    cache_pending_ = false;
+    entry_lock_waiting_since_ = TimeTicks();
+
+    if (result == OK) {
+      entry_ = new_entry_;
+      if (!cache_->IsWritingInProgress(entry())) {
+        open_entry_last_used_ = entry_->GetEntry()->GetLastUsed();
+      }
+    } else {
+      // The HttpCache transaction failed or timed out. Bypass the cache in
+      // this case independent of the state of the network IO callback.
+      mode_ = NONE;
+    }
+    new_entry_ = nullptr;
+
+    // See if there is a pending IO result that completed while the HttpCache
+    // transaction was being processed that now needs to be processed.
+    if (pending_io_result_) {
+      int stored_result = pending_io_result_.value();
+      pending_io_result_ = absl::nullopt;
+      OnIOComplete(stored_result);
+    }
+  } else {
+    DoLoop(result);
+  }
 }
 
 void HttpCache::Transaction::TransitionToState(State state) {
diff --git a/net/http/http_cache_transaction.h b/net/http/http_cache_transaction.h
index b7ef641..85a417fa 100644
--- a/net/http/http_cache_transaction.h
+++ b/net/http/http_cache_transaction.h
@@ -104,12 +104,21 @@
   // to the cache entry.
   LoadState GetWriterLoadState() const;
 
-  const CompletionRepeatingCallback& io_callback() { return io_callback_; }
-
   void SetIOCallBackForTest(CompletionRepeatingCallback cb) {
     io_callback_ = cb;
   }
 
+  // Returns the IO callback specific to HTTPCache callbacks. This is done
+  // indirectly so the callbacks can be replaced when testing.
+  // TODO(https://crbug.com/1454228/): Find a cleaner way to do this so the
+  // callback can be called directly.
+  const CompletionRepeatingCallback& cache_io_callback() {
+    return cache_io_callback_;
+  }
+  void SetCacheIOCallBackForTest(CompletionRepeatingCallback cb) {
+    cache_io_callback_ = cb;
+  }
+
   const NetLogWithSource& net_log() const;
 
   // Bypasses the cache lock whenever there is lock contention.
@@ -575,6 +584,11 @@
   // continue its processing.
   void OnIOComplete(int result);
 
+  // Called to signal completion of an asynchronous HTTPCache operation. It
+  // uses a separate callback from OnIoComplete so that cache transaction
+  // operations and network IO can be run in parallel.
+  void OnCacheIOComplete(int result);
+
   // When in a DoLoop, use this to set the next state as it verifies that the
   // state isn't set twice.
   void TransitionToState(State state);
@@ -611,6 +625,14 @@
 
   State next_state_{STATE_NONE};
 
+  // Set when a HTTPCache transaction is pending in parallel with other IO.
+  bool waiting_for_cache_io_ = false;
+
+  // If a pending async HTTPCache transaction takes longer than the parallel
+  // Network IO, this will store the result of the Network IO operation until
+  // the cache transaction completes (or times out).
+  absl::optional<int> pending_io_result_ = absl::nullopt;
+
   // Used for tracing.
   const uint64_t trace_id_;
 
@@ -689,6 +711,7 @@
   int effective_load_flags_ = 0;
   std::unique_ptr<PartialData> partial_;  // We are dealing with range requests.
   CompletionRepeatingCallback io_callback_;
+  CompletionRepeatingCallback cache_io_callback_;  // cache-specific IO callback
 
   // Error code to be returned from a subsequent Read call if shared writing
   // failed in a separate transaction.
diff --git a/net/http/http_cache_unittest.cc b/net/http/http_cache_unittest.cc
index 476dd1e..b33ed93 100644
--- a/net/http/http_cache_unittest.cc
+++ b/net/http/http_cache_unittest.cc
@@ -780,7 +780,50 @@
 
 }  // namespace
 
-using HttpCacheTest = TestWithTaskEnvironment;
+using HttpCacheTestBase = TestWithTaskEnvironment;
+
+enum class AsyncCacheLockTestCase {
+  kAsyncCacheLockDisabled,
+  kAsyncCacheLockEnabled,
+};
+
+class HttpCacheTest
+    : public HttpCacheTestBase,
+      public ::testing::WithParamInterface<AsyncCacheLockTestCase> {
+ public:
+  HttpCacheTest() {
+    switch (GetParam()) {
+      case AsyncCacheLockTestCase::kAsyncCacheLockDisabled:
+        scoped_feature_list_.InitAndDisableFeature(features::kAsyncCacheLock);
+        break;
+      case AsyncCacheLockTestCase::kAsyncCacheLockEnabled:
+        scoped_feature_list_.InitAndEnableFeature(features::kAsyncCacheLock);
+        break;
+    }
+  }
+  ~HttpCacheTest() override = default;
+
+  static const char* ParamInfoToString(
+      const testing::TestParamInfo<AsyncCacheLockTestCase>& info) {
+    switch (info.param) {
+      case AsyncCacheLockTestCase::kAsyncCacheLockDisabled:
+        return "AsyncCacheLockDisabled";
+      case AsyncCacheLockTestCase::kAsyncCacheLockEnabled:
+        return "AsyncCacheLockEnabled";
+    }
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    HttpCacheTest,
+    testing::ValuesIn({AsyncCacheLockTestCase::kAsyncCacheLockDisabled,
+                       AsyncCacheLockTestCase::kAsyncCacheLockEnabled}),
+    HttpCacheTest::ParamInfoToString);
+
 class HttpCacheIOCallbackTest : public HttpCacheTest {
  public:
   HttpCacheIOCallbackTest() = default;
@@ -823,6 +866,13 @@
   }
 };
 
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    HttpCacheIOCallbackTest,
+    testing::ValuesIn({AsyncCacheLockTestCase::kAsyncCacheLockDisabled,
+                       AsyncCacheLockTestCase::kAsyncCacheLockEnabled}),
+    HttpCacheTest::ParamInfoToString);
+
 class HttpSplitCacheKeyTest : public HttpCacheTest {
  public:
   HttpSplitCacheKeyTest() = default;
@@ -842,10 +892,17 @@
   }
 };
 
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    HttpSplitCacheKeyTest,
+    testing::ValuesIn({AsyncCacheLockTestCase::kAsyncCacheLockDisabled,
+                       AsyncCacheLockTestCase::kAsyncCacheLockEnabled}),
+    HttpCacheTest::ParamInfoToString);
+
 //-----------------------------------------------------------------------------
 // Tests.
 
-TEST_F(HttpCacheTest, CreateThenDestroy) {
+TEST_P(HttpCacheTest, CreateThenDestroy) {
   MockHttpCache cache;
 
   std::unique_ptr<HttpTransaction> trans;
@@ -853,7 +910,7 @@
   ASSERT_TRUE(trans.get());
 }
 
-TEST_F(HttpCacheTest, GetBackend) {
+TEST_P(HttpCacheTest, GetBackend) {
   MockHttpCache cache(HttpCache::DefaultBackend::InMemory(0));
 
   disk_cache::Backend* backend;
@@ -863,7 +920,7 @@
   EXPECT_THAT(cb.GetResult(rv), IsOk());
 }
 
-TEST_F(HttpCacheTest, SimpleGET) {
+TEST_P(HttpCacheTest, SimpleGET) {
   MockHttpCache cache;
   LoadTimingInfo load_timing_info;
 
@@ -880,7 +937,7 @@
 
 // This test verifies that the callback passed to SetConnectedCallback() is
 // called once for simple GET calls that traverse the cache.
-TEST_F(HttpCacheTest, SimpleGET_ConnectedCallback) {
+TEST_P(HttpCacheTest, SimpleGET_ConnectedCallback) {
   MockHttpCache cache;
 
   ScopedMockTransaction mock_transaction(kSimpleGET_Transaction);
@@ -906,7 +963,7 @@
 
 // This test verifies that when the callback passed to SetConnectedCallback()
 // returns an error, the transaction fails with that error.
-TEST_F(HttpCacheTest, SimpleGET_ConnectedCallbackReturnError) {
+TEST_P(HttpCacheTest, SimpleGET_ConnectedCallbackReturnError) {
   MockHttpCache cache;
   MockHttpRequest request(kSimpleGET_Transaction);
   ConnectedHandler connected_handler;
@@ -929,7 +986,7 @@
 
 // This test verifies that the callback passed to SetConnectedCallback() is
 // called once for requests that hit the cache.
-TEST_F(HttpCacheTest, SimpleGET_ConnectedCallbackOnCacheHit) {
+TEST_P(HttpCacheTest, SimpleGET_ConnectedCallbackOnCacheHit) {
   MockHttpCache cache;
 
   {
@@ -970,7 +1027,7 @@
 // This test verifies that when the callback passed to SetConnectedCallback()
 // is called for a request that hit the cache and returns an error, the cache
 // entry is reusable.
-TEST_F(HttpCacheTest, SimpleGET_ConnectedCallbackOnCacheHitReturnError) {
+TEST_P(HttpCacheTest, SimpleGET_ConnectedCallbackOnCacheHitReturnError) {
   MockHttpCache cache;
 
   {
@@ -1029,7 +1086,7 @@
 
 // This test verifies that when the callback passed to SetConnectedCallback()
 // returns `ERR_INCONSISTENT_IP_ADDRESS_SPACE`, the cache entry is invalidated.
-TEST_F(HttpCacheTest,
+TEST_P(HttpCacheTest,
        SimpleGET_ConnectedCallbackOnCacheHitReturnInconsistentIpError) {
   MockHttpCache cache;
 
@@ -1092,7 +1149,7 @@
 // returns
 // `ERR_CACHED_IP_ADDRESS_SPACE_BLOCKED_BY_LOCAL_NETWORK_ACCESS_POLICY`, the
 // cache entry is invalidated, and we'll retry the connection from the network.
-TEST_F(
+TEST_P(
     HttpCacheTest,
     SimpleGET_ConnectedCallbackOnCacheHitReturnLocalNetworkAccessBlockedError) {
   MockHttpCache cache;
@@ -1158,7 +1215,7 @@
 // This test verifies that the callback passed to SetConnectedCallback() is
 // called with the right transport type when the cached entry was originally
 // fetched via proxy.
-TEST_F(HttpCacheTest, SimpleGET_ConnectedCallbackOnCacheHitFromProxy) {
+TEST_P(HttpCacheTest, SimpleGET_ConnectedCallbackOnCacheHitFromProxy) {
   MockHttpCache cache;
 
   TransportInfo proxied_transport_info = TestTransportInfo();
@@ -1203,6 +1260,27 @@
               ElementsAre(expected_transport_info));
 }
 
+TEST_P(HttpCacheTest, SimpleGET_DelayedCacheLock) {
+  MockHttpCache cache;
+  LoadTimingInfo load_timing_info;
+
+  // Configure the cache to delay the response for AddTransactionToEntry so it
+  // gets sequenced behind any other tasks that get generated when starting the
+  // transaction (i.e. network activity when run in parallel with the cache
+  // lock).
+  cache.http_cache()->DelayAddTransactionToEntryForTesting();
+
+  // Write to the cache.
+  RunTransactionTestAndGetTiming(cache.http_cache(), kSimpleGET_Transaction,
+                                 NetLogWithSource::Make(NetLogSourceType::NONE),
+                                 &load_timing_info);
+
+  EXPECT_EQ(1, cache.network_layer()->transaction_count());
+  EXPECT_EQ(0, cache.disk_cache()->open_count());
+  EXPECT_EQ(1, cache.disk_cache()->create_count());
+  TestLoadTimingNetworkRequest(load_timing_info);
+}
+
 enum class SplitCacheTestCase {
   kSplitCacheDisabled,
   kSplitCacheNikFrameSiteEnabled,
@@ -1233,7 +1311,7 @@
 }
 
 class HttpCacheTest_SplitCacheFeature
-    : public HttpCacheTest,
+    : public HttpCacheTestBase,
       public ::testing::WithParamInterface<SplitCacheTestCase> {
  public:
   HttpCacheTest_SplitCacheFeature() {
@@ -1315,7 +1393,7 @@
       }
     });
 
-TEST_F(HttpCacheTest, SimpleGETNoDiskCache) {
+TEST_P(HttpCacheTest, SimpleGETNoDiskCache) {
   MockHttpCache cache;
 
   cache.disk_cache()->set_fail_requests(true);
@@ -1348,7 +1426,7 @@
   TestLoadTimingNetworkRequest(load_timing_info);
 }
 
-TEST_F(HttpCacheTest, SimpleGETNoDiskCache2) {
+TEST_P(HttpCacheTest, SimpleGETNoDiskCache2) {
   // This will initialize a cache object with NULL backend.
   auto factory = std::make_unique<MockBlockingBackendFactory>();
   factory->set_fail(true);
@@ -1363,7 +1441,7 @@
 }
 
 // Tests that IOBuffers are not referenced after IO completes.
-TEST_F(HttpCacheTest, ReleaseBuffer) {
+TEST_P(HttpCacheTest, ReleaseBuffer) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -1384,7 +1462,7 @@
   EXPECT_EQ(kBufferSize, cb.GetResult(rv));
 }
 
-TEST_F(HttpCacheTest, SimpleGETWithDiskFailures) {
+TEST_P(HttpCacheTest, SimpleGETWithDiskFailures) {
   MockHttpCache cache;
 
   cache.disk_cache()->set_soft_failures_mask(MockDiskEntry::FAIL_ALL);
@@ -1406,7 +1484,7 @@
 
 // Tests that disk failures after the transaction has started don't cause the
 // request to fail.
-TEST_F(HttpCacheTest, SimpleGETWithDiskFailures2) {
+TEST_P(HttpCacheTest, SimpleGETWithDiskFailures2) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -1443,7 +1521,7 @@
 }
 
 // Tests that we handle failures to read from the cache.
-TEST_F(HttpCacheTest, SimpleGETWithDiskFailures3) {
+TEST_P(HttpCacheTest, SimpleGETWithDiskFailures3) {
   MockHttpCache cache;
 
   // Read from the network, and write to the cache.
@@ -1479,7 +1557,7 @@
   EXPECT_EQ(3, cache.disk_cache()->create_count());
 }
 
-TEST_F(HttpCacheTest, SimpleGET_LoadOnlyFromCache_Hit) {
+TEST_P(HttpCacheTest, SimpleGET_LoadOnlyFromCache_Hit) {
   MockHttpCache cache;
 
   RecordingNetLogObserver net_log_observer;
@@ -1546,7 +1624,7 @@
   TestLoadTimingCachedResponse(load_timing_info);
 }
 
-TEST_F(HttpCacheTest, SimpleGET_LoadOnlyFromCache_Miss) {
+TEST_P(HttpCacheTest, SimpleGET_LoadOnlyFromCache_Miss) {
   MockHttpCache cache;
 
   // force this transaction to read from the cache
@@ -1571,7 +1649,7 @@
   EXPECT_EQ(0, cache.disk_cache()->create_count());
 }
 
-TEST_F(HttpCacheTest, SimpleGET_LoadPreferringCache_Hit) {
+TEST_P(HttpCacheTest, SimpleGET_LoadPreferringCache_Hit) {
   MockHttpCache cache;
 
   // write to the cache
@@ -1588,7 +1666,7 @@
   EXPECT_EQ(1, cache.disk_cache()->create_count());
 }
 
-TEST_F(HttpCacheTest, SimpleGET_LoadPreferringCache_Miss) {
+TEST_P(HttpCacheTest, SimpleGET_LoadPreferringCache_Miss) {
   MockHttpCache cache;
 
   // force this transaction to read from the cache if valid
@@ -1603,7 +1681,7 @@
 }
 
 // Tests LOAD_SKIP_CACHE_VALIDATION in the presence of vary headers.
-TEST_F(HttpCacheTest, SimpleGET_LoadPreferringCache_VaryMatch) {
+TEST_P(HttpCacheTest, SimpleGET_LoadPreferringCache_VaryMatch) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -1625,7 +1703,7 @@
 }
 
 // Tests LOAD_SKIP_CACHE_VALIDATION in the presence of vary headers.
-TEST_F(HttpCacheTest, SimpleGET_LoadPreferringCache_VaryMismatch) {
+TEST_P(HttpCacheTest, SimpleGET_LoadPreferringCache_VaryMismatch) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -1653,7 +1731,7 @@
 }
 
 // Tests that we honor Vary: * with LOAD_SKIP_CACHE_VALIDATION (crbug/778681)
-TEST_F(HttpCacheTest, SimpleGET_LoadSkipCacheValidation_VaryStar) {
+TEST_P(HttpCacheTest, SimpleGET_LoadSkipCacheValidation_VaryStar) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -1680,7 +1758,7 @@
 
 // Tests that was_cached was set properly on a failure, even if the cached
 // response wasn't returned.
-TEST_F(HttpCacheTest, SimpleGET_CacheSignal_Failure) {
+TEST_P(HttpCacheTest, SimpleGET_CacheSignal_Failure) {
   for (bool use_memory_entry_data : {false, true}) {
     MockHttpCache cache;
     cache.disk_cache()->set_support_in_memory_entry_data(use_memory_entry_data);
@@ -1731,7 +1809,7 @@
 // Tests that if the transaction is destroyed right after setting the
 // cache_entry_status_ as CANT_CONDITIONALIZE, then RecordHistograms should not
 // hit a dcheck.
-TEST_F(HttpCacheTest, RecordHistogramsCantConditionalize) {
+TEST_P(HttpCacheTest, RecordHistogramsCantConditionalize) {
   MockHttpCache cache;
   cache.disk_cache()->set_support_in_memory_entry_data(true);
 
@@ -1760,7 +1838,7 @@
 }
 
 // Confirm if we have an empty cache, a read is marked as network verified.
-TEST_F(HttpCacheTest, SimpleGET_NetworkAccessed_Network) {
+TEST_P(HttpCacheTest, SimpleGET_NetworkAccessed_Network) {
   MockHttpCache cache;
 
   // write to the cache
@@ -1778,7 +1856,7 @@
 
 // Confirm if we have a fresh entry in cache, it isn't marked as
 // network verified.
-TEST_F(HttpCacheTest, SimpleGET_NetworkAccessed_Cache) {
+TEST_P(HttpCacheTest, SimpleGET_NetworkAccessed_Cache) {
   MockHttpCache cache;
 
   // Prime cache.
@@ -1798,7 +1876,7 @@
   EXPECT_EQ(CacheEntryStatus::ENTRY_USED, response_info.cache_entry_status);
 }
 
-TEST_F(HttpCacheTest, SimpleGET_LoadBypassCache) {
+TEST_P(HttpCacheTest, SimpleGET_LoadBypassCache) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -1843,7 +1921,7 @@
   TestLoadTimingNetworkRequest(load_timing_info);
 }
 
-TEST_F(HttpCacheTest, SimpleGET_LoadBypassCache_Implicit) {
+TEST_P(HttpCacheTest, SimpleGET_LoadBypassCache_Implicit) {
   MockHttpCache cache;
 
   // write to the cache
@@ -1860,7 +1938,7 @@
   EXPECT_EQ(2, cache.disk_cache()->create_count());
 }
 
-TEST_F(HttpCacheTest, SimpleGET_LoadBypassCache_Implicit2) {
+TEST_P(HttpCacheTest, SimpleGET_LoadBypassCache_Implicit2) {
   MockHttpCache cache;
 
   // write to the cache
@@ -1877,7 +1955,7 @@
   EXPECT_EQ(2, cache.disk_cache()->create_count());
 }
 
-TEST_F(HttpCacheTest, SimpleGET_LoadValidateCache) {
+TEST_P(HttpCacheTest, SimpleGET_LoadValidateCache) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -1903,7 +1981,7 @@
   TestLoadTimingNetworkRequest(load_timing_info);
 }
 
-TEST_F(HttpCacheTest, SimpleGET_LoadValidateCache_Implicit) {
+TEST_P(HttpCacheTest, SimpleGET_LoadValidateCache_Implicit) {
   MockHttpCache cache;
 
   // write to the cache
@@ -1925,7 +2003,7 @@
 
 // Tests that |unused_since_prefetch| is updated accordingly (e.g. it is set to
 // true after a prefetch and set back to false when the prefetch is used).
-TEST_F(HttpCacheTest, SimpleGET_UnusedSincePrefetch) {
+TEST_P(HttpCacheTest, SimpleGET_UnusedSincePrefetch) {
   MockHttpCache cache;
   HttpResponseInfo response_info;
 
@@ -1971,7 +2049,7 @@
 // in HttpResponseInfo entries with the |restricted_prefetch| flag set. Also
 // tests that responses with |restricted_prefetch| flag set can only be used by
 // requests that have the LOAD_CAN_USE_RESTRICTED_PREFETCH load flag.
-TEST_F(HttpCacheTest, SimpleGET_RestrictedPrefetchIsRestrictedUntilReuse) {
+TEST_P(HttpCacheTest, SimpleGET_RestrictedPrefetchIsRestrictedUntilReuse) {
   MockHttpCache cache;
   HttpResponseInfo response_info;
 
@@ -2017,7 +2095,7 @@
   EXPECT_FALSE(response_info.network_accessed);
 }
 
-TEST_F(HttpCacheTest, SimpleGET_RestrictedPrefetchReuseIsLimited) {
+TEST_P(HttpCacheTest, SimpleGET_RestrictedPrefetchReuseIsLimited) {
   MockHttpCache cache;
   HttpResponseInfo response_info;
 
@@ -2053,7 +2131,7 @@
   EXPECT_FALSE(response_info.network_accessed);
 }
 
-TEST_F(HttpCacheTest, SimpleGET_UnusedSincePrefetchWriteError) {
+TEST_P(HttpCacheTest, SimpleGET_UnusedSincePrefetchWriteError) {
   MockHttpCache cache;
   HttpResponseInfo response_info;
 
@@ -2075,7 +2153,7 @@
 
 // Make sure that if a prefetch entry is truncated, then an attempt to re-use it
 // gets aborted in connected handler that truncated bit is not lost.
-TEST_F(HttpCacheTest, PrefetchTruncateCancelInConnectedCallback) {
+TEST_P(HttpCacheTest, PrefetchTruncateCancelInConnectedCallback) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -2152,7 +2230,7 @@
 // Make sure that if a stale-while-revalidate entry is truncated, then an
 // attempt to re-use it gets aborted in connected handler that truncated bit is
 // not lost.
-TEST_F(HttpCacheTest, StaleWhiteRevalidateTruncateCancelInConnectedCallback) {
+TEST_P(HttpCacheTest, StaleWhiteRevalidateTruncateCancelInConnectedCallback) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -2234,7 +2312,7 @@
 }
 
 // Tests that we don't remove extra headers for simple requests.
-TEST_F(HttpCacheTest, SimpleGET_PreserveRequestHeaders) {
+TEST_P(HttpCacheTest, SimpleGET_PreserveRequestHeaders) {
   for (bool use_memory_entry_data : {false, true}) {
     MockHttpCache cache;
     cache.disk_cache()->set_support_in_memory_entry_data(use_memory_entry_data);
@@ -2265,7 +2343,7 @@
 }
 
 // Tests that we don't remove extra headers for conditionalized requests.
-TEST_F(HttpCacheTest, ConditionalizedGET_PreserveRequestHeaders) {
+TEST_P(HttpCacheTest, ConditionalizedGET_PreserveRequestHeaders) {
   for (bool use_memory_entry_data : {false, true}) {
     MockHttpCache cache;
     // Unlike in SimpleGET_PreserveRequestHeaders, this entry can be
@@ -2289,7 +2367,7 @@
   }
 }
 
-TEST_F(HttpCacheTest, SimpleGET_ManyReaders) {
+TEST_P(HttpCacheTest, SimpleGET_ManyReaders) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -2351,7 +2429,7 @@
   EXPECT_EQ(1, cache.disk_cache()->create_count());
 }
 
-TEST_F(HttpCacheTest, RangeGET_FullAfterPartial) {
+TEST_P(HttpCacheTest, RangeGET_FullAfterPartial) {
   MockHttpCache cache;
 
   // Request a prefix.
@@ -2398,7 +2476,7 @@
 // Tests that when a range request transaction becomes a writer for the first
 // range and then fails conditionalization for the next range and decides to
 // doom the entry, then there should not be a dcheck assertion hit.
-TEST_F(HttpCacheTest, RangeGET_OverlappingRangesCouldntConditionalize) {
+TEST_P(HttpCacheTest, RangeGET_OverlappingRangesCouldntConditionalize) {
   MockHttpCache cache;
 
   {
@@ -2439,7 +2517,7 @@
   }
 }
 
-TEST_F(HttpCacheTest, RangeGET_FullAfterPartialReuse) {
+TEST_P(HttpCacheTest, RangeGET_FullAfterPartialReuse) {
   MockHttpCache cache;
 
   // Request a prefix.
@@ -2505,7 +2583,7 @@
 // This test verifies that the ConnectedCallback passed to a cache transaction
 // is called once per subrange in the case of a range request with a partial
 // cache hit.
-TEST_F(HttpCacheTest, RangeGET_ConnectedCallbackCalledForEachRange) {
+TEST_P(HttpCacheTest, RangeGET_ConnectedCallbackCalledForEachRange) {
   MockHttpCache cache;
 
   // Request an infix range and populate the cache with it.
@@ -2567,7 +2645,7 @@
 // This test verifies that when the ConnectedCallback passed to a cache range
 // transaction returns an `ERR_INCONSISTENT_IP_ADDRESS_SPACE` error during a
 // partial read from cache, then the cache entry is invalidated.
-TEST_F(HttpCacheTest, RangeGET_ConnectedCallbackReturnInconsistentIpError) {
+TEST_P(HttpCacheTest, RangeGET_ConnectedCallbackReturnInconsistentIpError) {
   MockHttpCache cache;
 
   // Request an infix range and populate the cache with it.
@@ -2658,7 +2736,7 @@
 // This test verifies that when the ConnectedCallback passed to a cache range
 // transaction returns an `ERR_INCONSISTENT_IP_ADDRESS_SPACE` error during a
 // network transaction, then the cache entry is invalidated.
-TEST_F(HttpCacheTest,
+TEST_P(HttpCacheTest,
        RangeGET_ConnectedCallbackReturnInconsistentIpErrorForNetwork) {
   MockHttpCache cache;
 
@@ -2746,7 +2824,7 @@
 // transaction returns an error for the second (or third) subrange transaction,
 // the overall cache transaction fails with that error. The cache entry is still
 // usable after that.
-TEST_F(HttpCacheTest, RangeGET_ConnectedCallbackReturnErrorSecondTime) {
+TEST_P(HttpCacheTest, RangeGET_ConnectedCallbackReturnErrorSecondTime) {
   MockHttpCache cache;
 
   // Request an infix range and populate the cache with it.
@@ -2842,7 +2920,7 @@
 // This test verifies that the ConnectedCallback passed to a cache transaction
 // is called once per subrange in the case of a range request with a partial
 // cache hit, even when a prefix of the range is cached.
-TEST_F(HttpCacheTest, RangeGET_ConnectedCallbackCalledForEachRangeWithPrefix) {
+TEST_P(HttpCacheTest, RangeGET_ConnectedCallbackCalledForEachRangeWithPrefix) {
   MockHttpCache cache;
 
   // Request a prefix range and populate the cache with it.
@@ -2894,7 +2972,7 @@
 
 // Tests that a range transaction is still usable even if it's unable to access
 // the cache.
-TEST_F(HttpCacheTest, RangeGET_FailedCacheAccess) {
+TEST_P(HttpCacheTest, RangeGET_FailedCacheAccess) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -2928,7 +3006,7 @@
 }
 
 // Tests that we can have parallel validation on range requests.
-TEST_F(HttpCacheTest, RangeGET_ParallelValidationNoMatch) {
+TEST_P(HttpCacheTest, RangeGET_ParallelValidationNoMatch) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -2991,7 +3069,7 @@
 // Tests that if a transaction is dooming the entry and the entry was doomed by
 // another transaction that was not part of the entry and created a new entry,
 // the new entry should not be incorrectly doomed. (crbug.com/736993)
-TEST_F(HttpCacheTest, RangeGET_ParallelValidationNoMatchDoomEntry) {
+TEST_P(HttpCacheTest, RangeGET_ParallelValidationNoMatchDoomEntry) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -3077,7 +3155,7 @@
 
 // Same as above but tests that the 2nd transaction does not do anything if
 // there is nothing to doom. (crbug.com/736993)
-TEST_F(HttpCacheTest, RangeGET_ParallelValidationNoMatchDoomEntry1) {
+TEST_P(HttpCacheTest, RangeGET_ParallelValidationNoMatchDoomEntry1) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -3165,7 +3243,7 @@
 }
 
 // Tests parallel validation on range requests with non-overlapping ranges.
-TEST_F(HttpCacheTest, RangeGET_ParallelValidationDifferentRanges) {
+TEST_P(HttpCacheTest, RangeGET_ParallelValidationDifferentRanges) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -3268,7 +3346,7 @@
 }
 
 // Tests that a request does not create Writers when readers is not empty.
-TEST_F(HttpCacheTest, RangeGET_DoNotCreateWritersWhenReaderExists) {
+TEST_P(HttpCacheTest, RangeGET_DoNotCreateWritersWhenReaderExists) {
   MockHttpCache cache;
 
   // Save a request in the cache so that the next request can become a
@@ -3314,7 +3392,7 @@
 
 // Tests parallel validation on range requests can be successfully restarted
 // when there is a cache lock timeout.
-TEST_F(HttpCacheTest, RangeGET_ParallelValidationCacheLockTimeout) {
+TEST_P(HttpCacheTest, RangeGET_ParallelValidationCacheLockTimeout) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -3403,7 +3481,7 @@
 // Tests a full request and a simultaneous range request and the range request
 // dooms the entry created by the full request due to not being able to
 // conditionalize.
-TEST_F(HttpCacheTest, RangeGET_ParallelValidationCouldntConditionalize) {
+TEST_P(HttpCacheTest, RangeGET_ParallelValidationCouldntConditionalize) {
   MockHttpCache cache;
 
   MockTransaction mock_transaction(kSimpleGET_Transaction);
@@ -3489,7 +3567,7 @@
 
 // Tests a 200 request and a simultaneous range request where conditionalization
 // is possible.
-TEST_F(HttpCacheTest, RangeGET_ParallelValidationCouldConditionalize) {
+TEST_P(HttpCacheTest, RangeGET_ParallelValidationCouldConditionalize) {
   MockHttpCache cache;
 
   MockTransaction mock_transaction(kSimpleGET_Transaction);
@@ -3573,7 +3651,7 @@
 }
 
 // Tests parallel validation on range requests with overlapping ranges.
-TEST_F(HttpCacheTest, RangeGET_ParallelValidationOverlappingRanges) {
+TEST_P(HttpCacheTest, RangeGET_ParallelValidationOverlappingRanges) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -3676,7 +3754,7 @@
 
 // Tests parallel validation on range requests with overlapping ranges and the
 // impact of deleting the writer on transactions that have validated.
-TEST_F(HttpCacheTest, RangeGET_ParallelValidationRestartDoneHeaders) {
+TEST_P(HttpCacheTest, RangeGET_ParallelValidationRestartDoneHeaders) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -3774,7 +3852,7 @@
 }
 
 // A test of doing a range request to a cached 301 response
-TEST_F(HttpCacheTest, RangeGET_CachedRedirect) {
+TEST_P(HttpCacheTest, RangeGET_CachedRedirect) {
   RangeTransactionServer handler;
   handler.set_redirect(true);
 
@@ -3874,7 +3952,7 @@
 // A transaction that fails to validate an entry, while attempting to write
 // the response, should still get data to its consumer even if the attempt to
 // create a new entry fails.
-TEST_F(HttpCacheTest, SimpleGET_ValidationFailureWithCreateFailure) {
+TEST_P(HttpCacheTest, SimpleGET_ValidationFailureWithCreateFailure) {
   MockHttpCache cache;
   MockHttpRequest request(kSimpleGET_Transaction);
   request.load_flags |= LOAD_VALIDATE_CACHE;
@@ -3935,7 +4013,7 @@
 }
 
 // Parallel validation results in 200.
-TEST_F(HttpCacheTest, SimpleGET_ParallelValidationNoMatch) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelValidationNoMatch) {
   MockHttpCache cache;
   MockHttpRequest request(kSimpleGET_Transaction);
   request.load_flags |= LOAD_VALIDATE_CACHE;
@@ -3985,7 +4063,7 @@
   EXPECT_EQ(5, cache.disk_cache()->create_count());
 }
 
-TEST_F(HttpCacheTest, RangeGET_Enormous) {
+TEST_P(HttpCacheTest, RangeGET_Enormous) {
   // Test for how blockfile's limit on range namespace interacts with
   // HttpCache::Transaction.
   // See https://crbug.com/770694
@@ -4054,7 +4132,7 @@
 
 // Parallel validation results in 200 for 1 transaction and validation matches
 // for subsequent transactions.
-TEST_F(HttpCacheTest, SimpleGET_ParallelValidationNoMatch1) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelValidationNoMatch1) {
   MockHttpCache cache;
   MockHttpRequest request(kSimpleGET_Transaction);
 
@@ -4115,7 +4193,7 @@
 
 // Tests that a GET followed by a DELETE results in DELETE immediately starting
 // the headers phase and the entry is doomed.
-TEST_F(HttpCacheTest, SimpleGET_ParallelValidationDelete) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelValidationDelete) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -4179,7 +4257,7 @@
 
 // Tests that a transaction which is in validated queue can be destroyed without
 // any impact to other transactions.
-TEST_F(HttpCacheTest, SimpleGET_ParallelValidationCancelValidated) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelValidationCancelValidated) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -4233,7 +4311,7 @@
 
 // Tests that an idle writer transaction can be deleted without impacting the
 // existing writers.
-TEST_F(HttpCacheTest, SimpleGET_ParallelWritingCancelIdleTransaction) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelWritingCancelIdleTransaction) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -4281,7 +4359,7 @@
 
 // Tests that a transaction which is in validated queue can timeout and start
 // the headers phase again.
-TEST_F(HttpCacheTest, SimpleGET_ParallelValidationValidatedTimeout) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelValidationValidatedTimeout) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -4336,7 +4414,7 @@
 
 // Tests that a transaction which is in readers can be destroyed without
 // any impact to other transactions.
-TEST_F(HttpCacheTest, SimpleGET_ParallelValidationCancelReader) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelValidationCancelReader) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -4433,7 +4511,7 @@
 
 // Tests that when the only writer goes away, it immediately cleans up rather
 // than wait for the network request to finish. See https://crbug.com/804868.
-TEST_F(HttpCacheTest, SimpleGET_HangingCacheWriteCleanup) {
+TEST_P(HttpCacheTest, SimpleGET_HangingCacheWriteCleanup) {
   MockHttpCache mock_cache;
   MockHttpRequest request(kSimpleGET_Transaction);
 
@@ -4476,7 +4554,7 @@
 // Tests that a transaction writer can be destroyed mid-read.
 // A waiting for read transaction should be able to read the data that was
 // driven by the Read started by the cancelled writer.
-TEST_F(HttpCacheTest, SimpleGET_ParallelWritingCancelWriter) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelWritingCancelWriter) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -4565,7 +4643,7 @@
 
 // Tests the case when network read failure happens. Idle and waiting
 // transactions should fail and headers transaction should be restarted.
-TEST_F(HttpCacheTest, SimpleGET_ParallelWritingNetworkReadFailed) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelWritingNetworkReadFailed) {
   MockHttpCache cache;
 
   ScopedMockTransaction fail_transaction(kSimpleGET_Transaction);
@@ -4645,7 +4723,7 @@
 
 // Tests the case when cache write failure happens. Idle and waiting
 // transactions should fail and headers transaction should be restarted.
-TEST_F(HttpCacheTest, SimpleGET_ParallelWritingCacheWriteFailed) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelWritingCacheWriteFailed) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -4742,7 +4820,7 @@
 // TODO(shivanisha) Testing this because it is allowed by the code but looks
 // like the code should disallow two POSTs without LOAD_ONLY_FROM_CACHE with the
 // same upload data identifier to map to the same entry.
-TEST_F(HttpCacheTest, SimplePOST_ParallelWritingDisallowed) {
+TEST_P(HttpCacheTest, SimplePOST_ParallelWritingDisallowed) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimplePOST_Transaction);
@@ -4803,7 +4881,7 @@
 
 // Tests the case when parallel writing succeeds. Tests both idle and waiting
 // transactions.
-TEST_F(HttpCacheTest, SimpleGET_ParallelWritingSuccess) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelWritingSuccess) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -4886,7 +4964,7 @@
 
 // Tests the case when parallel writing involves things bigger than what cache
 // can store. In this case, the best we can do is re-fetch it.
-TEST_F(HttpCacheTest, SimpleGET_ParallelWritingHuge) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelWritingHuge) {
   MockHttpCache cache;
   cache.disk_cache()->set_max_file_size(10);
 
@@ -4959,7 +5037,7 @@
 // transaction that created the network transaction becomes a reader. Also
 // verifies that the network bytes are only attributed to the transaction that
 // created the network transaction.
-TEST_F(HttpCacheTest, SimpleGET_ParallelWritingVerifyNetworkBytes) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelWritingVerifyNetworkBytes) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -5012,7 +5090,7 @@
 }
 
 // Tests than extra Read from the consumer should not hang/crash the browser.
-TEST_F(HttpCacheTest, SimpleGET_ExtraRead) {
+TEST_P(HttpCacheTest, SimpleGET_ExtraRead) {
   MockHttpCache cache;
   MockHttpRequest request(kSimpleGET_Transaction);
   Context c;
@@ -5044,7 +5122,7 @@
 
 // Tests when a writer is destroyed mid-read, all the other writer transactions
 // can continue writing to the entry.
-TEST_F(HttpCacheTest, SimpleGET_ParallelValidationCancelWriter) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelValidationCancelWriter) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -5109,7 +5187,7 @@
 
 // Tests that when StopCaching is invoked on a writer, dependent transactions
 // are restarted.
-TEST_F(HttpCacheTest, SimpleGET_ParallelValidationStopCaching) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelValidationStopCaching) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -5164,7 +5242,7 @@
 
 // Tests that when StopCaching is invoked on a writer transaction, it is a
 // no-op if there are other writer transactions.
-TEST_F(HttpCacheTest, SimpleGET_ParallelWritersStopCachingNoOp) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelWritersStopCachingNoOp) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -5230,7 +5308,7 @@
 
 // Tests that a transaction is currently in headers phase and is destroyed
 // leading to destroying the entry.
-TEST_F(HttpCacheTest, SimpleGET_ParallelValidationCancelHeaders) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelValidationCancelHeaders) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -5281,7 +5359,7 @@
 
 // Similar to the above test, except here cache write fails and the
 // validated transactions should be restarted.
-TEST_F(HttpCacheTest, SimpleGET_ParallelWritersFailWrite) {
+TEST_P(HttpCacheTest, SimpleGET_ParallelWritersFailWrite) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -5358,7 +5436,7 @@
 // If cancelling a request is racing with another request for the same resource
 // finishing, we have to make sure that we remove both transactions from the
 // entry.
-TEST_F(HttpCacheTest, SimpleGET_RacingReaders) {
+TEST_P(HttpCacheTest, SimpleGET_RacingReaders) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -5437,7 +5515,7 @@
 // Tests that we can doom an entry with pending transactions and delete one of
 // the pending transactions before the first one completes.
 // See http://code.google.com/p/chromium/issues/detail?id=25588
-TEST_F(HttpCacheTest, SimpleGET_DoomWithPending) {
+TEST_P(HttpCacheTest, SimpleGET_DoomWithPending) {
   // We need simultaneous doomed / not_doomed entries so let's use a real cache.
   MockHttpCache cache(HttpCache::DefaultBackend::InMemory(1024 * 1024));
 
@@ -5485,7 +5563,7 @@
   }
 }
 
-TEST_F(HttpCacheTest, DoomDoesNotSetHints) {
+TEST_P(HttpCacheTest, DoomDoesNotSetHints) {
   // Test that a doomed writer doesn't set in-memory index hints.
   MockHttpCache cache;
   cache.disk_cache()->set_support_in_memory_entry_data(true);
@@ -5559,7 +5637,7 @@
 // This is a test for http://code.google.com/p/chromium/issues/detail?id=4731.
 // We may attempt to delete an entry synchronously with the act of adding a new
 // transaction to said entry.
-TEST_F(HttpCacheTest, FastNoStoreGET_DoneWithPending) {
+TEST_P(HttpCacheTest, FastNoStoreGET_DoneWithPending) {
   MockHttpCache cache;
 
   // The headers will be served right from the call to Start() the request.
@@ -5610,7 +5688,7 @@
   RemoveMockTransaction(&kFastNoStoreGET_Transaction);
 }
 
-TEST_F(HttpCacheTest, SimpleGET_ManyWriters_CancelFirst) {
+TEST_P(HttpCacheTest, SimpleGET_ManyWriters_CancelFirst) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -5667,7 +5745,7 @@
 
 // Tests that we can cancel requests that are queued waiting to open the disk
 // cache entry.
-TEST_F(HttpCacheTest, SimpleGET_ManyWriters_CancelCreate) {
+TEST_P(HttpCacheTest, SimpleGET_ManyWriters_CancelCreate) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -5717,7 +5795,7 @@
 }
 
 // Tests that we can cancel a single request to open a disk cache entry.
-TEST_F(HttpCacheTest, SimpleGET_CancelCreate) {
+TEST_P(HttpCacheTest, SimpleGET_CancelCreate) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -5741,7 +5819,7 @@
 }
 
 // Tests that we delete/create entries even if multiple requests are queued.
-TEST_F(HttpCacheTest, SimpleGET_ManyWriters_BypassCache) {
+TEST_P(HttpCacheTest, SimpleGET_ManyWriters_BypassCache) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -5784,7 +5862,7 @@
 
 // Tests that a (simulated) timeout allows transactions waiting on the cache
 // lock to continue.
-TEST_F(HttpCacheTest, SimpleGET_WriterTimeout) {
+TEST_P(HttpCacheTest, SimpleGET_WriterTimeout) {
   MockHttpCache cache;
   cache.SimulateCacheLockTimeout();
 
@@ -5809,7 +5887,7 @@
 
 // Tests that a (simulated) timeout allows transactions waiting on the cache
 // lock to continue but read only transactions to error out.
-TEST_F(HttpCacheTest, SimpleGET_WriterTimeoutReadOnlyError) {
+TEST_P(HttpCacheTest, SimpleGET_WriterTimeoutReadOnlyError) {
   MockHttpCache cache;
 
   // Simulate timeout.
@@ -5835,7 +5913,7 @@
   ReadAndVerifyTransaction(c1.trans.get(), kSimpleGET_Transaction);
 }
 
-TEST_F(HttpCacheTest, SimpleGET_AbandonedCacheRead) {
+TEST_P(HttpCacheTest, SimpleGET_AbandonedCacheRead) {
   MockHttpCache cache;
 
   // write to the cache
@@ -5866,7 +5944,7 @@
 
 // Tests that we can delete the HttpCache and deal with queued transactions
 // ("waiting for the backend" as opposed to Active or Doomed entries).
-TEST_F(HttpCacheTest, SimpleGET_ManyWriters_DeleteCache) {
+TEST_P(HttpCacheTest, SimpleGET_ManyWriters_DeleteCache) {
   auto cache = std::make_unique<MockHttpCache>(
       std::make_unique<MockBackendNoCbFactory>());
 
@@ -5897,7 +5975,7 @@
 }
 
 // Tests that we queue requests when initializing the backend.
-TEST_F(HttpCacheTest, SimpleGET_WaitForBackend) {
+TEST_P(HttpCacheTest, SimpleGET_WaitForBackend) {
   auto factory = std::make_unique<MockBlockingBackendFactory>();
   MockBlockingBackendFactory* factory_ptr = factory.get();
   MockHttpCache cache(std::move(factory));
@@ -5944,7 +6022,7 @@
 
 // Tests that we can cancel requests that are queued waiting for the backend
 // to be initialized.
-TEST_F(HttpCacheTest, SimpleGET_WaitForBackend_CancelCreate) {
+TEST_P(HttpCacheTest, SimpleGET_WaitForBackend_CancelCreate) {
   auto factory = std::make_unique<MockBlockingBackendFactory>();
   MockBlockingBackendFactory* factory_ptr = factory.get();
   MockHttpCache cache(std::move(factory));
@@ -5995,7 +6073,7 @@
 }
 
 // Tests that we can delete the HttpCache while creating the backend.
-TEST_F(HttpCacheTest, DeleteCacheWaitingForBackend) {
+TEST_P(HttpCacheTest, DeleteCacheWaitingForBackend) {
   auto factory = std::make_unique<MockBlockingBackendFactory>();
   MockBlockingBackendFactory* factory_ptr = factory.get();
   auto cache = std::make_unique<MockHttpCache>(std::move(factory));
@@ -6028,7 +6106,7 @@
 
 // Tests that we can delete the cache while creating the backend, from within
 // one of the callbacks.
-TEST_F(HttpCacheTest, DeleteCacheWaitingForBackend2) {
+TEST_P(HttpCacheTest, DeleteCacheWaitingForBackend2) {
   auto factory = std::make_unique<MockBlockingBackendFactory>();
   MockBlockingBackendFactory* factory_ptr = factory.get();
   auto cache = std::make_unique<MockHttpCache>(std::move(factory));
@@ -6069,7 +6147,7 @@
   EXPECT_FALSE(cb2.have_result());
 }
 
-TEST_F(HttpCacheTest, TypicalGET_ConditionalRequest) {
+TEST_P(HttpCacheTest, TypicalGET_ConditionalRequest) {
   MockHttpCache cache;
 
   // write to the cache
@@ -6103,7 +6181,7 @@
   response_data->clear();
 }
 
-TEST_F(HttpCacheTest, ETagGET_ConditionalRequest_304) {
+TEST_P(HttpCacheTest, ETagGET_ConditionalRequest_304) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kETagGET_Transaction);
@@ -6179,7 +6257,7 @@
 }
 
 // Tests revalidation after a vary match.
-TEST_F(HttpCacheTest, GET_ValidateCache_VaryMatch) {
+TEST_P(HttpCacheTest, GET_ValidateCache_VaryMatch) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -6212,7 +6290,7 @@
 }
 
 // Tests revalidation after a vary mismatch if etag is present.
-TEST_F(HttpCacheTest, GET_ValidateCache_VaryMismatch) {
+TEST_P(HttpCacheTest, GET_ValidateCache_VaryMismatch) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -6246,7 +6324,7 @@
 }
 
 // Tests revalidation after a vary mismatch due to vary: * if etag is present.
-TEST_F(HttpCacheTest, GET_ValidateCache_VaryMismatchStar) {
+TEST_P(HttpCacheTest, GET_ValidateCache_VaryMismatchStar) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -6278,7 +6356,7 @@
 }
 
 // Tests lack of revalidation after a vary mismatch and no etag.
-TEST_F(HttpCacheTest, GET_DontValidateCache_VaryMismatch) {
+TEST_P(HttpCacheTest, GET_DontValidateCache_VaryMismatch) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -6311,7 +6389,7 @@
 }
 
 // Tests that a new vary header provided when revalidating an entry is saved.
-TEST_F(HttpCacheTest, GET_ValidateCache_VaryMatch_UpdateVary) {
+TEST_P(HttpCacheTest, GET_ValidateCache_VaryMatch_UpdateVary) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -6350,7 +6428,7 @@
 
 // Tests that new request headers causing a vary mismatch are paired with the
 // new response when the server says the old response can be used.
-TEST_F(HttpCacheTest, GET_ValidateCache_VaryMismatch_UpdateRequestHeader) {
+TEST_P(HttpCacheTest, GET_ValidateCache_VaryMismatch_UpdateRequestHeader) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -6385,7 +6463,7 @@
 
 // Tests that a 304 without vary headers doesn't delete the previously stored
 // vary data after a vary match revalidation.
-TEST_F(HttpCacheTest, GET_ValidateCache_VaryMatch_DontDeleteVary) {
+TEST_P(HttpCacheTest, GET_ValidateCache_VaryMatch_DontDeleteVary) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -6422,7 +6500,7 @@
 
 // Tests that a 304 without vary headers doesn't delete the previously stored
 // vary data after a vary mismatch.
-TEST_F(HttpCacheTest, GET_ValidateCache_VaryMismatch_DontDeleteVary) {
+TEST_P(HttpCacheTest, GET_ValidateCache_VaryMismatch_DontDeleteVary) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -6466,7 +6544,7 @@
       request->extra_headers.HasHeader(HttpRequestHeaders::kIfNoneMatch));
 }
 
-TEST_F(HttpCacheTest, ETagGET_Http10) {
+TEST_P(HttpCacheTest, ETagGET_Http10) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kETagGET_Transaction);
@@ -6489,7 +6567,7 @@
   EXPECT_EQ(1, cache.disk_cache()->create_count());
 }
 
-TEST_F(HttpCacheTest, ETagGET_Http10_Range) {
+TEST_P(HttpCacheTest, ETagGET_Http10_Range) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kETagGET_Transaction);
@@ -6525,7 +6603,7 @@
   response_data->clear();
 }
 
-TEST_F(HttpCacheTest, ETagGET_ConditionalRequest_304_NoStore) {
+TEST_P(HttpCacheTest, ETagGET_ConditionalRequest_304_NoStore) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kETagGET_Transaction);
@@ -6669,7 +6747,7 @@
 
 // Check that when an "if-modified-since" header is attached
 // to the request, the result still updates the cached entry.
-TEST_F(HttpCacheTest, ConditionalizedRequestUpdatesCache1) {
+TEST_P(HttpCacheTest, ConditionalizedRequestUpdatesCache1) {
   // First network response for |kUrl|.
   static const Response kNetResponse1 = {
     "HTTP/1.1 200 OK",
@@ -6695,7 +6773,7 @@
 
 // Check that when an "if-none-match" header is attached
 // to the request, the result updates the cached entry.
-TEST_F(HttpCacheTest, ConditionalizedRequestUpdatesCache2) {
+TEST_P(HttpCacheTest, ConditionalizedRequestUpdatesCache2) {
   // First network response for |kUrl|.
   static const Response kNetResponse1 = {
     "HTTP/1.1 200 OK",
@@ -6723,7 +6801,7 @@
 // Check that when an "if-modified-since" header is attached
 // to a request, the 304 (not modified result) result updates the cached
 // headers, and the 304 response is returned rather than the cached response.
-TEST_F(HttpCacheTest, ConditionalizedRequestUpdatesCache3) {
+TEST_P(HttpCacheTest, ConditionalizedRequestUpdatesCache3) {
   // First network response for |kUrl|.
   static const Response kNetResponse1 = {
     "HTTP/1.1 200 OK",
@@ -6760,7 +6838,7 @@
 // Test that when doing an externally conditionalized if-modified-since
 // and there is no corresponding cache entry, a new cache entry is NOT
 // created (304 response).
-TEST_F(HttpCacheTest, ConditionalizedRequestUpdatesCache4) {
+TEST_P(HttpCacheTest, ConditionalizedRequestUpdatesCache4) {
   MockHttpCache cache;
 
   const char kUrl[] = "http://foobar.com/main.css";
@@ -6804,7 +6882,7 @@
 // Test that when doing an externally conditionalized if-modified-since
 // and there is no corresponding cache entry, a new cache entry is NOT
 // created (200 response).
-TEST_F(HttpCacheTest, ConditionalizedRequestUpdatesCache5) {
+TEST_P(HttpCacheTest, ConditionalizedRequestUpdatesCache5) {
   MockHttpCache cache;
 
   const char kUrl[] = "http://foobar.com/main.css";
@@ -6849,7 +6927,7 @@
 // if the date does not match the cache entry's last-modified date,
 // then we do NOT use the response (304) to update the cache.
 // (the if-modified-since date is 2 days AFTER the cache's modification date).
-TEST_F(HttpCacheTest, ConditionalizedRequestUpdatesCache6) {
+TEST_P(HttpCacheTest, ConditionalizedRequestUpdatesCache6) {
   static const Response kNetResponse1 = {
     "HTTP/1.1 200 OK",
     "Date: Fri, 12 Jun 2009 21:46:42 GMT\n"
@@ -6879,7 +6957,7 @@
 // Test that when doing an externally conditionalized if-none-match
 // if the etag does not match the cache entry's etag, then we do not use the
 // response (304) to update the cache.
-TEST_F(HttpCacheTest, ConditionalizedRequestUpdatesCache7) {
+TEST_P(HttpCacheTest, ConditionalizedRequestUpdatesCache7) {
   static const Response kNetResponse1 = {
     "HTTP/1.1 200 OK",
     "Date: Fri, 12 Jun 2009 21:46:42 GMT\n"
@@ -6906,7 +6984,7 @@
 
 // Test that doing an externally conditionalized request with both if-none-match
 // and if-modified-since updates the cache.
-TEST_F(HttpCacheTest, ConditionalizedRequestUpdatesCache8) {
+TEST_P(HttpCacheTest, ConditionalizedRequestUpdatesCache8) {
   static const Response kNetResponse1 = {
     "HTTP/1.1 200 OK",
     "Date: Fri, 12 Jun 2009 21:46:42 GMT\n"
@@ -6934,7 +7012,7 @@
 
 // Test that doing an externally conditionalized request with both if-none-match
 // and if-modified-since does not update the cache with only one match.
-TEST_F(HttpCacheTest, ConditionalizedRequestUpdatesCache9) {
+TEST_P(HttpCacheTest, ConditionalizedRequestUpdatesCache9) {
   static const Response kNetResponse1 = {
     "HTTP/1.1 200 OK",
     "Date: Fri, 12 Jun 2009 21:46:42 GMT\n"
@@ -6963,7 +7041,7 @@
 
 // Test that doing an externally conditionalized request with both if-none-match
 // and if-modified-since does not update the cache with only one match.
-TEST_F(HttpCacheTest, ConditionalizedRequestUpdatesCache10) {
+TEST_P(HttpCacheTest, ConditionalizedRequestUpdatesCache10) {
   static const Response kNetResponse1 = {
     "HTTP/1.1 200 OK",
     "Date: Fri, 12 Jun 2009 21:46:42 GMT\n"
@@ -6990,7 +7068,7 @@
       kNetResponse1, kNetResponse2, kNetResponse1, kExtraRequestHeaders);
 }
 
-TEST_F(HttpCacheTest, UrlContainingHash) {
+TEST_P(HttpCacheTest, UrlContainingHash) {
   MockHttpCache cache;
 
   // Do a typical GET request -- should write an entry into our cache.
@@ -7016,7 +7094,7 @@
 
 // Tests that we skip the cache for POST requests that do not have an upload
 // identifier.
-TEST_F(HttpCacheTest, SimplePOST_SkipsCache) {
+TEST_P(HttpCacheTest, SimplePOST_SkipsCache) {
   MockHttpCache cache;
 
   RunTransactionTest(cache.http_cache(), kSimplePOST_Transaction);
@@ -7027,7 +7105,7 @@
 }
 
 // Tests POST handling with a disabled cache (no DCHECK).
-TEST_F(HttpCacheTest, SimplePOST_DisabledCache) {
+TEST_P(HttpCacheTest, SimplePOST_DisabledCache) {
   MockHttpCache cache;
   cache.http_cache()->set_mode(HttpCache::Mode::DISABLE);
 
@@ -7038,7 +7116,7 @@
   EXPECT_EQ(0, cache.disk_cache()->create_count());
 }
 
-TEST_F(HttpCacheTest, SimplePOST_LoadOnlyFromCache_Miss) {
+TEST_P(HttpCacheTest, SimplePOST_LoadOnlyFromCache_Miss) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimplePOST_Transaction);
@@ -7061,7 +7139,7 @@
   EXPECT_EQ(0, cache.disk_cache()->create_count());
 }
 
-TEST_F(HttpCacheTest, SimplePOST_LoadOnlyFromCache_Hit) {
+TEST_P(HttpCacheTest, SimplePOST_LoadOnlyFromCache_Hit) {
   MockHttpCache cache;
 
   // Test that we hit the cache for POST requests.
@@ -7097,7 +7175,7 @@
 }
 
 // Test that we don't hit the cache for POST requests if there is a byte range.
-TEST_F(HttpCacheTest, SimplePOST_WithRanges) {
+TEST_P(HttpCacheTest, SimplePOST_WithRanges) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimplePOST_Transaction);
@@ -7124,7 +7202,7 @@
 }
 
 // Tests that a POST is cached separately from a GET.
-TEST_F(HttpCacheTest, SimplePOST_SeparateCache) {
+TEST_P(HttpCacheTest, SimplePOST_SeparateCache) {
   MockHttpCache cache;
 
   std::vector<std::unique_ptr<UploadElementReader>> element_readers;
@@ -7153,7 +7231,7 @@
 }
 
 // Tests that a successful POST invalidates a previously cached GET.
-TEST_F(HttpCacheTest, SimplePOST_Invalidate_205) {
+TEST_P(HttpCacheTest, SimplePOST_Invalidate_205) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -7260,7 +7338,7 @@
 
 // Tests that a successful POST invalidates a previously cached GET, even when
 // there is no upload identifier.
-TEST_F(HttpCacheTest, SimplePOST_NoUploadId_Invalidate_205) {
+TEST_P(HttpCacheTest, SimplePOST_NoUploadId_Invalidate_205) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -7299,7 +7377,7 @@
 }
 
 // Tests that processing a POST before creating the backend doesn't crash.
-TEST_F(HttpCacheTest, SimplePOST_NoUploadId_NoBackend) {
+TEST_P(HttpCacheTest, SimplePOST_NoUploadId_NoBackend) {
   // This will initialize a cache object with NULL backend.
   auto factory = std::make_unique<MockBlockingBackendFactory>();
   factory->set_fail(true);
@@ -7322,7 +7400,7 @@
 }
 
 // Tests that we don't invalidate entries as a result of a failed POST.
-TEST_F(HttpCacheTest, SimplePOST_DontInvalidate_100) {
+TEST_P(HttpCacheTest, SimplePOST_DontInvalidate_100) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -7361,7 +7439,7 @@
 }
 
 // Tests that a HEAD request is not cached by itself.
-TEST_F(HttpCacheTest, SimpleHEAD_LoadOnlyFromCache_Miss) {
+TEST_P(HttpCacheTest, SimpleHEAD_LoadOnlyFromCache_Miss) {
   MockHttpCache cache;
   MockTransaction transaction(kSimplePOST_Transaction);
   AddMockTransaction(&transaction);
@@ -7387,7 +7465,7 @@
 }
 
 // Tests that a HEAD request is served from a cached GET.
-TEST_F(HttpCacheTest, SimpleHEAD_LoadOnlyFromCache_Hit) {
+TEST_P(HttpCacheTest, SimpleHEAD_LoadOnlyFromCache_Hit) {
   MockHttpCache cache;
   MockTransaction transaction(kSimpleGET_Transaction);
   AddMockTransaction(&transaction);
@@ -7412,7 +7490,7 @@
 }
 
 // Tests that a read-only request served from the cache preserves CL.
-TEST_F(HttpCacheTest, SimpleHEAD_ContentLengthOnHit_Read) {
+TEST_P(HttpCacheTest, SimpleHEAD_ContentLengthOnHit_Read) {
   MockHttpCache cache;
   MockTransaction transaction(kSimpleGET_Transaction);
   AddMockTransaction(&transaction);
@@ -7434,7 +7512,7 @@
 }
 
 // Tests that a read-write request served from the cache preserves CL.
-TEST_F(HttpCacheTest, ETagHEAD_ContentLengthOnHit_ReadWrite) {
+TEST_P(HttpCacheTest, ETagHEAD_ContentLengthOnHit_ReadWrite) {
   MockHttpCache cache;
   MockTransaction transaction(kETagGET_Transaction);
   AddMockTransaction(&transaction);
@@ -7457,7 +7535,7 @@
 }
 
 // Tests that a HEAD request that includes byte ranges bypasses the cache.
-TEST_F(HttpCacheTest, SimpleHEAD_WithRanges) {
+TEST_P(HttpCacheTest, SimpleHEAD_WithRanges) {
   MockHttpCache cache;
   MockTransaction transaction(kSimpleGET_Transaction);
   AddMockTransaction(&transaction);
@@ -7477,8 +7555,8 @@
   RemoveMockTransaction(&transaction);
 }
 
-// Tests that a HEAD request can be served from a partialy cached resource.
-TEST_F(HttpCacheTest, SimpleHEAD_WithCachedRanges) {
+// Tests that a HEAD request can be served from a partially cached resource.
+TEST_P(HttpCacheTest, SimpleHEAD_WithCachedRanges) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -7507,7 +7585,7 @@
 }
 
 // Tests that a HEAD request can be served from a truncated resource.
-TEST_F(HttpCacheTest, SimpleHEAD_WithTruncatedEntry) {
+TEST_P(HttpCacheTest, SimpleHEAD_WithTruncatedEntry) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -7540,7 +7618,7 @@
 }
 
 // Tests that a HEAD request updates the cached response.
-TEST_F(HttpCacheTest, TypicalHEAD_UpdatesResponse) {
+TEST_P(HttpCacheTest, TypicalHEAD_UpdatesResponse) {
   MockHttpCache cache;
   MockTransaction transaction(kTypicalGET_Transaction);
   AddMockTransaction(&transaction);
@@ -7578,7 +7656,7 @@
 }
 
 // Tests that an externally conditionalized HEAD request updates the cache.
-TEST_F(HttpCacheTest, TypicalHEAD_ConditionalizedRequestUpdatesResponse) {
+TEST_P(HttpCacheTest, TypicalHEAD_ConditionalizedRequestUpdatesResponse) {
   MockHttpCache cache;
   MockTransaction transaction(kTypicalGET_Transaction);
   AddMockTransaction(&transaction);
@@ -7618,7 +7696,7 @@
 }
 
 // Tests that a HEAD request invalidates an old cached entry.
-TEST_F(HttpCacheTest, SimpleHEAD_InvalidatesEntry) {
+TEST_P(HttpCacheTest, SimpleHEAD_InvalidatesEntry) {
   MockHttpCache cache;
   MockTransaction transaction(kTypicalGET_Transaction);
   AddMockTransaction(&transaction);
@@ -7642,7 +7720,7 @@
 }
 
 // Tests that we do not cache the response of a PUT.
-TEST_F(HttpCacheTest, SimplePUT_Miss) {
+TEST_P(HttpCacheTest, SimplePUT_Miss) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimplePOST_Transaction);
@@ -7666,7 +7744,7 @@
 }
 
 // Tests that we invalidate entries as a result of a PUT.
-TEST_F(HttpCacheTest, SimplePUT_Invalidate) {
+TEST_P(HttpCacheTest, SimplePUT_Invalidate) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -7702,7 +7780,7 @@
 }
 
 // Tests that we invalidate entries as a result of a PUT.
-TEST_F(HttpCacheTest, SimplePUT_Invalidate_305) {
+TEST_P(HttpCacheTest, SimplePUT_Invalidate_305) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -7741,7 +7819,7 @@
 }
 
 // Tests that we don't invalidate entries as a result of a failed PUT.
-TEST_F(HttpCacheTest, SimplePUT_DontInvalidate_404) {
+TEST_P(HttpCacheTest, SimplePUT_DontInvalidate_404) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -7780,7 +7858,7 @@
 }
 
 // Tests that we do not cache the response of a DELETE.
-TEST_F(HttpCacheTest, SimpleDELETE_Miss) {
+TEST_P(HttpCacheTest, SimpleDELETE_Miss) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimplePOST_Transaction);
@@ -7804,7 +7882,7 @@
 }
 
 // Tests that we invalidate entries as a result of a DELETE.
-TEST_F(HttpCacheTest, SimpleDELETE_Invalidate) {
+TEST_P(HttpCacheTest, SimpleDELETE_Invalidate) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -7840,7 +7918,7 @@
 }
 
 // Tests that we invalidate entries as a result of a DELETE.
-TEST_F(HttpCacheTest, SimpleDELETE_Invalidate_301) {
+TEST_P(HttpCacheTest, SimpleDELETE_Invalidate_301) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -7872,7 +7950,7 @@
 }
 
 // Tests that we don't invalidate entries as a result of a failed DELETE.
-TEST_F(HttpCacheTest, SimpleDELETE_DontInvalidate_416) {
+TEST_P(HttpCacheTest, SimpleDELETE_DontInvalidate_416) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -7905,7 +7983,7 @@
 }
 
 // Tests that we invalidate entries as a result of a PATCH.
-TEST_F(HttpCacheTest, SimplePATCH_Invalidate) {
+TEST_P(HttpCacheTest, SimplePATCH_Invalidate) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -7941,7 +8019,7 @@
 }
 
 // Tests that we invalidate entries as a result of a PATCH.
-TEST_F(HttpCacheTest, SimplePATCH_Invalidate_301) {
+TEST_P(HttpCacheTest, SimplePATCH_Invalidate_301) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -7973,7 +8051,7 @@
 }
 
 // Tests that we don't invalidate entries as a result of a failed PATCH.
-TEST_F(HttpCacheTest, SimplePATCH_DontInvalidate_416) {
+TEST_P(HttpCacheTest, SimplePATCH_DontInvalidate_416) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -8006,7 +8084,7 @@
 }
 
 // Tests that we don't invalidate entries after a failed network transaction.
-TEST_F(HttpCacheTest, SimpleGET_DontInvalidateOnFailure) {
+TEST_P(HttpCacheTest, SimpleGET_DontInvalidateOnFailure) {
   MockHttpCache cache;
 
   // Populate the cache.
@@ -8033,7 +8111,7 @@
   RemoveMockTransaction(&transaction);
 }
 
-TEST_F(HttpCacheTest, RangeGET_SkipsCache) {
+TEST_P(HttpCacheTest, RangeGET_SkipsCache) {
   MockHttpCache cache;
 
   // Test that we skip the cache for range GET requests.  Eventually, we will
@@ -8065,7 +8143,7 @@
 
 // Test that we skip the cache for range requests that include a validation
 // header.
-TEST_F(HttpCacheTest, RangeGET_SkipsCache2) {
+TEST_P(HttpCacheTest, RangeGET_SkipsCache2) {
   MockHttpCache cache;
 
   MockTransaction transaction(kRangeGET_Transaction);
@@ -8098,7 +8176,7 @@
   EXPECT_EQ(0, cache.disk_cache()->create_count());
 }
 
-TEST_F(HttpCacheTest, SimpleGET_DoesntLogHeaders) {
+TEST_P(HttpCacheTest, SimpleGET_DoesntLogHeaders) {
   MockHttpCache cache;
 
   RecordingNetLogObserver net_log_observer;
@@ -8109,7 +8187,7 @@
       net_log_observer, NetLogEventType::HTTP_CACHE_CALLER_REQUEST_HEADERS));
 }
 
-TEST_F(HttpCacheTest, RangeGET_LogsHeaders) {
+TEST_P(HttpCacheTest, RangeGET_LogsHeaders) {
   MockHttpCache cache;
 
   RecordingNetLogObserver net_log_observer;
@@ -8120,7 +8198,7 @@
       net_log_observer, NetLogEventType::HTTP_CACHE_CALLER_REQUEST_HEADERS));
 }
 
-TEST_F(HttpCacheTest, ExternalValidation_LogsHeaders) {
+TEST_P(HttpCacheTest, ExternalValidation_LogsHeaders) {
   MockHttpCache cache;
 
   RecordingNetLogObserver net_log_observer;
@@ -8133,7 +8211,7 @@
       net_log_observer, NetLogEventType::HTTP_CACHE_CALLER_REQUEST_HEADERS));
 }
 
-TEST_F(HttpCacheTest, SpecialHeaders_LogsHeaders) {
+TEST_P(HttpCacheTest, SpecialHeaders_LogsHeaders) {
   MockHttpCache cache;
 
   RecordingNetLogObserver net_log_observer;
@@ -8147,7 +8225,7 @@
 }
 
 // Tests that receiving 206 for a regular request is handled correctly.
-TEST_F(HttpCacheTest, GET_Crazy206) {
+TEST_P(HttpCacheTest, GET_Crazy206) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -8171,7 +8249,7 @@
 }
 
 // Tests that receiving 416 for a regular request is handled correctly.
-TEST_F(HttpCacheTest, GET_Crazy416) {
+TEST_P(HttpCacheTest, GET_Crazy416) {
   MockHttpCache cache;
 
   // Write to the cache.
@@ -8188,7 +8266,7 @@
 }
 
 // Tests that we don't store partial responses that can't be validated.
-TEST_F(HttpCacheTest, RangeGET_NoStrongValidators) {
+TEST_P(HttpCacheTest, RangeGET_NoStrongValidators) {
   MockHttpCache cache;
   std::string headers;
 
@@ -8215,7 +8293,7 @@
 }
 
 // Tests failures to conditionalize byte range requests.
-TEST_F(HttpCacheTest, RangeGET_NoConditionalization) {
+TEST_P(HttpCacheTest, RangeGET_NoConditionalization) {
   MockHttpCache cache;
   cache.FailConditionalizations();
   std::string headers;
@@ -8243,7 +8321,7 @@
 
 // Tests that restarting a partial request when the cached data cannot be
 // revalidated logs an event.
-TEST_F(HttpCacheTest, RangeGET_NoValidation_LogsRestart) {
+TEST_P(HttpCacheTest, RangeGET_NoValidation_LogsRestart) {
   MockHttpCache cache;
   cache.FailConditionalizations();
 
@@ -8264,7 +8342,7 @@
 
 // Tests that a failure to conditionalize a regular request (no range) with a
 // sparse entry results in a full response.
-TEST_F(HttpCacheTest, GET_NoConditionalization) {
+TEST_P(HttpCacheTest, GET_NoConditionalization) {
   for (bool use_memory_entry_data : {false, true}) {
     MockHttpCache cache;
     cache.disk_cache()->set_support_in_memory_entry_data(use_memory_entry_data);
@@ -8318,7 +8396,7 @@
 // Verifies that conditionalization failures when asking for a range that would
 // require the cache to modify the range to ask, result in a network request
 // that matches the user's one.
-TEST_F(HttpCacheTest, RangeGET_NoConditionalization2) {
+TEST_P(HttpCacheTest, RangeGET_NoConditionalization2) {
   MockHttpCache cache;
   cache.FailConditionalizations();
   std::string headers;
@@ -8356,7 +8434,7 @@
 }
 
 // Tests that we cache partial responses that lack content-length.
-TEST_F(HttpCacheTest, RangeGET_NoContentLength) {
+TEST_P(HttpCacheTest, RangeGET_NoContentLength) {
   MockHttpCache cache;
   std::string headers;
 
@@ -8388,7 +8466,7 @@
 
 // Tests that we can cache range requests and fetch random blocks from the
 // cache and the network.
-TEST_F(HttpCacheTest, RangeGET_OK) {
+TEST_P(HttpCacheTest, RangeGET_OK) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
   std::string headers;
@@ -8445,7 +8523,7 @@
   RemoveMockTransaction(&kRangeGET_TransactionOK);
 }
 
-TEST_F(HttpCacheTest, RangeGET_CacheReadError) {
+TEST_P(HttpCacheTest, RangeGET_CacheReadError) {
   // Tests recovery on cache read error on range request.
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
@@ -8478,7 +8556,7 @@
 
 // Tests that range requests with no-store get correct content-length
 // (https://crbug.com/700197).
-TEST_F(HttpCacheTest, RangeGET_NoStore) {
+TEST_P(HttpCacheTest, RangeGET_NoStore) {
   MockHttpCache cache;
 
   MockTransaction transaction(kRangeGET_TransactionOK);
@@ -8499,7 +8577,7 @@
 }
 
 // Tests a 304 setting no-store on existing 206 entry.
-TEST_F(HttpCacheTest, RangeGET_NoStore304) {
+TEST_P(HttpCacheTest, RangeGET_NoStore304) {
   MockHttpCache cache;
 
   MockTransaction transaction(kRangeGET_TransactionOK);
@@ -8540,7 +8618,7 @@
 
 // Tests that we can cache range requests and fetch random blocks from the
 // cache and the network, with synchronous responses.
-TEST_F(HttpCacheTest, RangeGET_SyncOK) {
+TEST_P(HttpCacheTest, RangeGET_SyncOK) {
   MockHttpCache cache;
 
   MockTransaction transaction(kRangeGET_TransactionOK);
@@ -8600,7 +8678,7 @@
 // Tests that if the previous transaction is cancelled while busy (doing sparse
 // IO), a new transaction (that reuses that same ActiveEntry) waits until the
 // entry is ready again.
-TEST_F(HttpCacheTest, Sparse_WaitForEntry) {
+TEST_P(HttpCacheTest, Sparse_WaitForEntry) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -8629,7 +8707,7 @@
 }
 
 // Tests that we don't revalidate an entry unless we are required to do so.
-TEST_F(HttpCacheTest, RangeGET_Revalidate1) {
+TEST_P(HttpCacheTest, RangeGET_Revalidate1) {
   MockHttpCache cache;
   std::string headers;
 
@@ -8679,7 +8757,7 @@
 }
 
 // Checks that we revalidate an entry when the headers say so.
-TEST_F(HttpCacheTest, RangeGET_Revalidate2) {
+TEST_P(HttpCacheTest, RangeGET_Revalidate2) {
   MockHttpCache cache;
   std::string headers;
 
@@ -8711,7 +8789,7 @@
 }
 
 // Tests that we deal with 304s for range requests.
-TEST_F(HttpCacheTest, RangeGET_304) {
+TEST_P(HttpCacheTest, RangeGET_304) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
   std::string headers;
@@ -8741,7 +8819,7 @@
 }
 
 // Tests that we deal with 206s when revalidating range requests.
-TEST_F(HttpCacheTest, RangeGET_ModifiedResult) {
+TEST_P(HttpCacheTest, RangeGET_ModifiedResult) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
   std::string headers;
@@ -8781,7 +8859,7 @@
 // the caller as is. In this context, a subrange means a response that starts
 // with the same byte that was requested, but that is not the whole range that
 // was requested.
-TEST_F(HttpCacheTest, RangeGET_206ReturnsSubrangeRange_NoCachedContent) {
+TEST_P(HttpCacheTest, RangeGET_206ReturnsSubrangeRange_NoCachedContent) {
   MockHttpCache cache;
   std::string headers;
 
@@ -8805,7 +8883,7 @@
 
 // Tests that when a server returns 206 with a sub-range of the requested range,
 // and there was an entry stored in the cache, the cache gets out of the way.
-TEST_F(HttpCacheTest, RangeGET_206ReturnsSubrangeRange_CachedContent) {
+TEST_P(HttpCacheTest, RangeGET_206ReturnsSubrangeRange_CachedContent) {
   MockHttpCache cache;
   std::string headers;
 
@@ -8846,7 +8924,7 @@
 // Tests that when a server returns 206 with a sub-range of the requested range,
 // and there was an entry stored in the cache, the cache gets out of the way,
 // when the caller is not using ranges.
-TEST_F(HttpCacheTest, GET_206ReturnsSubrangeRange_CachedContent) {
+TEST_P(HttpCacheTest, GET_206ReturnsSubrangeRange_CachedContent) {
   MockHttpCache cache;
   std::string headers;
 
@@ -8884,7 +8962,7 @@
 // not have any relationship with the requested range (may or may not be
 // contained). The important part is that the first byte doesn't match the first
 // requested byte.
-TEST_F(HttpCacheTest, RangeGET_206ReturnsWrongRange_NoCachedContent) {
+TEST_P(HttpCacheTest, RangeGET_206ReturnsWrongRange_NoCachedContent) {
   MockHttpCache cache;
   std::string headers;
 
@@ -8914,7 +8992,7 @@
 
 // Tests that when a server returns 206 with a random range and there is
 // an entry stored in the cache, the cache gets out of the way.
-TEST_F(HttpCacheTest, RangeGET_206ReturnsWrongRange_CachedContent) {
+TEST_P(HttpCacheTest, RangeGET_206ReturnsWrongRange_CachedContent) {
   MockHttpCache cache;
   std::string headers;
 
@@ -8952,7 +9030,7 @@
 
 // Tests that when a caller asks for a range beyond EOF, with an empty cache,
 // the response matches the one provided by the server.
-TEST_F(HttpCacheTest, RangeGET_206ReturnsSmallerFile_NoCachedContent) {
+TEST_P(HttpCacheTest, RangeGET_206ReturnsSmallerFile_NoCachedContent) {
   MockHttpCache cache;
   std::string headers;
 
@@ -8973,7 +9051,7 @@
 
 // Tests that when a caller asks for a range beyond EOF, with a cached entry,
 // the cache automatically fixes the request.
-TEST_F(HttpCacheTest, RangeGET_206ReturnsSmallerFile_CachedContent) {
+TEST_P(HttpCacheTest, RangeGET_206ReturnsSmallerFile_CachedContent) {
   MockHttpCache cache;
   std::string headers;
 
@@ -9000,7 +9078,7 @@
 
 // Tests that when a caller asks for a not-satisfiable range, the server's
 // response is forwarded to the caller.
-TEST_F(HttpCacheTest, RangeGET_416_NoCachedContent) {
+TEST_P(HttpCacheTest, RangeGET_416_NoCachedContent) {
   MockHttpCache cache;
   std::string headers;
 
@@ -9022,7 +9100,7 @@
 }
 
 // Tests that we cache 301s for range requests.
-TEST_F(HttpCacheTest, RangeGET_301) {
+TEST_P(HttpCacheTest, RangeGET_301) {
   MockHttpCache cache;
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
   transaction.status = "HTTP/1.1 301 Moved Permanently";
@@ -9045,7 +9123,7 @@
 
 // Tests that we can cache range requests when the start or end is unknown.
 // We start with one suffix request, followed by a request from a given point.
-TEST_F(HttpCacheTest, UnknownRangeGET_1) {
+TEST_P(HttpCacheTest, UnknownRangeGET_1) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
   std::string headers;
@@ -9080,7 +9158,7 @@
 // Tests that we can cache range requests when the start or end is unknown.
 // We start with one request from a given point, followed by a suffix request.
 // We'll also verify that synchronous cache responses work as intended.
-TEST_F(HttpCacheTest, UnknownRangeGET_2) {
+TEST_P(HttpCacheTest, UnknownRangeGET_2) {
   MockHttpCache cache;
   std::string headers;
 
@@ -9118,7 +9196,7 @@
 
 // Similar to UnknownRangeGET_2, except that the resource size is empty.
 // Regression test for crbug.com/813061, and probably https://crbug.com/1375128
-TEST_F(HttpCacheTest, UnknownRangeGET_3) {
+TEST_P(HttpCacheTest, UnknownRangeGET_3) {
   MockHttpCache cache;
   std::string headers;
 
@@ -9159,7 +9237,7 @@
 
 // Testcase for https://crbug.com/1433305, validation of range request to a
 // cache 302, which is notably bodiless.
-TEST_F(HttpCacheTest, UnknownRangeGET_302) {
+TEST_P(HttpCacheTest, UnknownRangeGET_302) {
   MockHttpCache cache;
   std::string headers;
 
@@ -9215,7 +9293,7 @@
 // Testcase for https://crbug.com/1433305, validation of range request to a
 // cache 302, which is notably bodiless, where the 302 is replaced with an
 // actual body.
-TEST_F(HttpCacheTest, UnknownRangeGET_302_Replaced) {
+TEST_P(HttpCacheTest, UnknownRangeGET_302_Replaced) {
   MockHttpCache cache;
   std::string headers;
 
@@ -9270,7 +9348,7 @@
 
 // Tests that receiving Not Modified when asking for an open range doesn't mess
 // up things.
-TEST_F(HttpCacheTest, UnknownRangeGET_304) {
+TEST_P(HttpCacheTest, UnknownRangeGET_304) {
   MockHttpCache cache;
   std::string headers;
 
@@ -9298,7 +9376,7 @@
 }
 
 // Tests that we can handle non-range requests when we have cached a range.
-TEST_F(HttpCacheTest, GET_Previous206) {
+TEST_P(HttpCacheTest, GET_Previous206) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
   std::string headers;
@@ -9336,7 +9414,7 @@
 
 // Tests that we can handle non-range requests when we have cached the first
 // part of the object and the server replies with 304 (Not Modified).
-TEST_F(HttpCacheTest, GET_Previous206_NotModified) {
+TEST_P(HttpCacheTest, GET_Previous206_NotModified) {
   MockHttpCache cache;
 
   MockTransaction transaction(kRangeGET_TransactionOK);
@@ -9388,7 +9466,7 @@
 
 // Tests that we can handle a regular request to a sparse entry, that results in
 // new content provided by the server (206).
-TEST_F(HttpCacheTest, GET_Previous206_NewContent) {
+TEST_P(HttpCacheTest, GET_Previous206_NewContent) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
   std::string headers;
@@ -9433,7 +9511,7 @@
 }
 
 // Tests that we can handle cached 206 responses that are not sparse.
-TEST_F(HttpCacheTest, GET_Previous206_NotSparse) {
+TEST_P(HttpCacheTest, GET_Previous206_NotSparse) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -9478,7 +9556,7 @@
 
 // Tests that we can handle cached 206 responses that are not sparse. This time
 // we issue a range request and expect to receive a range.
-TEST_F(HttpCacheTest, RangeGET_Previous206_NotSparse_2) {
+TEST_P(HttpCacheTest, RangeGET_Previous206_NotSparse_2) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -9519,7 +9597,7 @@
 }
 
 // Tests that we can handle cached 206 responses that can't be validated.
-TEST_F(HttpCacheTest, GET_Previous206_NotValidation) {
+TEST_P(HttpCacheTest, GET_Previous206_NotValidation) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -9561,7 +9639,7 @@
 }
 
 // Tests that we can handle range requests with cached 200 responses.
-TEST_F(HttpCacheTest, RangeGET_Previous200) {
+TEST_P(HttpCacheTest, RangeGET_Previous200) {
   MockHttpCache cache;
 
   // Store the whole thing with status 200.
@@ -9630,7 +9708,7 @@
 }
 
 // Tests that we can handle a 200 response when dealing with sparse entries.
-TEST_F(HttpCacheTest, RangeRequestResultsIn200) {
+TEST_P(HttpCacheTest, RangeRequestResultsIn200) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
   std::string headers;
@@ -9670,7 +9748,7 @@
 
 // Tests that a range request that falls outside of the size that we know about
 // only deletes the entry if the resource has indeed changed.
-TEST_F(HttpCacheTest, RangeGET_MoreThanCurrentSize) {
+TEST_P(HttpCacheTest, RangeGET_MoreThanCurrentSize) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
   std::string headers;
@@ -9703,7 +9781,7 @@
 }
 
 // Tests that we don't delete a sparse entry when we cancel a request.
-TEST_F(HttpCacheTest, RangeGET_Cancel) {
+TEST_P(HttpCacheTest, RangeGET_Cancel) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -9741,7 +9819,7 @@
 
 // Tests that we don't mark an entry as truncated if it is partial and not
 // already truncated.
-TEST_F(HttpCacheTest, RangeGET_CancelWhileReading) {
+TEST_P(HttpCacheTest, RangeGET_CancelWhileReading) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -9780,7 +9858,7 @@
 
 // Tests that we don't delete a sparse entry when we start a new request after
 // cancelling the previous one.
-TEST_F(HttpCacheTest, RangeGET_Cancel2) {
+TEST_P(HttpCacheTest, RangeGET_Cancel2) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -9826,7 +9904,7 @@
 
 // A slight variation of the previous test, this time we cancel two requests in
 // a row, making sure that the second is waiting for the entry to be ready.
-TEST_F(HttpCacheTest, RangeGET_Cancel3) {
+TEST_P(HttpCacheTest, RangeGET_Cancel3) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -9886,7 +9964,7 @@
 }
 
 // Tests that an invalid range response results in no cached entry.
-TEST_F(HttpCacheTest, RangeGET_InvalidResponse1) {
+TEST_P(HttpCacheTest, RangeGET_InvalidResponse1) {
   MockHttpCache cache;
   std::string headers;
 
@@ -9915,7 +9993,7 @@
 }
 
 // Tests that we reject a range that doesn't match the content-length.
-TEST_F(HttpCacheTest, RangeGET_InvalidResponse2) {
+TEST_P(HttpCacheTest, RangeGET_InvalidResponse2) {
   MockHttpCache cache;
   std::string headers;
 
@@ -9945,7 +10023,7 @@
 
 // Tests that if a server tells us conflicting information about a resource we
 // drop the entry.
-TEST_F(HttpCacheTest, RangeGET_InvalidResponse3) {
+TEST_P(HttpCacheTest, RangeGET_InvalidResponse3) {
   MockHttpCache cache;
   std::string headers;
 
@@ -9984,7 +10062,7 @@
 }
 
 // Tests that we handle large range values properly.
-TEST_F(HttpCacheTest, RangeGET_LargeValues) {
+TEST_P(HttpCacheTest, RangeGET_LargeValues) {
   // We need a real sparse cache for this test.
   MockHttpCache cache(HttpCache::DefaultBackend::InMemory(1024 * 1024));
   std::string headers;
@@ -10018,7 +10096,7 @@
 
 // Tests that we don't crash with a range request if the disk cache was not
 // initialized properly.
-TEST_F(HttpCacheTest, RangeGET_NoDiskCache) {
+TEST_P(HttpCacheTest, RangeGET_NoDiskCache) {
   auto factory = std::make_unique<MockBlockingBackendFactory>();
   factory->set_fail(true);
   factory->FinishCreation();  // We'll complete synchronously.
@@ -10033,7 +10111,7 @@
 }
 
 // Tests that we handle byte range requests that skip the cache.
-TEST_F(HttpCacheTest, RangeHEAD) {
+TEST_P(HttpCacheTest, RangeHEAD) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -10055,7 +10133,7 @@
 
 // Tests that we don't crash when after reading from the cache we issue a
 // request for the next range and the server gives us a 200 synchronously.
-TEST_F(HttpCacheTest, RangeGET_FastFlakyServer) {
+TEST_P(HttpCacheTest, RangeGET_FastFlakyServer) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -10083,7 +10161,7 @@
 
 // Tests that when the server gives us less data than expected, we don't keep
 // asking for more data.
-TEST_F(HttpCacheTest, RangeGET_FastFlakyServer2) {
+TEST_P(HttpCacheTest, RangeGET_FastFlakyServer2) {
   MockHttpCache cache;
 
   // First, check with an empty cache (WRITE mode).
@@ -10121,7 +10199,7 @@
   RemoveMockTransaction(&transaction);
 }
 
-TEST_F(HttpCacheTest, RangeGET_OK_LoadOnlyFromCache) {
+TEST_P(HttpCacheTest, RangeGET_OK_LoadOnlyFromCache) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -10158,7 +10236,7 @@
 }
 
 // Tests the handling of the "truncation" flag.
-TEST_F(HttpCacheTest, WriteResponseInfo_Truncated) {
+TEST_P(HttpCacheTest, WriteResponseInfo_Truncated) {
   MockHttpCache cache;
   disk_cache::Entry* entry;
   ASSERT_TRUE(cache.CreateBackendEntry(
@@ -10183,7 +10261,7 @@
 }
 
 // Tests basic pickling/unpickling of HttpResponseInfo.
-TEST_F(HttpCacheTest, PersistHttpResponseInfo) {
+TEST_P(HttpCacheTest, PersistHttpResponseInfo) {
   const IPEndPoint expected_endpoint = IPEndPoint(IPAddress(1, 2, 3, 4), 80);
   // Set some fields (add more if needed.)
   HttpResponseInfo response1;
@@ -10210,7 +10288,7 @@
 
 // Tests that we delete an entry when the request is cancelled before starting
 // to read from the network.
-TEST_F(HttpCacheTest, DoomOnDestruction) {
+TEST_P(HttpCacheTest, DoomOnDestruction) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -10240,7 +10318,7 @@
 
 // Tests that we delete an entry when the request is cancelled if the response
 // does not have content-length and strong validators.
-TEST_F(HttpCacheTest, DoomOnDestruction2) {
+TEST_P(HttpCacheTest, DoomOnDestruction2) {
   MockHttpCache cache;
 
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -10277,7 +10355,7 @@
 
 // Tests that we delete an entry when the request is cancelled if the response
 // has an "Accept-Ranges: none" header.
-TEST_F(HttpCacheTest, DoomOnDestruction3) {
+TEST_P(HttpCacheTest, DoomOnDestruction3) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -10322,7 +10400,7 @@
 }
 
 // Tests that we mark an entry as incomplete when the request is cancelled.
-TEST_F(HttpCacheTest, SetTruncatedFlag) {
+TEST_P(HttpCacheTest, SetTruncatedFlag) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -10373,7 +10451,7 @@
 
 // Tests that we do not mark an entry as truncated when the request is
 // cancelled.
-TEST_F(HttpCacheTest, DontSetTruncatedFlagForGarbledResponseCode) {
+TEST_P(HttpCacheTest, DontSetTruncatedFlagForGarbledResponseCode) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -10431,7 +10509,7 @@
 }
 
 // Tests that we don't mark an entry as truncated when we read everything.
-TEST_F(HttpCacheTest, DontSetTruncatedFlag) {
+TEST_P(HttpCacheTest, DontSetTruncatedFlag) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -10462,7 +10540,7 @@
 }
 
 // Tests that sparse entries don't set the truncate flag.
-TEST_F(HttpCacheTest, RangeGET_DontTruncate) {
+TEST_P(HttpCacheTest, RangeGET_DontTruncate) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -10489,7 +10567,7 @@
 
 // Tests that sparse entries don't set the truncate flag (when the byte range
 //  starts after 0).
-TEST_F(HttpCacheTest, RangeGET_DontTruncate2) {
+TEST_P(HttpCacheTest, RangeGET_DontTruncate2) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
@@ -10515,7 +10593,7 @@
 }
 
 // Tests that we can continue with a request that was interrupted.
-TEST_F(HttpCacheTest, GET_IncompleteResource) {
+TEST_P(HttpCacheTest, GET_IncompleteResource) {
   MockHttpCache cache;
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
 
@@ -10551,7 +10629,7 @@
 }
 
 // Tests the handling of no-store when revalidating a truncated entry.
-TEST_F(HttpCacheTest, GET_IncompleteResource_NoStore) {
+TEST_P(HttpCacheTest, GET_IncompleteResource_NoStore) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -10597,7 +10675,7 @@
 }
 
 // Tests cancelling a request after the server sent no-store.
-TEST_F(HttpCacheTest, GET_IncompleteResource_Cancel) {
+TEST_P(HttpCacheTest, GET_IncompleteResource_Cancel) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -10657,7 +10735,7 @@
 }
 
 // Tests that we delete truncated entries if the server changes its mind midway.
-TEST_F(HttpCacheTest, GET_IncompleteResource2) {
+TEST_P(HttpCacheTest, GET_IncompleteResource2) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -10695,7 +10773,7 @@
 }
 
 // Tests that we always validate a truncated request.
-TEST_F(HttpCacheTest, GET_IncompleteResource3) {
+TEST_P(HttpCacheTest, GET_IncompleteResource3) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -10731,7 +10809,7 @@
 }
 
 // Tests that we handle 401s for truncated resources.
-TEST_F(HttpCacheTest, GET_IncompleteResourceWithAuth) {
+TEST_P(HttpCacheTest, GET_IncompleteResourceWithAuth) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -10780,7 +10858,7 @@
 
 // Test that the transaction won't retry failed partial requests
 // after it starts reading data.  http://crbug.com/474835
-TEST_F(HttpCacheTest, TransactionRetryLimit) {
+TEST_P(HttpCacheTest, TransactionRetryLimit) {
   MockHttpCache cache;
 
   // Cache 0-9, so that we have data to read before failing.
@@ -10815,7 +10893,7 @@
 }
 
 // Tests that we cache a 200 response to the validation request.
-TEST_F(HttpCacheTest, GET_IncompleteResource4) {
+TEST_P(HttpCacheTest, GET_IncompleteResource4) {
   MockHttpCache cache;
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
 
@@ -10845,7 +10923,7 @@
 
 // Tests that when we cancel a request that was interrupted, we mark it again
 // as truncated.
-TEST_F(HttpCacheTest, GET_CancelIncompleteResource) {
+TEST_P(HttpCacheTest, GET_CancelIncompleteResource) {
   MockHttpCache cache;
   ScopedMockTransaction transaction(kRangeGET_TransactionOK);
 
@@ -10887,7 +10965,7 @@
 }
 
 // Tests that we can handle range requests when we have a truncated entry.
-TEST_F(HttpCacheTest, RangeGET_IncompleteResource) {
+TEST_P(HttpCacheTest, RangeGET_IncompleteResource) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -10912,7 +10990,7 @@
   RemoveMockTransaction(&kRangeGET_TransactionOK);
 }
 
-TEST_F(HttpCacheTest, SyncRead) {
+TEST_P(HttpCacheTest, SyncRead) {
   MockHttpCache cache;
 
   // This test ensures that a read that completes synchronously does not cause
@@ -10950,7 +11028,7 @@
   EXPECT_THAT(c3.error(), IsOk());
 }
 
-TEST_F(HttpCacheTest, ValidationResultsIn200) {
+TEST_P(HttpCacheTest, ValidationResultsIn200) {
   MockHttpCache cache;
 
   // This test ensures that a conditional request, which results in a 200
@@ -10968,7 +11046,7 @@
   RunTransactionTest(cache.http_cache(), kETagGET_Transaction);
 }
 
-TEST_F(HttpCacheTest, CachedRedirect) {
+TEST_P(HttpCacheTest, CachedRedirect) {
   MockHttpCache cache;
 
   ScopedMockTransaction kTestTransaction(kSimpleGET_Transaction);
@@ -11044,7 +11122,7 @@
 
 // Verify that no-cache resources are stored in cache, but are not fetched from
 // cache during normal loads.
-TEST_F(HttpCacheTest, CacheControlNoCacheNormalLoad) {
+TEST_P(HttpCacheTest, CacheControlNoCacheNormalLoad) {
   for (bool use_memory_entry_data : {false, true}) {
     MockHttpCache cache;
     cache.disk_cache()->set_support_in_memory_entry_data(use_memory_entry_data);
@@ -11080,7 +11158,7 @@
 
 // Verify that no-cache resources are stored in cache and fetched from cache
 // when the LOAD_SKIP_CACHE_VALIDATION flag is set.
-TEST_F(HttpCacheTest, CacheControlNoCacheHistoryLoad) {
+TEST_P(HttpCacheTest, CacheControlNoCacheHistoryLoad) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -11107,7 +11185,7 @@
   entry->Close();
 }
 
-TEST_F(HttpCacheTest, CacheControlNoStore) {
+TEST_P(HttpCacheTest, CacheControlNoStore) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -11132,7 +11210,7 @@
   EXPECT_FALSE(cache.OpenBackendEntry(request.CacheKey(), &entry));
 }
 
-TEST_F(HttpCacheTest, CacheControlNoStore2) {
+TEST_P(HttpCacheTest, CacheControlNoStore2) {
   // this test is similar to the above test, except that the initial response
   // is cachable, but when it is validated, no-store is received causing the
   // cached document to be deleted.
@@ -11161,7 +11239,7 @@
   EXPECT_FALSE(cache.OpenBackendEntry(request.CacheKey(), &entry));
 }
 
-TEST_F(HttpCacheTest, CacheControlNoStore3) {
+TEST_P(HttpCacheTest, CacheControlNoStore3) {
   // this test is similar to the above test, except that the response is a 304
   // instead of a 200.  this should never happen in practice, but it seems like
   // a good thing to verify that we still destroy the cache entry.
@@ -11192,7 +11270,7 @@
 }
 
 // Ensure that we don't cache requests served over bad HTTPS.
-TEST_F(HttpCacheTest, SimpleGET_SSLError) {
+TEST_P(HttpCacheTest, SimpleGET_SSLError) {
   MockHttpCache cache;
 
   MockTransaction transaction = kSimpleGET_Transaction;
@@ -11218,7 +11296,7 @@
 }
 
 // Ensure that we don't crash by if left-behind transactions.
-TEST_F(HttpCacheTest, OutlivedTransactions) {
+TEST_P(HttpCacheTest, OutlivedTransactions) {
   auto cache = std::make_unique<MockHttpCache>();
 
   std::unique_ptr<HttpTransaction> trans;
@@ -11229,7 +11307,7 @@
 }
 
 // Test that the disabled mode works.
-TEST_F(HttpCacheTest, CacheDisabledMode) {
+TEST_P(HttpCacheTest, CacheDisabledMode) {
   MockHttpCache cache;
 
   // write to the cache
@@ -11253,7 +11331,7 @@
 // HttpResponseHeaders::request_time and HttpResponseHeaders::response_time
 // fields also gets updated.
 // http://crbug.com/20594.
-TEST_F(HttpCacheTest, UpdatesRequestResponseTimeOn304) {
+TEST_P(HttpCacheTest, UpdatesRequestResponseTimeOn304) {
   MockHttpCache cache;
 
   const char kUrl[] = "http://foobar";
@@ -11413,7 +11491,7 @@
   EXPECT_FALSE(response.was_cached);
 }
 
-TEST_F(HttpCacheTest, HttpCacheProfileThirdPartyCSS) {
+TEST_P(HttpCacheTest, HttpCacheProfileThirdPartyCSS) {
   base::HistogramTester histograms;
   MockHttpCache cache;
   HttpResponseInfo response;
@@ -11456,7 +11534,7 @@
   histograms.ExpectTotalCount("HttpCache.Pattern.CSSThirdParty", 1);
 }
 
-TEST_F(HttpCacheTest, HttpCacheProfileThirdPartyJavaScript) {
+TEST_P(HttpCacheTest, HttpCacheProfileThirdPartyJavaScript) {
   base::HistogramTester histograms;
   MockHttpCache cache;
   HttpResponseInfo response;
@@ -11499,7 +11577,7 @@
   histograms.ExpectTotalCount("HttpCache.Pattern.JavaScriptThirdParty", 1);
 }
 
-TEST_F(HttpCacheTest, HttpCacheProfileThirdPartyFont) {
+TEST_P(HttpCacheTest, HttpCacheProfileThirdPartyFont) {
   base::HistogramTester histograms;
   MockHttpCache cache;
   HttpResponseInfo response;
@@ -11647,7 +11725,7 @@
   EXPECT_FALSE(response.was_cached);
 }
 
-TEST_F(HttpCacheTest, SplitCacheEnabledByDefault) {
+TEST_P(HttpCacheTest, SplitCacheEnabledByDefault) {
   HttpCache::ClearGlobalsForTesting();
   HttpCache::SplitCacheFeatureEnableByDefault();
   EXPECT_TRUE(HttpCache::IsSplitCacheEnabled());
@@ -11681,7 +11759,7 @@
   EXPECT_FALSE(response.was_cached);
 }
 
-TEST_F(HttpCacheTest, SplitCacheEnabledByDefaultButOverridden) {
+TEST_P(HttpCacheTest, SplitCacheEnabledByDefaultButOverridden) {
   HttpCache::ClearGlobalsForTesting();
   base::test::ScopedFeatureList feature_list;
   feature_list.InitAndDisableFeature(
@@ -11730,7 +11808,7 @@
   EXPECT_FALSE(response.was_cached);
 }
 
-TEST_F(HttpCacheTest, NonSplitCache) {
+TEST_F(HttpCacheTestBase, NonSplitCache) {
   base::test::ScopedFeatureList feature_list;
   feature_list.InitAndDisableFeature(
       net::features::kSplitCacheByNetworkIsolationKey);
@@ -11762,7 +11840,7 @@
   EXPECT_TRUE(response.was_cached);
 }
 
-TEST_F(HttpCacheTest, SkipVaryCheck) {
+TEST_P(HttpCacheTest, SkipVaryCheck) {
   MockHttpCache cache;
 
   // Write a simple vary transaction to the cache.
@@ -11787,7 +11865,7 @@
   RunTransactionTest(cache.http_cache(), transaction);
 }
 
-TEST_F(HttpCacheTest, SkipVaryCheckStar) {
+TEST_P(HttpCacheTest, SkipVaryCheckStar) {
   MockHttpCache cache;
 
   // Write a simple vary:* transaction to the cache.
@@ -11813,7 +11891,7 @@
 
 // Tests that we only return valid entries with LOAD_ONLY_FROM_CACHE
 // transactions unless LOAD_SKIP_CACHE_VALIDATION is set.
-TEST_F(HttpCacheTest, ValidLoadOnlyFromCache) {
+TEST_P(HttpCacheTest, ValidLoadOnlyFromCache) {
   MockHttpCache cache;
   base::SimpleTestClock clock;
   cache.http_cache()->SetClockForTesting(&clock);
@@ -11837,7 +11915,7 @@
   RunTransactionTest(cache.http_cache(), transaction);
 }
 
-TEST_F(HttpCacheTest, InvalidLoadFlagCombination) {
+TEST_P(HttpCacheTest, InvalidLoadFlagCombination) {
   MockHttpCache cache;
 
   // Put the resource in the cache.
@@ -11855,7 +11933,7 @@
 
 // Tests that we don't mark entries as truncated when a filter detects the end
 // of the stream.
-TEST_F(HttpCacheTest, FilterCompletion) {
+TEST_P(HttpCacheTest, FilterCompletion) {
   MockHttpCache cache;
   TestCompletionCallback callback;
 
@@ -11889,7 +11967,7 @@
 // Tests that we don't mark entries as truncated and release the cache
 // entry when DoneReading() is called before any Read() calls, such as
 // for a redirect.
-TEST_F(HttpCacheTest, DoneReading) {
+TEST_P(HttpCacheTest, DoneReading) {
   MockHttpCache cache;
   TestCompletionCallback callback;
 
@@ -11918,7 +11996,7 @@
 }
 
 // Tests that we stop caching when told.
-TEST_F(HttpCacheTest, StopCachingDeletesEntry) {
+TEST_P(HttpCacheTest, StopCachingDeletesEntry) {
   MockHttpCache cache;
   TestCompletionCallback callback;
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -11956,7 +12034,7 @@
 
 // Tests that we stop caching when told, even if DoneReading is called
 // after StopCaching.
-TEST_F(HttpCacheTest, StopCachingThenDoneReadingDeletesEntry) {
+TEST_P(HttpCacheTest, StopCachingThenDoneReadingDeletesEntry) {
   MockHttpCache cache;
   TestCompletionCallback callback;
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -11996,7 +12074,7 @@
 }
 
 // Tests that we stop caching when told, when using auth.
-TEST_F(HttpCacheTest, StopCachingWithAuthDeletesEntry) {
+TEST_P(HttpCacheTest, StopCachingWithAuthDeletesEntry) {
   MockHttpCache cache;
   TestCompletionCallback callback;
   MockTransaction mock_transaction(kSimpleGET_Transaction);
@@ -12027,7 +12105,7 @@
 }
 
 // Tests that when we are told to stop caching we don't throw away valid data.
-TEST_F(HttpCacheTest, StopCachingSavesEntry) {
+TEST_P(HttpCacheTest, StopCachingSavesEntry) {
   MockHttpCache cache;
   TestCompletionCallback callback;
   MockHttpRequest request(kSimpleGET_Transaction);
@@ -12066,7 +12144,7 @@
 }
 
 // Tests that we handle truncated enries when StopCaching is called.
-TEST_F(HttpCacheTest, StopCachingTruncatedEntry) {
+TEST_P(HttpCacheTest, StopCachingTruncatedEntry) {
   MockHttpCache cache;
   TestCompletionCallback callback;
   MockHttpRequest request(kRangeGET_TransactionOK);
@@ -12385,7 +12463,7 @@
 
 // Tests that we detect truncated resources from the net when there is
 // a Content-Length header.
-TEST_F(HttpCacheTest, TruncatedByContentLength) {
+TEST_P(HttpCacheTest, TruncatedByContentLength) {
   MockHttpCache cache;
   TestCompletionCallback callback;
 
@@ -12406,7 +12484,7 @@
 
 // Tests that we actually flag entries as truncated when we detect an error
 // from the net.
-TEST_F(HttpCacheTest, TruncatedByContentLength2) {
+TEST_P(HttpCacheTest, TruncatedByContentLength2) {
   MockHttpCache cache;
   TestCompletionCallback callback;
 
@@ -12425,7 +12503,7 @@
 
 // Make sure that calling SetPriority on a cache transaction passes on
 // its priority updates to its underlying network transaction.
-TEST_F(HttpCacheTest, SetPriority) {
+TEST_P(HttpCacheTest, SetPriority) {
   MockHttpCache cache;
 
   HttpRequestInfo info;
@@ -12462,7 +12540,7 @@
 
 // Make sure that calling SetWebSocketHandshakeStreamCreateHelper on a cache
 // transaction passes on its argument to the underlying network transaction.
-TEST_F(HttpCacheTest, SetWebSocketHandshakeStreamCreateHelper) {
+TEST_P(HttpCacheTest, SetWebSocketHandshakeStreamCreateHelper) {
   MockHttpCache cache;
   HttpRequestInfo info;
 
@@ -12489,7 +12567,7 @@
 
 // Make sure that a cache transaction passes on its priority to
 // newly-created network transactions.
-TEST_F(HttpCacheTest, SetPriorityNewTransaction) {
+TEST_P(HttpCacheTest, SetPriorityNewTransaction) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
 
@@ -12542,7 +12620,7 @@
 
 }  // namespace
 
-TEST_F(HttpCacheTest, NetworkBytesCacheMissAndThenHit) {
+TEST_P(HttpCacheTest, NetworkBytesCacheMissAndThenHit) {
   MockHttpCache cache;
 
   MockTransaction transaction(kSimpleGET_Transaction);
@@ -12556,7 +12634,7 @@
   EXPECT_EQ(0, received);
 }
 
-TEST_F(HttpCacheTest, NetworkBytesConditionalRequest304) {
+TEST_P(HttpCacheTest, NetworkBytesConditionalRequest304) {
   MockHttpCache cache;
 
   ScopedMockTransaction transaction(kETagGET_Transaction);
@@ -12572,7 +12650,7 @@
   EXPECT_EQ(MockNetworkTransaction::kTotalReceivedBytes, received);
 }
 
-TEST_F(HttpCacheTest, NetworkBytesConditionalRequest200) {
+TEST_P(HttpCacheTest, NetworkBytesConditionalRequest200) {
   MockHttpCache cache;
 
   MockTransaction transaction(kTypicalGET_Transaction);
@@ -12599,7 +12677,7 @@
   RemoveMockTransaction(&transaction);
 }
 
-TEST_F(HttpCacheTest, NetworkBytesRange) {
+TEST_P(HttpCacheTest, NetworkBytesRange) {
   MockHttpCache cache;
   AddMockTransaction(&kRangeGET_TransactionOK);
   MockTransaction transaction(kRangeGET_TransactionOK);
@@ -12742,7 +12820,7 @@
   EXPECT_FALSE(TransactionRequiredNetwork(LOAD_NORMAL));
 }
 
-TEST_F(HttpCacheTest, StaleContentNotUsedWhenLoadFlagNotSet) {
+TEST_P(HttpCacheTest, StaleContentNotUsedWhenLoadFlagNotSet) {
   MockHttpCache cache;
 
   ScopedMockTransaction stale_while_revalidate_transaction(
@@ -12767,7 +12845,7 @@
   EXPECT_FALSE(response_info.async_revalidation_requested);
 }
 
-TEST_F(HttpCacheTest, StaleContentUsedWhenLoadFlagSetAndUsableThenTimesout) {
+TEST_P(HttpCacheTest, StaleContentUsedWhenLoadFlagSetAndUsableThenTimesout) {
   MockHttpCache cache;
   base::SimpleTestClock clock;
   cache.http_cache()->SetClockForTesting(&clock);
@@ -12808,7 +12886,7 @@
   EXPECT_FALSE(response_info.async_revalidation_requested);
 }
 
-TEST_F(HttpCacheTest, StaleContentUsedWhenLoadFlagSetAndUsable) {
+TEST_P(HttpCacheTest, StaleContentUsedWhenLoadFlagSetAndUsable) {
   MockHttpCache cache;
   base::SimpleTestClock clock;
   cache.http_cache()->SetClockForTesting(&clock);
@@ -12864,7 +12942,7 @@
   EXPECT_TRUE(response_info.stale_revalidate_timeout.is_null());
 }
 
-TEST_F(HttpCacheTest, StaleContentNotUsedWhenUnusable) {
+TEST_P(HttpCacheTest, StaleContentNotUsedWhenUnusable) {
   MockHttpCache cache;
 
   ScopedMockTransaction stale_while_revalidate_transaction(
@@ -12890,7 +12968,7 @@
   EXPECT_FALSE(response_info.async_revalidation_requested);
 }
 
-TEST_F(HttpCacheTest, StaleContentWriteError) {
+TEST_P(HttpCacheTest, StaleContentWriteError) {
   MockHttpCache cache;
   base::SimpleTestClock clock;
   cache.http_cache()->SetClockForTesting(&clock);
@@ -12923,7 +13001,7 @@
 
 // Tests that we allow multiple simultaneous, non-overlapping transactions to
 // take place on a sparse entry.
-TEST_F(HttpCacheTest, RangeGET_MultipleRequests) {
+TEST_P(HttpCacheTest, RangeGET_MultipleRequests) {
   MockHttpCache cache;
 
   // Create a transaction for bytes 0-9.
@@ -12958,7 +13036,7 @@
 // implemented so it returns ERR_CACHE_MISS. See also
 // HttpCacheTest.RangeGET_OK_LoadOnlyFromCache.
 // TODO(ricea): Update this test if it is implemented in future.
-TEST_F(HttpCacheTest, RangeGET_Previous200_LoadOnlyFromCache) {
+TEST_P(HttpCacheTest, RangeGET_Previous200_LoadOnlyFromCache) {
   MockHttpCache cache;
 
   // Store the whole thing with status 200.
@@ -13000,7 +13078,7 @@
 // with "Cache-Control: no-store" arrives. That means that another request for
 // the same URL can be processed before the response body of the original
 // request arrives.
-TEST_F(HttpCacheTest, NoStoreResponseShouldNotBlockFollowingRequests) {
+TEST_P(HttpCacheTest, NoStoreResponseShouldNotBlockFollowingRequests) {
   MockHttpCache cache;
   ScopedMockTransaction mock_transaction(kSimpleGET_Transaction);
   mock_transaction.response_headers = "Cache-Control: no-store\n";
@@ -13041,7 +13119,7 @@
 
 // Tests that serving a response entirely from cache replays the previous
 // SSLInfo.
-TEST_F(HttpCacheTest, CachePreservesSSLInfo) {
+TEST_P(HttpCacheTest, CachePreservesSSLInfo) {
   static const uint16_t kTLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xc02f;
   int status = 0;
   SSLConnectionStatusSetCipherSuite(kTLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
@@ -13087,7 +13165,7 @@
 }
 
 // Tests that SSLInfo gets updated when revalidating a cached response.
-TEST_F(HttpCacheTest, RevalidationUpdatesSSLInfo) {
+TEST_P(HttpCacheTest, RevalidationUpdatesSSLInfo) {
   static const uint16_t kTLS_RSA_WITH_RC4_128_MD5 = 0x0004;
   static const uint16_t kTLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xc02f;
 
@@ -13147,7 +13225,7 @@
   EXPECT_TRUE(cert2->EqualsIncludingChain(response_info.ssl_info.cert.get()));
 }
 
-TEST_F(HttpCacheTest, CacheEntryStatusOther) {
+TEST_P(HttpCacheTest, CacheEntryStatusOther) {
   MockHttpCache cache;
 
   HttpResponseInfo response_info;
@@ -13159,7 +13237,7 @@
   EXPECT_EQ(CacheEntryStatus::ENTRY_OTHER, response_info.cache_entry_status);
 }
 
-TEST_F(HttpCacheTest, CacheEntryStatusNotInCache) {
+TEST_P(HttpCacheTest, CacheEntryStatusNotInCache) {
   MockHttpCache cache;
 
   HttpResponseInfo response_info;
@@ -13172,7 +13250,7 @@
             response_info.cache_entry_status);
 }
 
-TEST_F(HttpCacheTest, CacheEntryStatusUsed) {
+TEST_P(HttpCacheTest, CacheEntryStatusUsed) {
   MockHttpCache cache;
   RunTransactionTest(cache.http_cache(), kSimpleGET_Transaction);
 
@@ -13185,7 +13263,7 @@
   EXPECT_EQ(CacheEntryStatus::ENTRY_USED, response_info.cache_entry_status);
 }
 
-TEST_F(HttpCacheTest, CacheEntryStatusValidated) {
+TEST_P(HttpCacheTest, CacheEntryStatusValidated) {
   MockHttpCache cache;
   RunTransactionTest(cache.http_cache(), kETagGET_Transaction);
 
@@ -13203,7 +13281,7 @@
             response_info.cache_entry_status);
 }
 
-TEST_F(HttpCacheTest, CacheEntryStatusUpdated) {
+TEST_P(HttpCacheTest, CacheEntryStatusUpdated) {
   MockHttpCache cache;
   RunTransactionTest(cache.http_cache(), kETagGET_Transaction);
 
@@ -13219,7 +13297,7 @@
   EXPECT_EQ(CacheEntryStatus::ENTRY_UPDATED, response_info.cache_entry_status);
 }
 
-TEST_F(HttpCacheTest, CacheEntryStatusCantConditionalize) {
+TEST_P(HttpCacheTest, CacheEntryStatusCantConditionalize) {
   MockHttpCache cache;
   cache.FailConditionalizations();
   RunTransactionTest(cache.http_cache(), kTypicalGET_Transaction);
@@ -13234,7 +13312,7 @@
             response_info.cache_entry_status);
 }
 
-TEST_F(HttpSplitCacheKeyTest, GetResourceURLFromHttpCacheKey) {
+TEST_P(HttpSplitCacheKeyTest, GetResourceURLFromHttpCacheKey) {
   base::test::ScopedFeatureList feature_list;
   feature_list.InitAndEnableFeature(
       net::features::kSplitCacheByNetworkIsolationKey);
@@ -13248,7 +13326,7 @@
   }
 }
 
-TEST_F(HttpCacheTest, GetResourceURLFromHttpCacheKey) {
+TEST_P(HttpCacheTest, GetResourceURLFromHttpCacheKey) {
   const struct {
     std::string input;
     std::string output;
@@ -13297,13 +13375,14 @@
   }
 };
 
-TEST_F(HttpCacheIOCallbackTest, FailedDoomFollowedByOpen) {
+TEST_P(HttpCacheIOCallbackTest, FailedDoomFollowedByOpen) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to DoomEntry and OpenEntry
   // below require that it exists.
@@ -13336,13 +13415,14 @@
   ASSERT_EQ(entry1, nullptr);
 }
 
-TEST_F(HttpCacheIOCallbackTest, FailedDoomFollowedByCreate) {
+TEST_P(HttpCacheIOCallbackTest, FailedDoomFollowedByCreate) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to DoomEntry and CreateEntry
   // below require that it exists.
@@ -13375,13 +13455,14 @@
   ASSERT_EQ(entry1, nullptr);
 }
 
-TEST_F(HttpCacheIOCallbackTest, FailedDoomFollowedByDoom) {
+TEST_P(HttpCacheIOCallbackTest, FailedDoomFollowedByDoom) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to DoomEntry below require that
   // it exists.
@@ -13410,13 +13491,14 @@
   ASSERT_EQ(cb.results()[1], ERR_CACHE_RACE);
 }
 
-TEST_F(HttpCacheIOCallbackTest, FailedOpenFollowedByCreate) {
+TEST_P(HttpCacheIOCallbackTest, FailedOpenFollowedByCreate) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to OpenEntry and CreateEntry
   // below require that it exists.
@@ -13452,13 +13534,14 @@
   ASSERT_EQ(entry2, nullptr);
 }
 
-TEST_F(HttpCacheIOCallbackTest, FailedCreateFollowedByOpen) {
+TEST_P(HttpCacheIOCallbackTest, FailedCreateFollowedByOpen) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to CreateEntry and OpenEntry
   // below require that it exists.
@@ -13494,13 +13577,14 @@
   ASSERT_EQ(entry2, nullptr);
 }
 
-TEST_F(HttpCacheIOCallbackTest, FailedCreateFollowedByCreate) {
+TEST_P(HttpCacheIOCallbackTest, FailedCreateFollowedByCreate) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to CreateEntry below require
   // that it exists.
@@ -13535,13 +13619,14 @@
   ASSERT_EQ(entry2, nullptr);
 }
 
-TEST_F(HttpCacheIOCallbackTest, CreateFollowedByCreate) {
+TEST_P(HttpCacheIOCallbackTest, CreateFollowedByCreate) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to CreateEntry below require
   // that it exists.
@@ -13574,13 +13659,14 @@
   ASSERT_EQ(entry2, nullptr);
 }
 
-TEST_F(HttpCacheIOCallbackTest, OperationFollowedByDoom) {
+TEST_P(HttpCacheIOCallbackTest, OperationFollowedByDoom) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to CreateEntry and DoomEntry
   // below require that it exists.
@@ -13611,13 +13697,14 @@
   ASSERT_EQ(cb.results()[1], ERR_CACHE_RACE);
 }
 
-TEST_F(HttpCacheIOCallbackTest, CreateFollowedByOpenOrCreate) {
+TEST_P(HttpCacheIOCallbackTest, CreateFollowedByOpenOrCreate) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to CreateEntry and
   // OpenOrCreateEntry below require that it exists.
@@ -13651,13 +13738,14 @@
   ASSERT_EQ(entry1->disk_entry, entry2->disk_entry);
 }
 
-TEST_F(HttpCacheIOCallbackTest, FailedCreateFollowedByOpenOrCreate) {
+TEST_P(HttpCacheIOCallbackTest, FailedCreateFollowedByOpenOrCreate) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to CreateEntry and
   // OpenOrCreateEntry below require that it exists.
@@ -13693,13 +13781,14 @@
   ASSERT_EQ(entry2, nullptr);
 }
 
-TEST_F(HttpCacheIOCallbackTest, OpenFollowedByOpenOrCreate) {
+TEST_P(HttpCacheIOCallbackTest, OpenFollowedByOpenOrCreate) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to OpenEntry and
   // OpenOrCreateEntry below require that it exists.
@@ -13746,13 +13835,14 @@
   ASSERT_EQ(entry1->disk_entry, entry2->disk_entry);
 }
 
-TEST_F(HttpCacheIOCallbackTest, FailedOpenFollowedByOpenOrCreate) {
+TEST_P(HttpCacheIOCallbackTest, FailedOpenFollowedByOpenOrCreate) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to OpenEntry and
   // OpenOrCreateEntry below require that it exists.
@@ -13788,13 +13878,14 @@
   ASSERT_EQ(entry2, nullptr);
 }
 
-TEST_F(HttpCacheIOCallbackTest, OpenOrCreateFollowedByCreate) {
+TEST_P(HttpCacheIOCallbackTest, OpenOrCreateFollowedByCreate) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to OpenOrCreateEntry and
   // CreateEntry below require that it exists.
@@ -13827,13 +13918,14 @@
   ASSERT_EQ(entry2, nullptr);
 }
 
-TEST_F(HttpCacheIOCallbackTest, OpenOrCreateFollowedByOpenOrCreate) {
+TEST_P(HttpCacheIOCallbackTest, OpenOrCreateFollowedByOpenOrCreate) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to OpenOrCreateEntry below
   // require that it exists.
@@ -13866,13 +13958,14 @@
   ASSERT_NE(entry2, nullptr);
 }
 
-TEST_F(HttpCacheIOCallbackTest, FailedOpenOrCreateFollowedByOpenOrCreate) {
+TEST_P(HttpCacheIOCallbackTest, FailedOpenOrCreateFollowedByOpenOrCreate) {
   MockHttpCache cache;
   TestCompletionCallbackForHttpCache cb;
   std::unique_ptr<Transaction> transaction =
       std::make_unique<Transaction>(DEFAULT_PRIORITY, cache.http_cache());
 
   transaction->SetIOCallBackForTest(cb.callback());
+  transaction->SetCacheIOCallBackForTest(cb.callback());
 
   // Create the backend here as our direct calls to OpenOrCreateEntry below
   // require that it exists.
@@ -13908,7 +14001,7 @@
   ASSERT_EQ(entry2, nullptr);
 }
 
-TEST_F(HttpCacheTest, DnsAliasesNoRevalidation) {
+TEST_P(HttpCacheTest, DnsAliasesNoRevalidation) {
   MockHttpCache cache;
   HttpResponseInfo response;
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -13929,7 +14022,7 @@
   EXPECT_THAT(response.dns_aliases, testing::ElementsAre("alias1", "alias2"));
 }
 
-TEST_F(HttpCacheTest, NoDnsAliasesNoRevalidation) {
+TEST_P(HttpCacheTest, NoDnsAliasesNoRevalidation) {
   MockHttpCache cache;
   HttpResponseInfo response;
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -13950,7 +14043,7 @@
   EXPECT_TRUE(response.dns_aliases.empty());
 }
 
-TEST_F(HttpCacheTest, DnsAliasesRevalidation) {
+TEST_P(HttpCacheTest, DnsAliasesRevalidation) {
   MockHttpCache cache;
   HttpResponseInfo response;
   ScopedMockTransaction transaction(kTypicalGET_Transaction);
@@ -13982,7 +14075,7 @@
   EXPECT_THAT(response.dns_aliases, testing::ElementsAre("alias3", "alias4"));
 }
 
-TEST_F(HttpCacheTest, FirstPartySetsBypassCache_ShouldBypass_NoId) {
+TEST_P(HttpCacheTest, FirstPartySetsBypassCache_ShouldBypass_NoId) {
   MockHttpCache cache;
   HttpResponseInfo response;
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -13997,7 +14090,7 @@
   EXPECT_FALSE(response.was_cached);
 }
 
-TEST_F(HttpCacheTest, FirstPartySetsBypassCache_ShouldBypass_IdTooSmall) {
+TEST_P(HttpCacheTest, FirstPartySetsBypassCache_ShouldBypass_IdTooSmall) {
   MockHttpCache cache;
   HttpResponseInfo response;
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -14015,7 +14108,7 @@
   EXPECT_FALSE(response.was_cached);
 }
 
-TEST_F(HttpCacheTest, FirstPartySetsBypassCache_ShouldNotBypass) {
+TEST_P(HttpCacheTest, FirstPartySetsBypassCache_ShouldNotBypass) {
   MockHttpCache cache;
   HttpResponseInfo response;
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -14033,7 +14126,7 @@
   EXPECT_TRUE(response.was_cached);
 }
 
-TEST_F(HttpCacheTest, FirstPartySetsBypassCache_ShouldNotBypass_NoFilter) {
+TEST_P(HttpCacheTest, FirstPartySetsBypassCache_ShouldNotBypass_NoFilter) {
   MockHttpCache cache;
   HttpResponseInfo response;
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
@@ -14047,7 +14140,7 @@
   EXPECT_TRUE(response.was_cached);
 }
 
-TEST_F(HttpCacheTest, SecurityHeadersAreCopiedToConditionalizedResponse) {
+TEST_P(HttpCacheTest, SecurityHeadersAreCopiedToConditionalizedResponse) {
   MockHttpCache cache;
   HttpResponseInfo response;
   ScopedMockTransaction transaction(kSimpleGET_Transaction);
diff --git a/net/http/http_network_transaction.cc b/net/http/http_network_transaction.cc
index d8bade0..e976ccd 100644
--- a/net/http/http_network_transaction.cc
+++ b/net/http/http_network_transaction.cc
@@ -57,6 +57,7 @@
 #include "net/http/transport_security_state.h"
 #include "net/http/url_security_manager.h"
 #include "net/log/net_log_event_type.h"
+#include "net/proxy_resolution/proxy_info.h"
 #include "net/socket/client_socket_factory.h"
 #include "net/socket/next_proto.h"
 #include "net/socket/transport_client_socket_pool.h"
@@ -107,6 +108,23 @@
     response_info->proxy_server = ProxyServer();
 }
 
+// Returns true when Early Hints are allowed on the given protocol.
+bool EarlyHintsAreAllowedOn(HttpResponseInfo::ConnectionInfo connection_info) {
+  switch (connection_info) {
+    case HttpResponseInfo::ConnectionInfo::CONNECTION_INFO_HTTP0_9:
+    case HttpResponseInfo::ConnectionInfo::CONNECTION_INFO_HTTP1_0:
+      return false;
+    case HttpResponseInfo::ConnectionInfo::CONNECTION_INFO_HTTP1_1:
+      return base::FeatureList::IsEnabled(features::kEnableEarlyHintsOnHttp11);
+    default:
+      CHECK_NE(connection_info,
+               HttpResponseInfo::ConnectionInfo::NUM_OF_CONNECTION_INFOS);
+      // Implicitly allow CONNECTION_INFO_UNKNOWN because this is the default
+      // value and ConnectionInfo isn't always set.
+      return true;
+  }
+}
+
 }  // namespace
 
 const int HttpNetworkTransaction::kDrainBodyBufferSize;
@@ -1151,15 +1169,20 @@
         response_.headers.get());
 
     // Early Hints does not make sense for a WebSocket handshake.
-    if (ForWebSocketHandshake())
+    if (ForWebSocketHandshake()) {
       return ERR_FAILED;
+    }
 
-    // TODO(crbug.com/671310): Validate headers? It seems that
-    // "Content-Encoding" etc should not appear.
+    // TODO(https://crbug.com/671310): Validate headers?  "Content-Encoding" etc
+    // should not appear since informational responses can't contain content.
+    // https://www.rfc-editor.org/rfc/rfc9110#name-informational-1xx
 
-    if (early_response_headers_callback_)
+    if (EarlyHintsAreAllowedOn(response_.connection_info) &&
+        early_response_headers_callback_) {
       early_response_headers_callback_.Run(std::move(response_.headers));
+    }
 
+    // Reset response headers for the final response.
     response_.headers =
         base::MakeRefCounted<HttpResponseHeaders>(std::string());
     next_state_ = STATE_READ_HEADERS;
diff --git a/services/device/public/cpp/geolocation/geolocation_manager.cc b/services/device/public/cpp/geolocation/geolocation_manager.cc
index 15a0d940..54991a73 100644
--- a/services/device/public/cpp/geolocation/geolocation_manager.cc
+++ b/services/device/public/cpp/geolocation/geolocation_manager.cc
@@ -46,15 +46,6 @@
 }
 
 #if BUILDFLAG(IS_APPLE) || BUILDFLAG(IS_CHROMEOS)
-
-void GeolocationManager::AppAttemptsToUseGeolocation() {
-  system_geolocation_source_->AppAttemptsToUseGeolocation();
-}
-
-void GeolocationManager::AppCeasesToUseGeolocation() {
-  system_geolocation_source_->AppCeasesToUseGeolocation();
-}
-
 GeolocationManager::GeolocationManager(
     std::unique_ptr<SystemGeolocationSource> system_geolocation_source)
     : system_geolocation_source_(std::move(system_geolocation_source)),
@@ -109,12 +100,20 @@
   return *system_geolocation_source_;
 }
 
-#else
-
-void GeolocationManager::AppAttemptsToUseGeolocation() {}
-
-void GeolocationManager::AppCeasesToUseGeolocation() {}
-
 #endif
 
+void GeolocationManager::TrackGeolocationAttempted(
+    const std::string& app_name) {
+#if BUILDFLAG(IS_APPLE) || BUILDFLAG(IS_CHROMEOS)
+  system_geolocation_source_->TrackGeolocationAttempted(app_name);
+#endif
+}
+
+void GeolocationManager::TrackGeolocationRelinquished(
+    const std::string& app_name) {
+#if BUILDFLAG(IS_APPLE) || BUILDFLAG(IS_CHROMEOS)
+  system_geolocation_source_->TrackGeolocationRelinquished(app_name);
+#endif
+}
+
 }  // namespace device
diff --git a/services/device/public/cpp/geolocation/geolocation_manager.h b/services/device/public/cpp/geolocation/geolocation_manager.h
index 3550912e..546fe74 100644
--- a/services/device/public/cpp/geolocation/geolocation_manager.h
+++ b/services/device/public/cpp/geolocation/geolocation_manager.h
@@ -6,6 +6,7 @@
 #define SERVICES_DEVICE_PUBLIC_CPP_GEOLOCATION_GEOLOCATION_MANAGER_H_
 
 #include <memory>
+#include <string>
 
 #include "base/component_export.h"
 #include "build/build_config.h"
@@ -33,8 +34,8 @@
   // Sets the global instance of the Geolocation Manager.
   static void SetInstance(std::unique_ptr<GeolocationManager> manager);
 
-  void AppAttemptsToUseGeolocation();
-  void AppCeasesToUseGeolocation();
+  void TrackGeolocationAttempted(const std::string& app_name = "");
+  void TrackGeolocationRelinquished(const std::string& app_name = "");
 
 #if !BUILDFLAG(IS_APPLE) && !BUILDFLAG(IS_CHROMEOS)
 // Default empty implementation of Geolocation Manager. It is used on operation
diff --git a/services/device/public/cpp/geolocation/system_geolocation_source.h b/services/device/public/cpp/geolocation/system_geolocation_source.h
index 08e2f9d..68cc9be4 100644
--- a/services/device/public/cpp/geolocation/system_geolocation_source.h
+++ b/services/device/public/cpp/geolocation/system_geolocation_source.h
@@ -46,12 +46,12 @@
 
   // Informs system that some page wants to use geolocation. This function may
   // be implemented if the OS specific implementation requires it.
-  virtual void AppAttemptsToUseGeolocation() {}
+  virtual void TrackGeolocationAttempted(const std::string& app_name) {}
   // Informs that some page does not need to use geolocation any more. This
   // function should be called only if the intention to use geolocation was
-  // signalled for the same page using AppAttemptsToUseGeolocation(). This
+  // signalled for the same page using TrackGeolocationAttempted(). This
   // function may be implemented if the OS specific implementation requires it.
-  virtual void AppCeasesToUseGeolocation() {}
+  virtual void TrackGeolocationRelinquished(const std::string& app_name) {}
 
 #if BUILDFLAG(IS_APPLE)
   // This method accepts a callback. The callback is to be called always when
diff --git a/services/device/public/cpp/geolocation/system_geolocation_source_mac.h b/services/device/public/cpp/geolocation/system_geolocation_source_mac.h
index 64c87e1..86951c48 100644
--- a/services/device/public/cpp/geolocation/system_geolocation_source_mac.h
+++ b/services/device/public/cpp/geolocation/system_geolocation_source_mac.h
@@ -41,7 +41,7 @@
   void StopWatchingPosition() override;
 
   // Calls requestWhenInUseAuthorization from CLLocationManager.
-  void AppAttemptsToUseGeolocation() override;
+  void TrackGeolocationAttempted(const std::string& app_name) override;
 
  private:
   LocationSystemPermissionStatus GetSystemPermission() const;
diff --git a/services/device/public/cpp/geolocation/system_geolocation_source_mac.mm b/services/device/public/cpp/geolocation/system_geolocation_source_mac.mm
index aed8700..8a15fa8 100644
--- a/services/device/public/cpp/geolocation/system_geolocation_source_mac.mm
+++ b/services/device/public/cpp/geolocation/system_geolocation_source_mac.mm
@@ -110,7 +110,8 @@
   return LocationSystemPermissionStatus::kDenied;
 }
 
-void SystemGeolocationSourceMac::AppAttemptsToUseGeolocation() {
+void SystemGeolocationSourceMac::TrackGeolocationAttempted(
+    const std::string& app_name) {
 #if BUILDFLAG(IS_IOS)
   if (@available(ios 8.0, macOS 10.15, *)) {
     [location_manager_ requestWhenInUseAuthorization];
diff --git a/services/network/public/cpp/proxy_config_mojom_traits.cc b/services/network/public/cpp/proxy_config_mojom_traits.cc
index 5795671..700be7d4 100644
--- a/services/network/public/cpp/proxy_config_mojom_traits.cc
+++ b/services/network/public/cpp/proxy_config_mojom_traits.cc
@@ -4,6 +4,8 @@
 
 #include "services/network/public/cpp/proxy_config_mojom_traits.h"
 
+#include "base/debug/dump_without_crashing.h"
+#include "mojo/public/cpp/bindings/scoped_message_error_crash_key.h"
 #include "net/base/proxy_string_util.h"
 #include "url/gurl.h"
 
@@ -27,8 +29,12 @@
   if (!data.ReadRules(&rules))
     return false;
   for (const auto& rule : rules) {
-    if (!out_proxy_bypass_rules->AddRuleFromString(rule))
+    if (!out_proxy_bypass_rules->AddRuleFromString(rule)) {
+      mojo::debug::ScopedMessageErrorCrashKey crash_key_value(
+          "AddRuleFromString fault");
+      base::debug::DumpWithoutCrashing();
       return false;
+    }
   }
   return true;
 }
@@ -51,8 +57,12 @@
     return false;
   for (const auto& proxy : proxies) {
     net::ProxyServer proxy_server = net::PacResultElementToProxyServer(proxy);
-    if (!proxy_server.is_valid())
+    if (!proxy_server.is_valid()) {
+      mojo::debug::ScopedMessageErrorCrashKey crash_key_value(
+          "!proxy_server.is_valid()");
+      base::debug::DumpWithoutCrashing();
       return false;
+    }
     out_proxy_list->AddProxyServer(proxy_server);
   }
   return true;
diff --git a/testing/buildbot/chromium.chromiumos.json b/testing/buildbot/chromium.chromiumos.json
index 7235a06..d170b86 100644
--- a/testing/buildbot/chromium.chromiumos.json
+++ b/testing/buildbot/chromium.chromiumos.json
@@ -5669,9 +5669,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -5682,8 +5682,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -5834,9 +5834,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -5847,8 +5847,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -5981,9 +5981,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -5994,8 +5994,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/chromium.coverage.json b/testing/buildbot/chromium.coverage.json
index 5d2b261..d8d827d0 100644
--- a/testing/buildbot/chromium.coverage.json
+++ b/testing/buildbot/chromium.coverage.json
@@ -25493,9 +25493,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -25506,8 +25506,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -25658,9 +25658,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -25671,8 +25671,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -25805,9 +25805,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -25818,8 +25818,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/chromium.fyi.json b/testing/buildbot/chromium.fyi.json
index 24db1695..612331e 100644
--- a/testing/buildbot/chromium.fyi.json
+++ b/testing/buildbot/chromium.fyi.json
@@ -38426,9 +38426,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -38438,8 +38438,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -38591,9 +38591,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -38603,8 +38603,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -38738,9 +38738,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -38750,8 +38750,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -40215,9 +40215,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -40227,8 +40227,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -40380,9 +40380,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -40392,8 +40392,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -40527,9 +40527,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -40539,8 +40539,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -41275,9 +41275,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -41287,8 +41287,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/chromium.memory.json b/testing/buildbot/chromium.memory.json
index 6a51f841..c2a51b6c 100644
--- a/testing/buildbot/chromium.memory.json
+++ b/testing/buildbot/chromium.memory.json
@@ -18080,12 +18080,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -18096,8 +18096,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -18265,12 +18265,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -18281,8 +18281,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
@@ -18427,12 +18427,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 116.0.5828.0",
+        "description": "Run with ash-chrome version 116.0.5829.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -18443,8 +18443,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v116.0.5828.0",
-              "revision": "version:116.0.5828.0"
+              "location": "lacros_version_skew_tests_v116.0.5829.0",
+              "revision": "version:116.0.5829.0"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/variants.pyl b/testing/buildbot/variants.pyl
index 76432ce..1b19ba8 100644
--- a/testing/buildbot/variants.pyl
+++ b/testing/buildbot/variants.pyl
@@ -22,16 +22,16 @@
   },
   'LACROS_VERSION_SKEW_CANARY': {
     'args': [
-      '--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5828.0/test_ash_chrome',
+      '--ash-chrome-path-override=../../lacros_version_skew_tests_v116.0.5829.0/test_ash_chrome',
     ],
-    'description': 'Run with ash-chrome version 116.0.5828.0',
+    'description': 'Run with ash-chrome version 116.0.5829.0',
     'identifier': 'Lacros version skew testing ash canary',
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/linux-ash-chromium/x86_64/ash.zip',
-          'location': 'lacros_version_skew_tests_v116.0.5828.0',
-          'revision': 'version:116.0.5828.0',
+          'location': 'lacros_version_skew_tests_v116.0.5829.0',
+          'revision': 'version:116.0.5829.0',
         },
       ],
     },
diff --git a/testing/libfuzzer/getting_started.md b/testing/libfuzzer/getting_started.md
index be4a1fa..6879da3c 100644
--- a/testing/libfuzzer/getting_started.md
+++ b/testing/libfuzzer/getting_started.md
@@ -8,70 +8,78 @@
 
 ## Getting started
 
-### Setting up your build environment
+### Simple Example
 
-Generate build files by using the `use_libfuzzer` [GN] argument together with a
-sanitizer. Pick the [GN config] that corresponds to the DUT you're deploying to:
+Before writing any code let us look at a simple
+examples of a test that uses input fuzzing. The test is setup to exercise the
+[`CreateFnmatchQuery`](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ash/extensions/file_manager/search_by_pattern.h;drc=4bc4bcef0ab5581a5a27cea986296739582243a6)
+function. The role of this function is to take a user query and produce
+a case-insensitive pattern that matches file names containing the
+query in them. For example, for a query "1abc" the function generates
+"\*1[aA][bB][cC]\*". Unlike a traditional test, an input fuzzing test does not
+care about the output of the tested function. Instead it verifies that that no
+matter what string the user enters `CreateFnmatchQuery` does not do something
+unexpected, such as a crash, overriding a memory region, etc. The test
+[create_fnmatch_query_fuzzer.cc](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ash/extensions/file_manager/create_fnmatch_query_fuzzer.cc;drc=1f5a5af3eb1bbdf9e4566c3e6d2051e68de112eb)
+is shown below:
 
-```bash
-# AddressSanitizer is the default config we recommend testing with.
-# Linux:
-tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Upload Linux ASan' out/libfuzzer
-# Chrome OS:
-tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Upload Chrome OS ASan' out/libfuzzer
-# Mac:
-tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Upload Mac ASan' out/libfuzzer
-# Windows:
-python tools\mb\mb.py gen -m chromium.fuzz -b "Libfuzzer Upload Windows ASan" out\libfuzzer
+```cpp
+#include <stddef.h>
+#include <stdint.h>
+
+#include <string>
+
+#include "chrome/browser/ash/extensions/file_manager/search_by_pattern.h"
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  std::string str = std::string(reinterpret_cast<const char*>(data), size);
+  extensions::CreateFnmatchQuery(str);
+  return 0;
+}
 ```
 
-If testing things locally these are the recommended configurations
+The code starts by including `stddef.h` for `size_t` definition, `stdint.h`
+for `uint8_t` definition, `string` for `std::string` definition and finally
+the file where `extensions::CreateFnmatchQuery` function is defined. Next
+it declares and defines the `LLVMFuzzerTestOneInput` function, which is
+the function called by the testing framework. The function is supplied with two
+arguments, a pointer to an array of bytes, and the size of the array. These
+bytes are generated by the fuzzing test harness and their specific values
+are irrelevant. The job of the test is to convert those bytes to input
+parameters of the tested function. In our case bytes are converted
+to a `std::string` and given to the `CreateFnmatchQuery` function. If
+the function completes its job and the code successfully returns, the
+`LLVMFuzzerTestOneInput` function returns 0, signaling a successful execution.
 
-```bash
-# AddressSanitizer is the default config we recommend testing with.
-# Linux:
-tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Local Linux ASan' out/libfuzzer
-# Chrome OS:
-tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Local Chrome OS ASan' out/libfuzzer
-# Mac:
-tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Local Mac ASan' out/libfuzzer
-# Windows:
-python tools\mb\mb.py gen -m chromium.fuzz -b "Libfuzzer Local Windows ASan" out\libfuzzer
+The above pattern is typical to fuzzing tests. You create a
+`LLVMFuzzerTestOneInput` function. You then write code that uses the provided
+random bytes to form input parameters to the function you intend to test. Next,
+you call the function, and if it successfully completes, return 0.
+
+To run this test we need to create a `fuzzer_test` target in the appropriate
+`BUILD.gn` file. For the above example, the target is defined as
+
+```python
+fuzzer_test("create_fnmatch_query_fuzzer") {
+  sources = [ "extensions/file_manager/create_fnmatch_query_fuzzer.cc" ]
+  deps = [
+    ":ash",
+    "//base",
+    "//chrome/browser",
+    "//components/exo/wayland:ui_controls_protocol",
+    "//components/exo/wayland:weston_test",
+  ]
+}
 ```
-
-*** note
-**Note:** The above invocations may set `use_remoteexec` or `use_rbe` to true.
-However, these args aren't compatible on local workstations yet. So if you run
-into reclient errors when building locally, remove both those args and set
-`use_goma` instead.
-
-You can also invoke [AFL] by using the `use_afl` GN argument, but we
-recommend libFuzzer for local development. Running libFuzzer locally doesn't
-require any special configuration and gives quick, meaningful output for speed,
-coverage, and other parameters.
-***
-
-It’s possible to run fuzz targets without sanitizers, but not recommended, as
-sanitizers help to detect errors which may not result in a crash otherwise.
-`use_libfuzzer` is supported in the following sanitizer configurations.
-
-| GN Argument | Description | Supported OS |
-|-------------|-------------|--------------|
-| `is_asan=true` | Enables [AddressSanitizer] to catch problems like buffer overruns. | Linux, Windows, Mac, Chrome OS |
-| `is_msan=true` | Enables [MemorySanitizer] to catch problems like uninitialized reads<sup>\[[\*](reference.md#MSan)\]</sup>. | Linux |
-| `is_ubsan_security=true` | Enables [UndefinedBehaviorSanitizer] to catch<sup>\[[\*](reference.md#UBSan)\]</sup> undefined behavior like integer overflow.| Linux |
-
-For more on builder and sanitizer configurations, see the [Integration
-Reference] page.
-
-*** note
-**Hint**: Fuzz targets are built with minimal symbols by default. You can adjust
-the symbol level by setting the `symbol_level` attribute.
-***
+The source field typically specified just the file that contains the test. The
+dependencies are specific to the tested function. Here we are listing them for
+the completeness. In your test all but `//base` dependencies are unlikely to be
+required.
 
 ### Creating your first fuzz target
 
-After you set up your build environment, you can create your first fuzz target:
+Having seen a concrete example, let us describe the generic flow of steps to
+create a new fuzzing test.
 
 1. In the same directory as the code you are going to fuzz (or next to the tests
    for that code), create a new `<my_fuzzer>.cc` file.
@@ -112,21 +120,91 @@
 target.
 ***
 
+Once you created your first fuzz target, in order to run it, you must set up
+your build environment. This is described next.
+
+### Setting up your build environment
+
+Generate build files by using the `use_libfuzzer` [GN] argument together with a
+sanitizer. Rather than generating a GN build configuration by hand, we recommend
+that you run the meta-builder tool using [GN config] that corresponds to the
+operating system of the DUT you're deploying to:
+
+```bash
+# AddressSanitizer is the default config we recommend testing with.
+# Linux:
+tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Upload Linux ASan' out/libfuzzer
+# Chrome OS:
+tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Upload Chrome OS ASan' out/libfuzzer
+# Mac:
+tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Upload Mac ASan' out/libfuzzer
+# Windows:
+python tools\mb\mb.py gen -m chromium.fuzz -b "Libfuzzer Upload Windows ASan" out\libfuzzer
+```
+
+If testing things locally these are the recommended configurations
+
+```bash
+# AddressSanitizer is the default config we recommend testing with.
+# Linux:
+tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Local Linux ASan' out/libfuzzer
+# Chrome OS:
+tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Local Chrome OS ASan' out/libfuzzer
+# Mac:
+tools/mb/mb.py gen -m chromium.fuzz -b 'Libfuzzer Local Mac ASan' out/libfuzzer
+# Windows:
+python tools\mb\mb.py gen -m chromium.fuzz -b "Libfuzzer Local Windows ASan" out\libfuzzer
+```
+
+[`tools/mb/mb.py`](https://source.chromium.org/chromium/chromium/src/+/main:tools/mb/mb.py;drc=c771c017eca9a6a859d245be54c511acafdc9867)
+is "a wrapper script for GN that [..] generate[s] build files for sets of
+canned configurations." The `-m` flag selects the builder group, while the
+`-b` flag selects a specific builder in the builder group. The `out/libfuzzer`
+is the directory to which GN configuration is written. If you wish, you can
+inspect the generated config by running `gn args out/libfuzzer`, once the
+`mb.py` script is done.
+
+*** note
+**Note:** The above invocations may set `use_remoteexec` or `use_rbe` to true.
+However, these args aren't compatible on local workstations yet. So if you run
+into reclient errors when building locally, remove both those args and set
+`use_goma` instead.
+
+You can also invoke [AFL] by using the `use_afl` GN argument, but we
+recommend libFuzzer for local development. Running libFuzzer locally doesn't
+require any special configuration and gives quick, meaningful output for speed,
+coverage, and other parameters.
+***
+
+It’s possible to run fuzz targets without sanitizers, but not recommended, as
+sanitizers help to detect errors which may not result in a crash otherwise.
+`use_libfuzzer` is supported in the following sanitizer configurations.
+
+| GN Argument | Description | Supported OS |
+|-------------|-------------|--------------|
+| `is_asan=true` | Enables [AddressSanitizer] to catch problems like buffer overruns. | Linux, Windows, Mac, Chrome OS |
+| `is_msan=true` | Enables [MemorySanitizer] to catch problems like uninitialized reads<sup>\[[\*](reference.md#MSan)\]</sup>. | Linux |
+| `is_ubsan_security=true` | Enables [UndefinedBehaviorSanitizer] to catch<sup>\[[\*](reference.md#UBSan)\]</sup> undefined behavior like integer overflow.| Linux |
+
+For more on builder and sanitizer configurations, see the [Integration
+Reference] page.
+
+*** note
+**Hint**: Fuzz targets are built with minimal symbols by default. You can adjust
+the symbol level by setting the `symbol_level` attribute.
+***
+
 ### Running the fuzz target
 
-After you create your fuzz target, build it with autoninja and run it locally. In
-most cases you don't want to commit your locally generated corpus, so save it
-somewhere like `/tmp/corpus`.
+After you create your fuzz target, build it with autoninja and run it locally.
+To make this example concrete, we are going to use the existing 
+`create_fnmatch_query_fuzzer` target.
 
 ```bash
 # Build the fuzz target.
-autoninja -C out/libfuzzer url_parse_fuzzer
-# Create an empty corpus directory.
-mkdir /tmp/corpus
+autoninja -C chrome/browser/ash:create_fnmatch_query_fuzzer
 # Run the fuzz target.
-./out/libfuzzer/url_parse_fuzzer /tmp/corpus
-# If have other corpus directories, pass their paths as well:
-./out/libfuzzer/url_parse_fuzzer /tmp/corpus seed_corpus_dir_1 seed_corpus_dir_N
+./out/libfuzzer/create_fnmatch_query_fuzzer
 ```
 
 Your fuzz target should produce output like this:
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 6d78f4c1..f4cb4c40 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -792,11 +792,26 @@
             ],
             "experiments": [
                 {
+                    "name": "Enabled_Dogfood",
+                    "enable_features": [
+                        "ArcVmmSwapPolicy",
+                        "ArcvmSwapoutKeyboardShortcut"
+                    ],
+                    "min_os_version": "15485.0.0"
+                }
+            ]
+        },
+        {
+            "platforms": [
+                "chromeos"
+            ],
+            "experiments": [
+                {
                     "name": "Enabled",
                     "enable_features": [
                         "ArcVmmSwapPolicy"
                     ],
-                    "min_os_version": "15474.2.0"
+                    "min_os_version": "15485.0.0"
                 }
             ]
         }
@@ -870,6 +885,26 @@
             ]
         }
     ],
+    "AsyncCacheLock": [
+        {
+            "platforms": [
+                "android",
+                "chromeos",
+                "chromeos_lacros",
+                "linux",
+                "mac",
+                "windows"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "AsyncCacheLock"
+                    ]
+                }
+            ]
+        }
+    ],
     "AsyncDnsLinux": [
         {
             "platforms": [
@@ -10170,6 +10205,21 @@
             ]
         }
     ],
+    "PdfOcr": [
+        {
+            "platforms": [
+                "chromeos"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "PdfOcr"
+                    ]
+                }
+            ]
+        }
+    ],
     "PdfUseSkiaRenderer": [
         {
             "platforms": [
@@ -10997,6 +11047,9 @@
                     "name": "Enabled",
                     "enable_features": [
                         "ProcessPerSiteUpToMainFrameThreshold"
+                    ],
+                    "disable_features": [
+                        "AllowDevToolsMainThreadDebuggerForMultipleMainFrames"
                     ]
                 }
             ]
@@ -12671,25 +12724,14 @@
             ],
             "experiments": [
                 {
-                    "name": "FindRegistrationImprovements_20230222",
+                    "name": "FindReg_20230609",
                     "enable_features": [
+                        "ServiceWorkerFetchResponseCallbackUseHighPriority",
                         "ServiceWorkerMergeFindRegistrationForClientUrl",
+                        "ServiceWorkerRegistrationCache",
                         "ServiceWorkerScopeCache",
                         "ServiceWorkerStorageControlOnThreadPool",
                         "ServiceWorkerStorageControlResponseUseHighPriority"
-                    ],
-                    "disable_features": [
-                        "SpeculativeServiceWorkerStartup"
-                    ]
-                },
-                {
-                    "name": "FindRegistrationImprovements_20221130",
-                    "enable_features": [
-                        "ServiceWorkerStorageControlOnThreadPool",
-                        "ServiceWorkerStorageControlResponseUseHighPriority"
-                    ],
-                    "disable_features": [
-                        "SpeculativeServiceWorkerStartup"
                     ]
                 }
             ]
diff --git a/third_party/blink/public/mojom/service_worker/service_worker.mojom b/third_party/blink/public/mojom/service_worker/service_worker.mojom
index d1250c6..3470b82 100644
--- a/third_party/blink/public/mojom/service_worker/service_worker.mojom
+++ b/third_party/blink/public/mojom/service_worker/service_worker.mojom
@@ -27,6 +27,7 @@
 import "third_party/blink/public/mojom/service_worker/service_worker_fetch_response_callback.mojom";
 import "third_party/blink/public/mojom/service_worker/service_worker_object.mojom";
 import "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom";
+import "third_party/blink/public/mojom/service_worker/service_worker_router_rule.mojom";
 import "third_party/blink/public/mojom/storage_key/storage_key.mojom";
 import "url/mojom/origin.mojom";
 import "url/mojom/url.mojom";
@@ -112,6 +113,13 @@
   // On success, |error| is kNone without |error_msg| set.
   // Otherwise, |error| and |error_msg| describe the failure.
   ClaimClients() => (ServiceWorkerErrorType error, string? error_msg);
+
+  // Experimental feature.
+  // Registers the service worker router rules, which are evaluated before
+  // the regular navigation path. i.e. evaluated before the service worker
+  // fetch handler is invoked. The method may only be called once.
+  // https://github.com/yoshisatoyanagisawa/service-worker-static-routing-api
+  RegisterRouter(ServiceWorkerRouterRules rules) => ();
 };
 
 struct ExtendableMessageEvent {
diff --git a/third_party/blink/renderer/core/dom/child_node_part.h b/third_party/blink/renderer/core/dom/child_node_part.h
index 6e6f8f68..1e00d4a6 100644
--- a/third_party/blink/renderer/core/dom/child_node_part.h
+++ b/third_party/blink/renderer/core/dom/child_node_part.h
@@ -22,7 +22,7 @@
 // Implementation of the ChildNodePart class, which is part of the DOM Parts
 // API. A ChildNodePart stores a reference to a range of nodes within the
 // children of a single parent |Node| in the DOM tree.
-class CORE_EXPORT ChildNodePart : public Part {
+class CORE_EXPORT ChildNodePart : public PartRoot {
   DEFINE_WRAPPERTYPEINFO();
 
  public:
@@ -54,11 +54,12 @@
     return HeapVector<Member<Node>>();
   }
   // TODO(crbug.com/1453291) Implement this method.
-  HeapVector<Member<Part>> getParts() const {
+  HeapVector<Member<Part>> getParts() override {
     return HeapVector<Member<Part>>();
   }
+
   // TODO(crbug.com/1453291) Implement this method.
-  DocumentPart* clone() const { return nullptr; }
+  PartRoot* root() const override { return nullptr; }
   // TODO(crbug.com/1453291) Implement this method.
   void replaceChildren(const HeapVector<Member<V8UnionNodeOrString>>& nodes) {}
 
diff --git a/third_party/blink/renderer/core/dom/child_node_part.idl b/third_party/blink/renderer/core/dom/child_node_part.idl
index f1eafa70a..228e94a 100644
--- a/third_party/blink/renderer/core/dom/child_node_part.idl
+++ b/third_party/blink/renderer/core/dom/child_node_part.idl
@@ -5,12 +5,10 @@
 // https://github.com/tbondwilkinson/dom-parts
 
 [RuntimeEnabled=DOMPartsAPI,Exposed=Window]
-interface ChildNodePart : Part {
+interface ChildNodePart : PartRoot {
   constructor(Node previousSibling, Node nextSibling, optional NodePartInit init = {});
   readonly attribute Node previousSibling;
   readonly attribute Node nextSibling;
   readonly attribute FrozenArray<Node> children;
-  FrozenArray<Part> getParts();
-  ChildNodePart clone();
   void replaceChildren((Node or DOMString)... nodes);
 };
diff --git a/third_party/blink/renderer/core/dom/document_part.cc b/third_party/blink/renderer/core/dom/document_part.cc
index 10e5c5d..1bade56a 100644
--- a/third_party/blink/renderer/core/dom/document_part.cc
+++ b/third_party/blink/renderer/core/dom/document_part.cc
@@ -8,8 +8,8 @@
 #include "third_party/blink/renderer/bindings/core/v8/v8_union_document_documentfragment.h"
 #include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/dom/document_fragment.h"
-#include "third_party/blink/renderer/core/dom/part_root.h"
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
+#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
 
 namespace blink {
 
diff --git a/third_party/blink/renderer/core/dom/document_part.h b/third_party/blink/renderer/core/dom/document_part.h
index 9476584..cda1051 100644
--- a/third_party/blink/renderer/core/dom/document_part.h
+++ b/third_party/blink/renderer/core/dom/document_part.h
@@ -8,6 +8,7 @@
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/dom/part_root.h"
 #include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
+#include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 #include "third_party/blink/renderer/platform/heap/member.h"
 
@@ -15,10 +16,11 @@
 
 class Document;
 class DocumentFragment;
+class Part;
 class V8UnionDocumentOrDocumentFragment;
 
 // Implementation of the DocumentPart class, which is part of the DOM Parts API.
-// A DocumentPart represents the PartRoot for a |Document| object.
+// A DocumentPart holds the parts for a Document or DocumentFragment object.
 class CORE_EXPORT DocumentPart : public PartRoot {
   DEFINE_WRAPPERTYPEINFO();
 
@@ -38,7 +40,13 @@
   void Trace(Visitor*) const override;
 
   // DocumentPart API
+  // TODO(crbug.com/1453291) Implement this method.
+  PartRoot* root() const override { return nullptr; }
   DocumentPart* clone() const;
+  // TODO(crbug.com/1453291) Implement this method.
+  HeapVector<Member<Part>> getParts() override {
+    return HeapVector<Member<Part>>();
+  }
 
  private:
   Member<Document> document_;
diff --git a/third_party/blink/renderer/core/dom/node_part.h b/third_party/blink/renderer/core/dom/node_part.h
index 50e4c1c..854aa84 100644
--- a/third_party/blink/renderer/core/dom/node_part.h
+++ b/third_party/blink/renderer/core/dom/node_part.h
@@ -13,6 +13,8 @@
 
 namespace blink {
 
+class PartRoot;
+
 // Implementation of the NodePart class, which is part of the DOM Parts API.
 // A NodePart stores a reference to a single |Node| in the DOM tree.
 class CORE_EXPORT NodePart : public Part {
@@ -34,6 +36,8 @@
 
   // NodePart API
   Node* node() const { return node_; }
+  // TODO(crbug.com/1453291) Implement this method.
+  PartRoot* root() const override { return nullptr; }
 
  private:
   Member<Node> node_;
diff --git a/third_party/blink/renderer/core/dom/part.h b/third_party/blink/renderer/core/dom/part.h
index eef6fdfc..adb1284 100644
--- a/third_party/blink/renderer/core/dom/part.h
+++ b/third_party/blink/renderer/core/dom/part.h
@@ -25,10 +25,9 @@
   ~Part() override = default;
 
   // Part API
+  virtual PartRoot* root() const = 0;
 
   // TODO(crbug.com/1453291) Implement this method.
-  PartRoot* root() const { return nullptr; }
-  // TODO(crbug.com/1453291) Implement this method.
   HeapVector<String> metadata() const { return HeapVector<String>(); }
 
   void disconnect() {}
diff --git a/third_party/blink/renderer/core/dom/part_root.h b/third_party/blink/renderer/core/dom/part_root.h
index 39dd7336..430eece 100644
--- a/third_party/blink/renderer/core/dom/part_root.h
+++ b/third_party/blink/renderer/core/dom/part_root.h
@@ -5,34 +5,27 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_DOM_PART_ROOT_H_
 #define THIRD_PARTY_BLINK_RENDERER_CORE_DOM_PART_ROOT_H_
 
+#include "third_party/blink/renderer/bindings/core/v8/v8_part_root.h"
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/dom/part.h"
-#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
+#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+#include "third_party/blink/renderer/platform/heap/member.h"
 
 namespace blink {
 
 // Implementation of the PartRoot class, which is part of the DOM Parts API.
-// A PartRoot can exist for a |Document| or |DocumentFragment|, and this the
-// entrypoint for the |getParts()| interface, which queries for contained parts.
-class CORE_EXPORT PartRoot : public ScriptWrappable {
+// A PartRoot adds getParts to Part.
+class CORE_EXPORT PartRoot : public Part {
   DEFINE_WRAPPERTYPEINFO();
 
  public:
+  PartRoot() = default;
   PartRoot(const PartRoot&) = delete;
   ~PartRoot() override = default;
 
-  void Trace(Visitor* visitor) const override {
-    ScriptWrappable::Trace(visitor);
-  }
-
   // PartRoot API
-
-  // TODO(crbug.com/1453291) Implement this method.
-  HeapVector<Member<Part>> getParts() { return HeapVector<Member<Part>>(); }
-
- protected:
-  PartRoot() = default;
+  virtual HeapVector<Member<Part>> getParts() = 0;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/dom/part_root.idl b/third_party/blink/renderer/core/dom/part_root.idl
index 99b4342..5859930 100644
--- a/third_party/blink/renderer/core/dom/part_root.idl
+++ b/third_party/blink/renderer/core/dom/part_root.idl
@@ -5,6 +5,6 @@
 // https://github.com/tbondwilkinson/dom-parts/blob/main/README.md
 
 [RuntimeEnabled=DOMPartsAPI,Exposed=Window]
-interface PartRoot {
+interface PartRoot : Part {
   FrozenArray<Part> getParts();
 };
diff --git a/third_party/blink/renderer/core/inspector/main_thread_debugger.cc b/third_party/blink/renderer/core/inspector/main_thread_debugger.cc
index 3e4c70b..afe62a4 100644
--- a/third_party/blink/renderer/core/inspector/main_thread_debugger.cc
+++ b/third_party/blink/renderer/core/inspector/main_thread_debugger.cc
@@ -31,9 +31,11 @@
 #include "third_party/blink/renderer/core/inspector/main_thread_debugger.h"
 
 #include <memory>
+#include <set>
 
 #include "base/feature_list.h"
 #include "base/synchronization/lock.h"
+#include "base/unguessable_token.h"
 #include "build/chromeos_buildflags.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/renderer/bindings/core/v8/binding_security.h"
@@ -346,14 +348,14 @@
     return true;
   }
 
-  size_t num_main_frames = 0;
+  std::set<base::UnguessableToken> browsing_context_group_tokens;
   for (auto& page : Page::OrdinaryPages()) {
     if (page->MainFrame() && page->MainFrame()->IsOutermostMainFrame()) {
-      ++num_main_frames;
+      browsing_context_group_tokens.insert(page->BrowsingContextGroupToken());
     }
   }
 
-  if (num_main_frames > 1) {
+  if (browsing_context_group_tokens.size() > 1) {
     String message = String(
         "DevTools debugger is disabled because it is attached to a process "
         "that hosts multiple top-level frames, where DevTools debugger doesn't "
diff --git a/third_party/blink/renderer/core/intersection_observer/intersection_geometry.cc b/third_party/blink/renderer/core/intersection_observer/intersection_geometry.cc
index 31a6a0a..d864dc4 100644
--- a/third_party/blink/renderer/core/intersection_observer/intersection_geometry.cc
+++ b/third_party/blink/renderer/core/intersection_observer/intersection_geometry.cc
@@ -333,8 +333,7 @@
   RootAndTarget root_and_target(root_node, target_element);
 
   if (ShouldUseCachedRects()) {
-    CHECK(!RootIsImplicit() ||
-          RuntimeEnabledFeatures::IntersectionOptimizationEnabled());
+    CHECK(!RootIsImplicit());
     // Cached rects can only be used if there are no scrollable objects in the
     // hierarchy between target and root (a scrollable root is ok). The reason
     // is that a scroll change in an intermediate scroller would change the
diff --git a/third_party/blink/renderer/core/intersection_observer/intersection_observation.cc b/third_party/blink/renderer/core/intersection_observer/intersection_observation.cc
index e33cdd2a..1703015 100644
--- a/third_party/blink/renderer/core/intersection_observer/intersection_observation.cc
+++ b/third_party/blink/renderer/core/intersection_observer/intersection_observation.cc
@@ -39,11 +39,8 @@
       // should be -1, but since last_threshold_index_ is unsigned, we use a
       // different sentinel value.
       last_threshold_index_(kMaxThresholdIndex - 1) {
-  if (!observer.RootIsImplicit() ||
-      RuntimeEnabledFeatures::IntersectionOptimizationEnabled()) {
-    // TODO(crbug.com/1400495): Avoid unique_ptr for IntersectionOptimization.
+  if (!observer.RootIsImplicit())
     cached_rects_ = std::make_unique<IntersectionGeometry::CachedRects>();
-  }
 }
 
 int64_t IntersectionObservation::ComputeIntersection(
@@ -67,8 +64,7 @@
       [this](unsigned geometry_flags) {
         return IntersectionGeometry(
             observer_->root(), *Target(), observer_->RootMargin(),
-            observer_->thresholds(), observer_->TargetMargin(), geometry_flags,
-            cached_rects_.get());
+            observer_->thresholds(), observer_->TargetMargin(), geometry_flags);
       },
       compute_flags, monotonic_time);
 }
diff --git a/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc b/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc
index 934ba5d..152fdcc5 100644
--- a/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc
+++ b/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc
@@ -1126,59 +1126,6 @@
   }
 }
 
-TEST_P(IntersectionObserverTest, CachedRectsImplicitRoot) {
-  WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
-  SimRequest main_resource("https://example.com/", "text/html");
-  SimRequest iframe_resource("https://example.com/iframe.html", "text/html");
-  LoadURL("https://example.com/");
-  main_resource.Complete(R"HTML(
-    <div id="target-in-main">Hello, world!</div>
-    <iframe src="iframe.html" style="width:200px; height:100px"></iframe>
-    <div id='spacer' style='height:2000px'></div>
-  )HTML");
-  iframe_resource.Complete(R"HTML(
-    <div id='target-in-frame'>Hello, world!</div>
-    <div id='spacer' style='height:2000px'></div>
-  )HTML");
-
-  auto do_nothing = base::DoNothingAs<void(
-      const HeapVector<Member<IntersectionObserverEntry>>&)>();
-  IntersectionObserver* observer_in_main = IntersectionObserver::Create(
-      {}, {}, &GetDocument(), do_nothing, LocalFrameUkmAggregator::kLayout);
-  Document* iframe_document = To<WebLocalFrameImpl>(MainFrame().FirstChild())
-                                  ->GetFrame()
-                                  ->GetDocument();
-  Element* target_in_main = GetDocument().getElementById("target-in-main");
-  DummyExceptionStateForTesting exception_state;
-  observer_in_main->observe(target_in_main, exception_state);
-  ASSERT_FALSE(exception_state.HadException());
-
-  IntersectionObserver* observer_in_frame = IntersectionObserver::Create(
-      {}, {}, iframe_document, do_nothing, LocalFrameUkmAggregator::kLayout);
-  Element* target_in_frame = iframe_document->getElementById("target-in-frame");
-  observer_in_frame->observe(target_in_frame, exception_state);
-  ASSERT_FALSE(exception_state.HadException());
-
-  IntersectionObservation* observation_in_main =
-      target_in_main->IntersectionObserverData()->GetObservationFor(
-          *observer_in_main);
-  EXPECT_FALSE(observation_in_main->CanUseCachedRectsForTesting());
-  IntersectionObservation* observation_in_frame =
-      target_in_frame->IntersectionObserverData()->GetObservationFor(
-          *observer_in_frame);
-  EXPECT_FALSE(observation_in_frame->CanUseCachedRectsForTesting());
-
-  // Generate initial notifications and populate cache.
-  Compositor().BeginFrame();
-  test::RunPendingTasks();
-
-  if (RuntimeEnabledFeatures::IntersectionOptimizationEnabled()) {
-    EXPECT_TRUE(observation_in_main->CanUseCachedRectsForTesting());
-  } else {
-    EXPECT_FALSE(observation_in_main->CanUseCachedRectsForTesting());
-  }
-}
-
 TEST_P(IntersectionObserverTest, MinScrollDeltaToUpdateThresholdZero) {
   if (!RuntimeEnabledFeatures::IntersectionOptimizationEnabled()) {
     return;
diff --git a/third_party/blink/renderer/core/layout/build.gni b/third_party/blink/renderer/core/layout/build.gni
index 074fe4e8..8e84e8e 100644
--- a/third_party/blink/renderer/core/layout/build.gni
+++ b/third_party/blink/renderer/core/layout/build.gni
@@ -315,6 +315,8 @@
   "ng/inline/ng_line_truncator.h",
   "ng/inline/ng_line_utils.cc",
   "ng/inline/ng_line_utils.h",
+  "ng/inline/ng_line_widths.cc",
+  "ng/inline/ng_line_widths.h",
   "ng/inline/ng_logical_line_item.cc",
   "ng/inline/ng_logical_line_item.h",
   "ng/inline/ng_offset_mapping.cc",
@@ -749,6 +751,7 @@
   "ng/inline/ng_line_breaker_test.cc",
   "ng/inline/ng_line_break_candidate_test.cc",
   "ng/inline/ng_line_info_list_test.cc",
+  "ng/inline/ng_line_widths_test.cc",
   "ng/inline/ng_offset_mapping_test.cc",
   "ng/inline/ng_paragraph_line_breaker_test.cc",
   "ng/inline/ng_physical_line_box_fragment_test.cc",
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.cc b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.cc
index 2daa1cf..588a5bf 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.cc
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.cc
@@ -227,8 +227,9 @@
     GridTrackSizingDirection track_direction,
     bool has_synthesized_baseline) {
   // Alignment fallback is only possible when baseline alignment is specified.
-  if (!IsBaselineSpecifiedForDirection(track_direction))
+  if (!IsBaselineSpecified(track_direction)) {
     return;
+  }
 
   auto CanParticipateInBaselineAlignment = [&]() -> bool {
     // "If baseline alignment is specified on a grid item whose size in that
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.h b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.h
index ab6bf3d8..6d45f68 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.h
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.h
@@ -56,38 +56,37 @@
         is_block_axis_overflow_safe);
   }
 
-  bool IsBaselineAlignedForDirection(
-      GridTrackSizingDirection track_direction) const {
-    // TODO(ethavar): Baseline alignment for subgrids is dependent on
-    // accumulating the baseline in `ComputeSubgridContributionSize`.
-    if (has_subgridded_columns || has_subgridded_rows ||
-        is_subgridded_to_parent_grid) {
+  bool IsBaselineAligned(GridTrackSizingDirection track_direction) const {
+    const bool is_for_columns = track_direction == kForColumns;
+    const bool has_subgridded_axis =
+        is_for_columns ? has_subgridded_columns : has_subgridded_rows;
+
+    if (has_subgridded_axis) {
       return false;
     }
-    return (track_direction == kForColumns)
-               ? (InlineAxisAlignment() == AxisEdge::kFirstBaseline ||
-                  InlineAxisAlignment() == AxisEdge::kLastBaseline)
-               : (BlockAxisAlignment() == AxisEdge::kFirstBaseline ||
-                  BlockAxisAlignment() == AxisEdge::kLastBaseline);
+
+    const auto axis_alignment =
+        is_for_columns ? InlineAxisAlignment() : BlockAxisAlignment();
+    return (axis_alignment == AxisEdge::kFirstBaseline ||
+            axis_alignment == AxisEdge::kLastBaseline);
   }
 
-  bool IsBaselineSpecifiedForDirection(
-      GridTrackSizingDirection track_direction) const {
-    // TODO(ethavar): Baseline alignment for subgrids is dependent on
-    // accumulating the baseline in `ComputeSubgridContributionSize`.
-    if (has_subgridded_columns || has_subgridded_rows ||
-        is_subgridded_to_parent_grid) {
+  bool IsBaselineSpecified(GridTrackSizingDirection track_direction) const {
+    const bool is_for_columns = track_direction == kForColumns;
+    const bool has_subgridded_axis =
+        is_for_columns ? has_subgridded_columns : has_subgridded_rows;
+
+    if (has_subgridded_axis) {
       return false;
     }
-    return (track_direction == kForColumns)
-               ? (inline_axis_alignment == AxisEdge::kFirstBaseline ||
-                  inline_axis_alignment == AxisEdge::kLastBaseline)
-               : (block_axis_alignment == AxisEdge::kFirstBaseline ||
-                  block_axis_alignment == AxisEdge::kLastBaseline);
+
+    const auto axis_alignment =
+        is_for_columns ? inline_axis_alignment : block_axis_alignment;
+    return (axis_alignment == AxisEdge::kFirstBaseline ||
+            axis_alignment == AxisEdge::kLastBaseline);
   }
 
-  bool IsLastBaselineSpecifiedForDirection(
-      GridTrackSizingDirection track_direction) const {
+  bool IsLastBaselineSpecified(GridTrackSizingDirection track_direction) const {
     return (track_direction == kForColumns)
                ? inline_axis_alignment == AxisEdge::kLastBaseline
                : block_axis_alignment == AxisEdge::kLastBaseline;
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc
index 3996a93..333fc6a 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc
@@ -539,8 +539,7 @@
 
     bool must_create_baselines = false;
     for (auto& grid_item : sizing_node.grid_items) {
-      must_create_baselines |=
-          grid_item.IsBaselineSpecifiedForDirection(track_direction);
+      must_create_baselines |= grid_item.IsBaselineSpecified(track_direction);
 
       auto& range_indices = grid_item.RangeIndices(track_direction);
       range_builder.EnsureTrackCoverage(grid_item.StartLine(track_direction),
@@ -1000,6 +999,28 @@
   return is_for_columns ? kForColumns : kForRows;
 }
 
+LayoutUnit GetExtraMarginForBaseline(
+    const NGBoxStrut& margins,
+    const NGSubgriddedItemData& subgridded_item,
+    GridTrackSizingDirection track_direction,
+    WritingMode writing_mode) {
+  const auto& track_collection = (track_direction == kForColumns)
+                                     ? subgridded_item.Columns(writing_mode)
+                                     : subgridded_item.Rows(writing_mode);
+  const auto& [begin_set_index, end_set_index] =
+      subgridded_item->SetIndices(track_collection.Direction());
+
+  const LayoutUnit extra_margin =
+      (subgridded_item->BaselineGroup(track_direction) == BaselineGroup::kMajor)
+          ? track_collection.StartExtraMargin(begin_set_index)
+          : track_collection.EndExtraMargin(end_set_index);
+
+  return extra_margin +
+         (subgridded_item->IsLastBaselineSpecified(track_direction)
+              ? margins.block_end
+              : margins.block_start);
+}
+
 }  // namespace
 
 LayoutUnit NGGridLayoutAlgorithm::GetLogicalBaseline(
@@ -1063,13 +1084,14 @@
     if (track_baseline == LayoutUnit::Min())
       return;
 
-    const auto item_margins =
+    const LayoutUnit extra_margin = GetExtraMarginForBaseline(
         ComputeMarginsFor(space, item_style,
-                          grid_item->BaselineWritingDirection(track_direction));
+                          grid_item->BaselineWritingDirection(track_direction)),
+        subgridded_item, track_direction, writing_mode);
 
-    // Determine the delta between the baselines; subtract out the start margin
-    // so it doesn't get added a second time at the end of this method.
-    baseline_shim = track_baseline - baseline - item_margins.block_start;
+    // Determine the delta between the baselines; subtract out the margin so it
+    // doesn't get added a second time at the end of this method.
+    baseline_shim = track_baseline - baseline - extra_margin;
   };
 
   auto SubgridContributionSize = [&](bool is_min_content) -> LayoutUnit {
@@ -1115,11 +1137,11 @@
     const auto content_size =
         is_min_content ? result.sizes.min_size : result.sizes.max_size;
 
-    if (grid_item->IsBaselineAlignedForDirection(track_direction)) {
+    if (grid_item->IsBaselineAligned(track_direction)) {
       CalculateBaselineShim(GetSynthesizedLogicalBaseline(
           content_size,
           grid_item->BaselineWritingDirection(track_direction).IsFlippedLines(),
-          grid_item->IsLastBaselineSpecifiedForDirection(track_direction)));
+          grid_item->IsLastBaselineSpecified(track_direction)));
     }
     return content_size + baseline_shim;
   };
@@ -1168,10 +1190,10 @@
         grid_item->BaselineWritingDirection(track_direction),
         To<NGPhysicalBoxFragment>(result->PhysicalFragment()));
 
-    if (grid_item->IsBaselineAlignedForDirection(track_direction)) {
+    if (grid_item->IsBaselineAligned(track_direction)) {
       CalculateBaselineShim(GetLogicalBaseline(
           baseline_fragment,
-          grid_item->IsLastBaselineSpecifiedForDirection(track_direction)));
+          grid_item->IsLastBaselineSpecified(track_direction)));
     }
     return baseline_fragment.BlockSize() + baseline_shim;
   };
@@ -1495,11 +1517,11 @@
 }
 
 void NGGridLayoutAlgorithm::ComputeGridItemBaselines(
+    const NGGridLayoutSubtree& layout_subtree,
     const NGGridSizingSubtree& sizing_subtree,
     GridTrackSizingDirection track_direction,
     SizingConstraint sizing_constraint) const {
   auto& sizing_data = sizing_subtree.SubtreeRootData();
-
   auto& track_collection =
       sizing_data.layout_data.SizingCollection(track_direction);
 
@@ -1507,16 +1529,34 @@
     return;
   }
 
+  const auto writing_mode = ConstraintSpace().GetWritingMode();
+
   track_collection.ResetBaselines();
+
+  auto next_subgrid_subtree = layout_subtree.FirstChild();
   for (auto& grid_item : sizing_data.grid_items) {
-    if (!grid_item.IsBaselineSpecifiedForDirection(track_direction) ||
+    if (!grid_item.IsBaselineSpecified(track_direction) ||
         !grid_item.IsConsideredForSizing(track_direction)) {
       continue;
     }
 
-    LogicalRect unused_grid_area;
+    NGGridLayoutSubtree subgrid_layout_subtree;
+    if (grid_item.IsSubgrid()) {
+      DCHECK(next_subgrid_subtree);
+      subgrid_layout_subtree = next_subgrid_subtree;
+      next_subgrid_subtree = next_subgrid_subtree.NextSibling();
+    }
+
+    const auto subgridded_item =
+        grid_item.is_subgridded_to_parent_grid
+            ? sizing_subtree.LookupSubgriddedItemData(grid_item)
+            : NGSubgriddedItemData(grid_item, sizing_subtree.LayoutData(),
+                                   writing_mode);
+
+    LogicalRect unused_containing_grid_area;
     const auto space = CreateConstraintSpaceForLayout(
-        grid_item, sizing_data.layout_data, &unused_grid_area);
+        *subgridded_item, subgridded_item.ParentLayoutData(),
+        &unused_containing_grid_area, std::move(subgrid_layout_subtree));
 
     // Skip this item if we aren't able to resolve our inline size.
     const auto& item_style = grid_item.node.Style();
@@ -1539,27 +1579,29 @@
         !baseline_fragment.FirstBaseline().has_value();
     grid_item.SetAlignmentFallback(track_direction, has_synthesized_baseline);
 
-    if (!grid_item.IsBaselineAlignedForDirection(track_direction))
+    if (!grid_item.IsBaselineAligned(track_direction)) {
       continue;
+    }
 
-    const auto margins =
-        ComputeMarginsFor(space, item_style, baseline_writing_direction);
+    const LayoutUnit extra_margin = GetExtraMarginForBaseline(
+        ComputeMarginsFor(space, item_style, baseline_writing_direction),
+        subgridded_item, track_direction, writing_mode);
+
     const bool is_last_baseline =
-        grid_item.IsLastBaselineSpecifiedForDirection(track_direction);
+        grid_item.IsLastBaselineSpecified(track_direction);
     const LayoutUnit baseline =
-        (is_last_baseline ? margins.block_end : margins.block_start) +
-        GetLogicalBaseline(baseline_fragment, is_last_baseline);
+        extra_margin + GetLogicalBaseline(baseline_fragment, is_last_baseline);
 
     // "If a box spans multiple shared alignment contexts, then it participates
     //  in first/last baseline alignment within its start-most/end-most shared
     //  alignment context along that axis"
     // https://www.w3.org/TR/css-align-3/#baseline-sharing-group
+    const auto& [begin_set_index, end_set_index] =
+        grid_item.SetIndices(track_direction);
     if (grid_item.BaselineGroup(track_direction) == BaselineGroup::kMajor) {
-      track_collection.SetMajorBaseline(
-          grid_item.SetIndices(track_direction).begin, baseline);
+      track_collection.SetMajorBaseline(begin_set_index, baseline);
     } else {
-      track_collection.SetMinorBaseline(
-          grid_item.SetIndices(track_direction).end - 1, baseline);
+      track_collection.SetMinorBaseline(end_set_index - 1, baseline);
     }
   }
 }
@@ -1764,9 +1806,6 @@
                                       : grid_available_size_.block_size,
                                   GutterSize(track_direction));
 
-  // Cache baselines, as these contributions can influence track sizing
-  ComputeGridItemBaselines(sizing_subtree, track_direction, sizing_constraint);
-
   // 2. Resolve intrinsic track sizing functions to absolute lengths.
   if (track_collection.HasIntrinsicTrack()) {
     ResolveIntrinsicTrackSizes(sizing_subtree, track_direction,
@@ -1869,6 +1908,10 @@
     GridTrackSizingDirection track_direction,
     SizingConstraint sizing_constraint,
     bool* opt_needs_additional_pass) const {
+  ComputeBaselineAlignment(NGGridLayoutSubtree(sizing_tree.FinalizeTree()),
+                           NGGridSizingSubtree(sizing_tree),
+                           /* opt_subgrid_data */ kNoSubgriddedItemData,
+                           track_direction, sizing_constraint);
   CompleteTrackSizingAlgorithm(NGGridSizingSubtree(sizing_tree),
                                /* opt_subgrid_data */ kNoSubgriddedItemData,
                                track_direction, sizing_constraint,
@@ -1876,22 +1919,35 @@
 }
 
 void NGGridLayoutAlgorithm::ComputeBaselineAlignment(
+    const NGGridLayoutSubtree& layout_subtree,
     const NGGridSizingSubtree& sizing_subtree,
     const NGSubgriddedItemData& opt_subgrid_data,
     const absl::optional<GridTrackSizingDirection>& opt_track_direction,
     SizingConstraint sizing_constraint) const {
   DCHECK(sizing_subtree);
 
-  auto& sizing_node = sizing_subtree.SubtreeRootData();
+  auto& layout_data = sizing_subtree.LayoutData();
 
   auto ComputeOrRecreateBaselines =
       [&](GridTrackSizingDirection track_direction) {
-        if (sizing_node.layout_data.HasSubgriddedAxis(track_direction)) {
-          // TODO(ikilpatrick): Recreate the subgrid track collection.
+        if (layout_data.HasSubgriddedAxis(track_direction)) {
           DCHECK(opt_subgrid_data.IsSubgrid());
+          // Recreate the subgrid track collection if there are baselines which
+          // need to be inherited.
+          const bool is_for_columns_in_parent =
+              opt_subgrid_data->is_parallel_with_root_grid
+                  ? track_direction == kForColumns
+                  : track_direction == kForRows;
+          const auto& parent_track_collection = is_for_columns_in_parent
+                                                    ? opt_subgrid_data.Columns()
+                                                    : opt_subgrid_data.Rows();
+          if (parent_track_collection.HasBaselines()) {
+            layout_data.SetTrackCollection(CreateSubgridTrackCollection(
+                opt_subgrid_data, track_direction));
+          }
         } else {
-          ComputeGridItemBaselines(sizing_subtree, track_direction,
-                                   sizing_constraint);
+          ComputeGridItemBaselines(layout_subtree, sizing_subtree,
+                                   track_direction, sizing_constraint);
         }
       };
 
@@ -1902,21 +1958,25 @@
     ComputeOrRecreateBaselines(kForRows);
   }
 
+  auto next_layout_subtree = layout_subtree.FirstChild();
   ForEachSubgrid(sizing_subtree,
                  [&](const NGGridLayoutAlgorithm& subgrid_algorithm,
                      const NGGridSizingSubtree& subgrid_subtree,
                      const NGSubgriddedItemData& subgrid_data) {
+                   DCHECK(next_layout_subtree);
                    subgrid_algorithm.ComputeBaselineAlignment(
-                       subgrid_subtree, subgrid_data,
+                       next_layout_subtree, subgrid_subtree, subgrid_data,
                        RelativeDirectionFilterInSubgrid(opt_track_direction,
                                                         *subgrid_data),
                        sizing_constraint);
+                   next_layout_subtree = next_layout_subtree.NextSibling();
                  });
 }
 
 void NGGridLayoutAlgorithm::CompleteFinalBaselineAlignment(
     const NGGridSizingTree& sizing_tree) const {
-  ComputeBaselineAlignment(NGGridSizingSubtree(sizing_tree),
+  ComputeBaselineAlignment(NGGridLayoutSubtree(sizing_tree.FinalizeTree()),
+                           NGGridSizingSubtree(sizing_tree),
                            /* opt_subgrid_data */ kNoSubgriddedItemData,
                            /* opt_track_direction */ absl::nullopt,
                            SizingConstraint::kLayout);
@@ -3434,8 +3494,9 @@
 
     auto BaselineOffset = [&](GridTrackSizingDirection track_direction,
                               LayoutUnit size) -> LayoutUnit {
-      if (!grid_item.IsBaselineAlignedForDirection(track_direction))
+      if (!grid_item.IsBaselineAligned(track_direction)) {
         return LayoutUnit();
+      }
 
       NGBoxFragment baseline_fragment(
           grid_item.BaselineWritingDirection(track_direction),
@@ -3446,7 +3507,7 @@
           Baseline(layout_data, grid_item, track_direction) -
           GetLogicalBaseline(
               baseline_fragment,
-              grid_item.IsLastBaselineSpecifiedForDirection(track_direction));
+              grid_item.IsLastBaselineSpecified(track_direction));
       if (grid_item.BaselineGroup(track_direction) == BaselineGroup::kMajor)
         return baseline_delta;
 
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.h b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.h
index 65e11391..73cdf2c 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.h
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.h
@@ -114,7 +114,8 @@
 
   // Determines the major/minor alignment baselines for each row/column based on
   // each item in |grid_items|, and stores the results in |track_collection|.
-  void ComputeGridItemBaselines(const NGGridSizingSubtree& sizing_subtree,
+  void ComputeGridItemBaselines(const NGGridLayoutSubtree& layout_subtree,
+                                const NGGridSizingSubtree& sizing_subtree,
                                 GridTrackSizingDirection track_direction,
                                 SizingConstraint sizing_constraint) const;
 
@@ -161,6 +162,7 @@
 
   // Performs the final baseline alignment pass of a grid sizing subtree.
   void ComputeBaselineAlignment(
+      const NGGridLayoutSubtree& layout_subtree,
       const NGGridSizingSubtree& sizing_subtree,
       const NGSubgriddedItemData& opt_subgrid_data,
       const absl::optional<GridTrackSizingDirection>& opt_track_direction,
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_sizing_tree.h b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_sizing_tree.h
index 6c6f3f6..b438887 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_sizing_tree.h
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_sizing_tree.h
@@ -64,6 +64,11 @@
                : parent_layout_data_->Columns();
   }
 
+  const NGGridLayoutData& ParentLayoutData() const {
+    DCHECK(parent_layout_data_);
+    return *parent_layout_data_;
+  }
+
  private:
   const GridItemData* item_data_in_parent_{nullptr};
   const NGGridLayoutData* parent_layout_data_{nullptr};
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_track_collection.cc b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_track_collection.cc
index ff92069..9a81b24 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_track_collection.cc
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_track_collection.cc
@@ -673,26 +673,27 @@
 
   // Copy the sets geometry and adjust its offsets to accommodate the subgrid's
   // margin, border, scrollbar, padding, and gutter size.
+  const auto subgrid_gutter_size_delta = subgrid_gutter_size - gutter_size_;
+
+  const bool is_for_columns = subgrid_track_direction == kForColumns;
+  const auto subgrid_margin_start =
+      is_for_columns ? subgrid_margin.inline_start : subgrid_margin.block_start;
+
+  const auto subgrid_border_scrollbar_padding_start =
+      is_for_columns ? subgrid_border_scrollbar_padding.inline_start
+                     : subgrid_border_scrollbar_padding.block_start;
+
+  const auto subgrid_margin_border_scrollbar_padding_start =
+      subgrid_margin_start + subgrid_border_scrollbar_padding_start;
+  const auto subgrid_margin_border_scrollbar_padding_end =
+      is_for_columns ? subgrid_margin.inline_end +
+                           subgrid_border_scrollbar_padding.inline_end
+                     : subgrid_margin.block_end +
+                           subgrid_border_scrollbar_padding.block_end;
+
+  // Accumulate the extra margin from the spanned sets in the parent track
+  // collection and this subgrid's margins and gutter size delta.
   {
-    const auto subgrid_gutter_size_delta = subgrid_gutter_size - gutter_size_;
-
-    const bool is_for_columns = subgrid_track_direction == kForColumns;
-    const auto subgrid_margin_start = is_for_columns
-                                          ? subgrid_margin.inline_start
-                                          : subgrid_margin.block_start;
-
-    const auto subgrid_border_scrollbar_padding_start =
-        is_for_columns ? subgrid_border_scrollbar_padding.inline_start
-                       : subgrid_border_scrollbar_padding.block_start;
-
-    const auto subgrid_margin_border_scrollbar_padding_end =
-        is_for_columns ? subgrid_margin.inline_end +
-                             subgrid_border_scrollbar_padding.inline_end
-                       : subgrid_margin.block_end +
-                             subgrid_border_scrollbar_padding.block_end;
-
-    // Accumulate the extra margin from the spanned sets in the parent track
-    // collection and this subgrid's margins and gutter size delta.
     subgrid_track_collection.accumulated_gutter_size_delta_ =
         subgrid_gutter_size_delta + accumulated_gutter_size_delta_;
 
@@ -703,7 +704,7 @@
 
     // Opposite direction subgrids adjust extra margin from the opposite side.
     subgrid_track_collection.accumulated_start_extra_margin_ =
-        subgrid_margin_start + subgrid_border_scrollbar_padding_start +
+        subgrid_margin_border_scrollbar_padding_start +
         (is_opposite_direction_in_root_grid
              ? EndExtraMargin(end_set_index)
              : StartExtraMargin(begin_set_index));
@@ -801,12 +802,28 @@
     subgrid_baselines.major.ReserveInitialCapacity(set_span_size);
     subgrid_baselines.minor.ReserveInitialCapacity(set_span_size);
 
+    // Adjust the baselines to accommodate the subgrid extra margins.
     for (wtf_size_t i = 0; i < set_span_size; ++i) {
+      LayoutUnit major_adjust =
+          (i == 0) ? subgrid_margin_border_scrollbar_padding_start
+                   : subgrid_gutter_size_delta / 2;
+      LayoutUnit minor_adjust =
+          (i == set_span_size - 1) ? subgrid_margin_border_scrollbar_padding_end
+                                   : subgrid_gutter_size_delta / 2;
+      if (is_opposite_direction_in_root_grid) {
+        std::swap(major_adjust, minor_adjust);
+      }
       const wtf_size_t current_index = is_opposite_direction_in_root_grid
-                                           ? end_set_index - i + 1
+                                           ? end_set_index - i - 1
                                            : begin_set_index + i;
-      subgrid_baselines.major.emplace_back(baselines_->major[current_index]);
-      subgrid_baselines.minor.emplace_back(baselines_->minor[current_index]);
+      subgrid_baselines.major.emplace_back(baselines_->major[current_index] -
+                                           major_adjust);
+      subgrid_baselines.minor.emplace_back(baselines_->minor[current_index] -
+                                           minor_adjust);
+    }
+
+    if (is_opposite_direction_in_root_grid) {
+      std::swap(subgrid_baselines.major, subgrid_baselines.minor);
     }
 
     subgrid_track_collection.baselines_ = std::move(subgrid_baselines);
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.cc b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.cc
index 79e1067..60a6d14 100644
--- a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.cc
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.cc
@@ -289,6 +289,24 @@
 }
 
 template <typename OffsetMappingBuilder>
+inline void
+NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::DidAppendForcedBreak() {
+  // Bisecting available widths can't handle multiple logical paragraphs, so
+  // forced break should disable it. See `NGParagraphLineBreaker`.
+  is_bisect_line_break_disabled_ = true;
+}
+
+template <typename OffsetMappingBuilder>
+inline void
+NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::DidAppendTextReusing(
+    const NGInlineItem& item) {
+  is_block_level_ &= item.IsBlockLevel();
+  if (item.IsForcedLineBreak()) {
+    DidAppendForcedBreak();
+  }
+}
+
+template <typename OffsetMappingBuilder>
 bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendTextReusing(
     const NGInlineNodeData& original_data,
     LayoutText* layout_text) {
@@ -435,7 +453,7 @@
     // itself may be reused.
     if (item.StartOffset() == start) {
       items_->push_back(item);
-      is_block_level_ &= item.IsBlockLevel();
+      DidAppendTextReusing(item);
       continue;
     }
 
@@ -465,7 +483,7 @@
 #endif
 
     items_->push_back(adjusted_item);
-    is_block_level_ &= adjusted_item.IsBlockLevel();
+    DidAppendTextReusing(adjusted_item);
   }
   return true;
 }
@@ -956,9 +974,7 @@
     }
   }
 
-  // Bisecting available widths can't handle multiple logical paragraphs, so
-  // forced break should disable it. See `NGParagraphLineBreaker`.
-  is_bisect_line_break_disabled_ = true;
+  DidAppendForcedBreak();
 }
 
 template <typename OffsetMappingBuilder>
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.h b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.h
index 713b038..09b4c52 100644
--- a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.h
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.h
@@ -224,6 +224,9 @@
   void ExitAndEnterSvgTextChunk(LayoutText& layout_text);
   void EnterSvgTextChunk(const ComputedStyle* style);
 
+  void DidAppendTextReusing(const NGInlineItem& item);
+  void DidAppendForcedBreak();
+
   void RemoveTrailingCollapsibleSpaceIfExists();
   void RemoveTrailingCollapsibleSpace(NGInlineItem*);
 
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h
index 3525b7a1..a9619e6 100644
--- a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h
@@ -61,6 +61,8 @@
   }
 
  private:
+  friend class NGLineWidthsTest;
+
   unsigned PositionLeadingFloats(NGExclusionSpace*, NGPositionedFloatVector*);
   NGPositionedFloat PositionFloat(LayoutUnit origin_block_bfc_offset,
                                   LayoutObject* floating_object,
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_line_widths.cc b/third_party/blink/renderer/core/layout/ng/inline/ng_line_widths.cc
new file mode 100644
index 0000000..78b3cae0
--- /dev/null
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_line_widths.cc
@@ -0,0 +1,88 @@
+// 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 "third_party/blink/renderer/core/layout/ng/inline/ng_line_widths.h"
+
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h"
+#include "third_party/blink/renderer/platform/fonts/shaping/shape_result.h"
+
+namespace blink {
+
+bool NGLineWidths::Set(const NGInlineNode& node,
+                       base::span<const NGLayoutOpportunity> opportunities) {
+  // Set the default width if no exclusions.
+  DCHECK_GE(opportunities.size(), 1u);
+  const NGLayoutOpportunity& first_opportunity = opportunities.front();
+  if (opportunities.size() == 1) {
+    DCHECK(!first_opportunity.HasShapeExclusions());
+    default_width_ = first_opportunity.rect.InlineSize();
+    DCHECK(!num_excluded_lines_);
+    return true;
+  }
+
+  // This class supports only single simple exclusion.
+  if (opportunities.size() > 2 || first_opportunity.HasShapeExclusions()) {
+    return false;
+  }
+
+  // Check if all lines have the same line heights.
+  const ComputedStyle& block_style = node.Style();
+  const Font& block_font = block_style.GetFont();
+  const SimpleFontData* primary_font = block_font.PrimaryFont();
+  DCHECK(primary_font);
+  const NGInlineItemsData& items_data = node.ItemsData(/*is_first_line*/ false);
+  // `::first-line` is not supported.
+  DCHECK_EQ(&items_data, &node.ItemsData(true));
+  const HeapVector<NGInlineItem>& items = items_data.items;
+  for (const NGInlineItem& item : items) {
+    switch (item.Type()) {
+      case NGInlineItem::kText: {
+        const ShapeResult* shape_result = item.TextShapeResult();
+        DCHECK(shape_result);
+        if (shape_result->PrimaryFont() != primary_font ||
+            shape_result->HasFallbackFonts()) {
+          return false;
+        }
+        break;
+      }
+      case NGInlineItem::kOpenTag: {
+        DCHECK(item.Style());
+        const ComputedStyle& style = *item.Style();
+        if (style.GetFont().PrimaryFont() != primary_font ||
+            style.VerticalAlign() != EVerticalAlign::kBaseline) {
+          return false;
+        }
+        break;
+      }
+      default:
+        break;
+    }
+  }
+
+  // All lines have the same line height. Compute the line height.
+  const FontBaseline baseline_type = block_style.GetFontBaseline();
+  NGInlineBoxState box;
+  box.ComputeTextMetrics(block_style, block_font, baseline_type);
+  const LayoutUnit line_height = box.metrics.LineHeight();
+  if (line_height <= LayoutUnit()) {
+    return false;
+  }
+
+  // Compute the number of lines that have the exclusion.
+  const NGLayoutOpportunity& last_opportunity = opportunities.back();
+  DCHECK(!last_opportunity.HasShapeExclusions());
+  default_width_ = last_opportunity.rect.InlineSize();
+  const LayoutUnit exclusion_block_size =
+      last_opportunity.rect.BlockStartOffset() -
+      first_opportunity.rect.BlockStartOffset();
+  DCHECK_GT(exclusion_block_size, LayoutUnit());
+  const int num_excluded_lines = (exclusion_block_size / line_height).Ceil();
+  DCHECK_GT(num_excluded_lines, 0);
+  num_excluded_lines_ = num_excluded_lines;
+  excluded_width_ = first_opportunity.rect.InlineSize();
+  return true;
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_line_widths.h b/third_party/blink/renderer/core/layout/ng/inline/ng_line_widths.h
new file mode 100644
index 0000000..0363d8c
--- /dev/null
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_line_widths.h
@@ -0,0 +1,48 @@
+// 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.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_LINE_WIDTHS_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_LINE_WIDTHS_H_
+
+#include "base/containers/span.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/layout/ng/exclusions/ng_layout_opportunity.h"
+#include "third_party/blink/renderer/platform/geometry/layout_unit.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class NGInlineNode;
+
+//
+// This class computes the line width of each line for _simple_ nodes without
+// actually laying them out.
+//
+class CORE_EXPORT NGLineWidths {
+  STACK_ALLOCATED();
+
+ public:
+  // Returns the width of a line. The `index` is 0-based line index.
+  LayoutUnit operator[](wtf_size_t index) const;
+
+  // Compute the line widths. Returns `false` if the `node` is not _simple_.
+  bool Set(const NGInlineNode& node,
+           base::span<const NGLayoutOpportunity> opportunities);
+
+ private:
+  LayoutUnit default_width_;
+  LayoutUnit excluded_width_;
+  wtf_size_t num_excluded_lines_ = 0;
+};
+
+inline LayoutUnit NGLineWidths::operator[](wtf_size_t index) const {
+  if (UNLIKELY(index < num_excluded_lines_)) {
+    return excluded_width_;
+  }
+  return default_width_;
+}
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_LINE_WIDTHS_H_
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_line_widths_test.cc b/third_party/blink/renderer/core/layout/ng/inline/ng_line_widths_test.cc
new file mode 100644
index 0000000..77ef6e5
--- /dev/null
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_line_widths_test.cc
@@ -0,0 +1,181 @@
+// 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 "third_party/blink/renderer/core/layout/ng/inline/ng_line_widths.h"
+
+#include "testing/gmock/include/gmock/gmock.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h"
+#include "third_party/blink/renderer/core/layout/ng/ng_box_fragment_builder.h"
+#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h"
+#include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
+
+namespace blink {
+
+namespace {
+
+LayoutUnit FragmentWidth(const NGInlineNode& node) {
+  const NGPhysicalBoxFragment* fragment =
+      node.GetLayoutBox()->GetPhysicalFragment(0);
+  return fragment->Size().width;
+}
+
+}  // namespace
+
+class NGLineWidthsTest : public RenderingTest {
+ public:
+  absl::optional<NGLineWidths> ComputeLineWidths(NGInlineNode node) {
+    const LayoutUnit width = FragmentWidth(node);
+    NGConstraintSpace space = ConstraintSpaceForAvailableSize(width);
+    const ComputedStyle& style = node.Style();
+    NGBoxFragmentBuilder container_builder(node, &style, space,
+                                           style.GetWritingDirection());
+    NGSimpleInlineChildLayoutContext context(node, &container_builder);
+    NGInlineLayoutAlgorithm algorithm(node, space, /*break_token*/ nullptr,
+                                      /*column_spanner_path*/ nullptr,
+                                      &context);
+    NGExclusionSpace exclusion_space(space.ExclusionSpace());
+    NGPositionedFloatVector leading_floats;
+    algorithm.PositionLeadingFloats(&exclusion_space, &leading_floats);
+    const LayoutOpportunityVector& opportunities =
+        exclusion_space.AllLayoutOpportunities(
+            {space.BfcOffset().line_offset, /*bfc_block_offset*/ LayoutUnit()},
+            space.AvailableSize().inline_size);
+    NGLineWidths line_width;
+    if (line_width.Set(node, opportunities)) {
+      return line_width;
+    }
+    return absl::nullopt;
+  }
+
+ protected:
+};
+
+struct LineWidthsData {
+  std::vector<int> widths;
+  const char* html;
+} line_widths_data[] = {
+    // It should be computable if no floats.
+    {{100, 100}, R"HTML(
+      <div id="target">0123 5678</div>
+    )HTML"},
+    {{100, 100}, R"HTML(
+      <div id="target">0123 <b>5</b>678</div>
+    )HTML"},
+    // Single left/right float should be computable.
+    {{70, 100}, R"HTML(
+      <div id="target">
+        <div class="left"></div>
+        0123 5678
+      </div>
+    )HTML"},
+    {{70, 100}, R"HTML(
+      <div id="target">
+        <div class="right"></div>
+        0123 5678
+      </div>
+    )HTML"},
+    {{70, 100}, R"HTML(
+      <div id="target" style="line-height: 15px">
+        <div class="left" style="height: 11px"></div>
+        0123 5678
+      </div>
+    )HTML"},
+    // The 2nd line is also narrow if the float is taller than one line.
+    {{70, 70, 100}, R"HTML(
+      <div id="target">
+        <div class="left" style="height: 11px"></div>
+        0123 5678
+      </div>
+    )HTML"},
+    {{70, 70, 100}, R"HTML(
+      <div id="target" style="line-height: 15px">
+        <div class="left" style="height: 16px"></div>
+        0123 5678
+      </div>
+    )HTML"},
+    // Multiple floats are computable if they produce single exclusion.
+    {{40, 100}, R"HTML(
+      <div id="target">
+        <div class="left"></div>
+        <div class="left"></div>
+        0123 5678
+      </div>
+    )HTML"},
+    // ...but not computable if they produce multiple exclusions.
+    {{}, R"HTML(
+      <div id="target">
+        <div class="left" style="height: 20px"></div>
+        <div class="left"></div>
+        0123 5678
+      </div>
+    )HTML"},
+    // Different fonts/`vertical-align` are not computable.
+    {{}, R"HTML(
+      <div id="target">
+        <div class="left"></div>
+        0123 5678 <b>0123</b> 5678
+      </div>
+    )HTML"},
+    {{}, R"HTML(
+      <div id="target">
+        <div class="left"></div>
+        0123 5678 <span style="vertical-align: top">0</span>123 5678
+      </div>
+    )HTML"},
+    // Fallback fonts are also not computable.
+    {{}, R"HTML(
+      <div id="target">
+        <div class="left"></div>
+        0123 5678 \u3040
+      </div>
+    )HTML"},
+};
+class NGLineWidthsDataTest
+    : public NGLineWidthsTest,
+      public testing::WithParamInterface<LineWidthsData> {};
+INSTANTIATE_TEST_SUITE_P(NGLineWidthsTest,
+                         NGLineWidthsDataTest,
+                         testing::ValuesIn(line_widths_data));
+
+TEST_P(NGLineWidthsDataTest, Data) {
+  const auto& data = GetParam();
+  LoadAhem();
+  SetBodyInnerHTML(String::Format(R"HTML(
+    <!DOCTYPE html>
+    <style>
+    #target {
+      font-family: Ahem;
+      font-size: 10px;
+      width: 100px;
+    }
+    .left {
+      float: left;
+      width: 30px;
+      height: 10px;
+    }
+    .right {
+      float: right;
+      width: 30px;
+      height: 10px;
+    }
+    </style>
+    %s
+  )HTML",
+                                  data.html));
+  const NGInlineNode target = GetInlineNodeByElementId("target");
+  const absl::optional<NGLineWidths> line_widths = ComputeLineWidths(target);
+  if (!line_widths) {
+    EXPECT_EQ(data.widths.size(), 0u);
+    return;
+  }
+  std::vector<int> actual_widths;
+  for (wtf_size_t i = 0; i < data.widths.size(); ++i) {
+    actual_widths.push_back((*line_widths)[i].ToInt());
+  }
+  EXPECT_THAT(actual_widths, data.widths);
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_paragraph_line_breaker_test.cc b/third_party/blink/renderer/core/layout/ng/inline/ng_paragraph_line_breaker_test.cc
index e3e2c90b..7d98347 100644
--- a/third_party/blink/renderer/core/layout/ng/inline/ng_paragraph_line_breaker_test.cc
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_paragraph_line_breaker_test.cc
@@ -116,6 +116,29 @@
   EXPECT_FALSE(AttemptParagraphBalancing(target));
 }
 
+TEST_F(NGParagraphLineBreakerTest, IsDisabledByForcedBreakReusing) {
+  SetBodyInnerHTML(R"HTML(
+    <!DOCTYPE html>
+    <style>
+    #target {
+      font-size: 10px;
+      width: 10ch;
+      white-space: pre;
+    }
+    </style>
+    <div id="target">1234 6789
+1234
+    </div>
+  )HTML");
+  const NGInlineNode target = GetInlineNodeByElementId("target");
+  Element* target_node = To<Element>(target.GetDOMNode());
+  target_node->AppendChild(GetDocument().createTextNode(" 6789"));
+  UpdateAllLifecyclePhasesForTest();
+  EXPECT_TRUE(target.IsBisectLineBreakDisabled());
+  EXPECT_FALSE(target.IsScoreLineBreakDisabled());
+  EXPECT_FALSE(AttemptParagraphBalancing(target));
+}
+
 TEST_F(NGParagraphLineBreakerTest, IsDisabledByInitialLetter) {
   SetBodyInnerHTML(R"HTML(
     <!DOCTYPE html>
diff --git a/third_party/blink/renderer/modules/BUILD.gn b/third_party/blink/renderer/modules/BUILD.gn
index 40d530b..50f07a2 100644
--- a/third_party/blink/renderer/modules/BUILD.gn
+++ b/third_party/blink/renderer/modules/BUILD.gn
@@ -606,6 +606,7 @@
     "service_worker/service_worker_container_test.cc",
     "service_worker/service_worker_event_queue_test.cc",
     "service_worker/service_worker_installed_scripts_manager_test.cc",
+    "service_worker/service_worker_router_type_converter_test.cc",
     "service_worker/thread_safe_script_container_test.cc",
     "service_worker/web_embedded_worker_impl_test.cc",
     "service_worker/web_service_worker_fetch_context_impl_test.cc",
diff --git a/third_party/blink/renderer/modules/accessibility/DEPS b/third_party/blink/renderer/modules/accessibility/DEPS
index 0deb9d661..bb2920e0 100644
--- a/third_party/blink/renderer/modules/accessibility/DEPS
+++ b/third_party/blink/renderer/modules/accessibility/DEPS
@@ -6,8 +6,6 @@
     "+third_party/blink/renderer/modules/media_controls",
     "+third_party/blink/renderer/modules/modules_export.h",
     "+third_party/blink/renderer/modules/permissions",
-    "+ui/accessibility/accessibility_features.h",
-    "+ui/accessibility/accessibility_switches.h",
     "+ui/accessibility/ax_action_data.h",
     "+ui/accessibility/ax_common.h",
     "+ui/accessibility/ax_enum_util.h",
diff --git a/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc b/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc
index 6c763880..ca581cd 100644
--- a/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc
+++ b/third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.cc
@@ -1638,6 +1638,13 @@
     if (node->IsPseudoElement()) {
       DeferTreeUpdate(&AXObjectCacheImpl::EnsureMarkDirtyWithCleanLayout, node);
     }
+    if (layout_object->Style() &&
+        !layout_object->Style()->IsContentVisibilityVisible()) {
+      // If a content-visibility: auto/hidden node is removed, remove the entire
+      // subtree because any AXObject descendants are now invalid, and there
+      // will not be any other signals to hook for invalidation or removal.
+      RemoveSubtreeWhenSafe(node);
+    }
     if (IsA<HTMLImageElement>(node)) {
       // If an image is removed, ensure its entire subtree is deleted as there
       // may have been children supplied via a map.
@@ -2183,6 +2190,17 @@
     return;
   }
 
+  // If the node is the root of a display locked subtree, and it already had an
+  // AXObject, its entire subtree needs to be invalidated. Now that it has
+  // layout, its subtree will be rebuilt using AXLayoutObject rather than
+  // AXNodeObject.
+  if (node->GetLayoutObject() && node->GetLayoutObject()->Style() &&
+      !node->GetLayoutObject()->Style()->IsContentVisibilityVisible() &&
+      node_object_mapping_.Contains(node)) {
+    RemoveSubtreeWithFlatTraversal(node, /* remove_root */ false,
+                                   /* notify_parent */ false);
+  }
+
   DeferTreeUpdate(
       &AXObjectCacheImpl::UpdateCacheAfterNodeIsAttachedWithCleanLayout, node);
 }
diff --git a/third_party/blink/renderer/modules/accessibility/blink_ax_tree_source.cc b/third_party/blink/renderer/modules/accessibility/blink_ax_tree_source.cc
index fb7950f..c58f2fe5 100644
--- a/third_party/blink/renderer/modules/accessibility/blink_ax_tree_source.cc
+++ b/third_party/blink/renderer/modules/accessibility/blink_ax_tree_source.cc
@@ -19,8 +19,6 @@
 #include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
 #include "third_party/blink/renderer/modules/accessibility/ax_selection.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_deque.h"
-#include "ui/accessibility/accessibility_features.h"
-#include "ui/accessibility/accessibility_switches.h"
 #include "ui/accessibility/ax_common.h"
 #include "ui/accessibility/ax_enum_util.h"
 #include "ui/accessibility/ax_role_properties.h"
diff --git a/third_party/blink/renderer/modules/remote_objects/remote_object.cc b/third_party/blink/renderer/modules/remote_objects/remote_object.cc
index 0993ec9..54160a8 100644
--- a/third_party/blink/renderer/modules/remote_objects/remote_object.cc
+++ b/third_party/blink/renderer/modules/remote_objects/remote_object.cc
@@ -6,7 +6,6 @@
 
 #include <tuple>
 
-#include "base/metrics/histogram_macros.h"
 #include "base/numerics/safe_conversions.h"
 #include "gin/converter.h"
 #include "third_party/blink/public/web/blink.h"
@@ -19,21 +18,6 @@
 
 namespace {
 
-// Used to specify what kind of error was encountered during Java bridge method
-// invocation.
-// Note: these values are logged to UMA. Entries should not be renumbered and
-// numeric values should never be reused. Please keep in sync with
-// "JavaJsBridgeMethodInvocationError" in
-// src/tools/metrics/histograms/enums.xml.
-enum class JavaJsBridgeMethodInvocationError {
-  kAsConstructorDisallowed,
-  kNonexistentMethod,
-  kOnNonInjectedObjectDisallowed,
-  kErrorMessage,
-  // Magic constant used by the histogram macros.
-  kMaxValue = kErrorMessage,
-};
-
 const char kMethodInvocationAsConstructorDisallowed[] =
     "Java bridge method can't be invoked as a constructor";
 const char kMethodInvocationNonexistentMethod[] =
@@ -336,9 +320,6 @@
     // This is not a constructor. Throw and return.
     isolate->ThrowException(v8::Exception::Error(
         V8String(isolate, kMethodInvocationAsConstructorDisallowed)));
-    UMA_HISTOGRAM_ENUMERATION(
-        "Blink.JavaJsBridge.MethodInvocationError",
-        JavaJsBridgeMethodInvocationError::kAsConstructorDisallowed);
     return;
   }
 
@@ -347,9 +328,6 @@
     // Someone messed with the |this| pointer. Throw and return.
     isolate->ThrowException(v8::Exception::Error(
         V8String(isolate, kMethodInvocationOnNonInjectedObjectDisallowed)));
-    UMA_HISTOGRAM_ENUMERATION(
-        "Blink.JavaJsBridge.MethodInvocationError",
-        JavaJsBridgeMethodInvocationError::kOnNonInjectedObjectDisallowed);
     return;
   }
 
@@ -368,9 +346,6 @@
   if (cached_method->IsUndefined()) {
     isolate->ThrowException(v8::Exception::Error(
         V8String(isolate, kMethodInvocationNonexistentMethod)));
-    UMA_HISTOGRAM_ENUMERATION(
-        "Blink.JavaJsBridge.MethodInvocationError",
-        JavaJsBridgeMethodInvocationError::kNonexistentMethod);
     return;
   }
 
@@ -394,8 +369,6 @@
     String message = String::Format("%s : ", kMethodInvocationErrorMessage) +
                      RemoteInvocationErrorToString(result->error);
     isolate->ThrowException(v8::Exception::Error(V8String(isolate, message)));
-    UMA_HISTOGRAM_ENUMERATION("Blink.JavaJsBridge.MethodInvocationError",
-                              JavaJsBridgeMethodInvocationError::kErrorMessage);
     return;
   }
 
diff --git a/third_party/blink/renderer/modules/service_worker/BUILD.gn b/third_party/blink/renderer/modules/service_worker/BUILD.gn
index f953086..0052a031 100644
--- a/third_party/blink/renderer/modules/service_worker/BUILD.gn
+++ b/third_party/blink/renderer/modules/service_worker/BUILD.gn
@@ -52,6 +52,8 @@
     "service_worker_module_tree_client.h",
     "service_worker_registration.cc",
     "service_worker_registration.h",
+    "service_worker_router_type_converter.cc",
+    "service_worker_router_type_converter.h",
     "service_worker_script_cached_metadata_handler.cc",
     "service_worker_script_cached_metadata_handler.h",
     "service_worker_thread.cc",
diff --git a/third_party/blink/renderer/modules/service_worker/DEPS b/third_party/blink/renderer/modules/service_worker/DEPS
index e22e4f6..67318e1 100644
--- a/third_party/blink/renderer/modules/service_worker/DEPS
+++ b/third_party/blink/renderer/modules/service_worker/DEPS
@@ -10,6 +10,7 @@
     "+third_party/blink/renderer/modules/event_target_modules.h",
     "+third_party/blink/renderer/modules/modules_export.h",
     "+third_party/blink/renderer/modules/service_worker",
+    "+third_party/liburlpattern",
 ]
 
 specific_include_rules = {
diff --git a/third_party/blink/renderer/modules/service_worker/OWNERS b/third_party/blink/renderer/modules/service_worker/OWNERS
index 970e7ea..01f3934 100644
--- a/third_party/blink/renderer/modules/service_worker/OWNERS
+++ b/third_party/blink/renderer/modules/service_worker/OWNERS
@@ -1,2 +1,4 @@
 file://content/browser/service_worker/OWNERS
+per-file *_type_converter*.*=set noparent
+per-file *_type_converter*.*=file://ipc/SECURITY_OWNERS
 
diff --git a/third_party/blink/renderer/modules/service_worker/install_event.cc b/third_party/blink/renderer/modules/service_worker/install_event.cc
index c95d2b6..33fb158 100644
--- a/third_party/blink/renderer/modules/service_worker/install_event.cc
+++ b/third_party/blink/renderer/modules/service_worker/install_event.cc
@@ -4,15 +4,40 @@
 
 #include "third_party/blink/renderer/modules/service_worker/install_event.h"
 
+#include "third_party/blink/public/mojom/service_worker/service_worker_router_rule.mojom-blink.h"
 #include "third_party/blink/public/platform/web_security_origin.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_union_routerrule_routerrulesequence.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
+#include "third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h"
+#include "third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter.h"
+#include "third_party/blink/renderer/platform/bindings/exception_code.h"
 #include "third_party/blink/renderer/platform/bindings/script_state.h"
 #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h"
 
 namespace blink {
 
+namespace {
+
+void DidRegisterRouter(ScriptPromiseResolver* resolver) {
+  if (!resolver->GetExecutionContext() ||
+      resolver->GetExecutionContext()->IsContextDestroyed()) {
+    return;
+  }
+  resolver->Resolve();
+}
+
+ScriptPromise ParseErrorPromise(ScriptState* script_state) {
+  return ScriptPromise::Reject(
+      script_state, V8ThrowException::CreateTypeError(
+                        script_state->GetIsolate(),
+                        "Failed to parse a rule. Possibly syntax error."));
+}
+
+}  // namespace
+
 InstallEvent* InstallEvent::Create(const AtomicString& type,
                                    const ExtendableEventInit* event_init) {
   return MakeGarbageCollected<InstallEvent>(type, event_init);
@@ -45,9 +70,47 @@
 ScriptPromise InstallEvent::registerRouter(
     ScriptState* script_state,
     const V8UnionRouterRuleOrRouterRuleSequence* v8_rules) {
-  return ScriptPromise::Reject(
-      script_state, V8ThrowException::CreateTypeError(
-                        script_state->GetIsolate(), "Not implemented yet"));
+  ServiceWorkerGlobalScope* global_scope =
+      To<ServiceWorkerGlobalScope>(ExecutionContext::From(script_state));
+  if (!global_scope) {
+    return ScriptPromise::Reject(
+        script_state,
+        V8ThrowDOMException::CreateOrDie(script_state->GetIsolate(),
+                                         DOMExceptionCode::kInvalidStateError,
+                                         "No ServiceWorkerGlobalScope."));
+  }
+  if (did_register_router_) {
+    return ScriptPromise::Reject(
+        script_state, V8ThrowException::CreateTypeError(
+                          script_state->GetIsolate(),
+                          "registerRouter is called multiple times."));
+  }
+  did_register_router_ = true;
+
+  blink::ServiceWorkerRouterRules rules;
+  if (v8_rules->IsRouterRule()) {
+    auto r = mojo::ConvertTo<absl::optional<blink::ServiceWorkerRouterRule>>(
+        v8_rules->GetAsRouterRule());
+    if (!r) {
+      return ParseErrorPromise(script_state);
+    }
+    rules.rules.emplace_back(*r);
+  } else {
+    CHECK(v8_rules->IsRouterRuleSequence());
+    for (const blink::RouterRule* rule : v8_rules->GetAsRouterRuleSequence()) {
+      auto r =
+          mojo::ConvertTo<absl::optional<blink::ServiceWorkerRouterRule>>(rule);
+      if (!r) {
+        return ParseErrorPromise(script_state);
+      }
+      rules.rules.emplace_back(*r);
+    }
+  }
+
+  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
+  global_scope->GetServiceWorkerHost()->RegisterRouter(
+      rules, WTF::BindOnce(&DidRegisterRouter, WrapPersistent(resolver)));
+  return resolver->Promise();
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/service_worker/install_event.h b/third_party/blink/renderer/modules/service_worker/install_event.h
index ec49479..ee41456 100644
--- a/third_party/blink/renderer/modules/service_worker/install_event.h
+++ b/third_party/blink/renderer/modules/service_worker/install_event.h
@@ -40,6 +40,7 @@
 
  protected:
   const int event_id_;
+  bool did_register_router_ = false;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/service_worker/install_event.idl b/third_party/blink/renderer/modules/service_worker/install_event.idl
index 5601028e..33146c1 100644
--- a/third_party/blink/renderer/modules/service_worker/install_event.idl
+++ b/third_party/blink/renderer/modules/service_worker/install_event.idl
@@ -10,5 +10,5 @@
     constructor(DOMString type, optional ExtendableEventInit eventInitDict = {});
     // Experimental
     // https://github.com/yoshisatoyanagisawa/service-worker-static-routing-api
-    [RuntimeEnabled=ServiceWorkerRouter, CallWith=ScriptState] Promise<undefined> registerRouter((RouterRule or sequence<RouterRule>) rules);
+    [RuntimeEnabled=ServiceWorkerStaticRouter, CallWith=ScriptState] Promise<undefined> registerRouter((RouterRule or sequence<RouterRule>) rules);
 };
diff --git a/third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter.cc b/third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter.cc
new file mode 100644
index 0000000..1342bb7f
--- /dev/null
+++ b/third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter.cc
@@ -0,0 +1,102 @@
+// 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 "third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter.h"
+
+#include "third_party/blink/renderer/bindings/modules/v8/v8_router_condition.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_router_rule.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_router_source_enum.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_router_url_pattern_condition.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h"
+#include "third_party/liburlpattern/parse.h"
+#include "third_party/liburlpattern/pattern.h"
+
+namespace {
+
+absl::optional<blink::ServiceWorkerRouterCondition>
+RouterUrlPatternConditionToBlink(
+    blink::RouterUrlPatternCondition* v8_condition) {
+  if (!v8_condition) {
+    // No UrlCondition configured.
+    return absl::nullopt;
+  }
+  if (v8_condition->urlPattern().empty()) {
+    // No URLPattern configured.
+    return absl::nullopt;
+  }
+  // TODO(crbug.com/1371756): unify the code with manifest_parser.cc
+  WTF::StringUTF8Adaptor utf8(v8_condition->urlPattern());
+  auto parse_result = liburlpattern::Parse(
+      base::StringPiece(utf8.data(), utf8.size()),
+      [](base::StringPiece input) { return std::string(input); });
+  if (!parse_result.ok()) {
+    return absl::nullopt;
+  }
+  std::vector<liburlpattern::Part> part_list;
+  for (auto& part : parse_result.value().PartList()) {
+    // We don't allow custom regex for security reasons as this will be used
+    // in the browser process.
+    if (part.type == liburlpattern::PartType::kRegex) {
+      DLOG(INFO) << "regex URLPattern is not allowed as Router Condition";
+      return absl::nullopt;
+    }
+    part_list.push_back(std::move(part));
+  }
+  blink::ServiceWorkerRouterCondition condition;
+  condition.type =
+      blink::ServiceWorkerRouterCondition::ConditionType::kUrlPattern;
+  blink::UrlPattern url_pattern;
+  url_pattern.pathname = std::move(part_list);
+  condition.url_pattern = std::move(url_pattern);
+  return condition;
+}
+
+absl::optional<blink::ServiceWorkerRouterSource> RouterSourceEnumToBlink(
+    blink::V8RouterSourceEnum v8_source_enum) {
+  if (v8_source_enum != blink::V8RouterSourceEnum::Enum::kNetwork) {
+    return absl::nullopt;
+  }
+  blink::ServiceWorkerRouterSource source;
+  source.type = blink::ServiceWorkerRouterSource::SourceType::kNetwork;
+  source.network_source.emplace();
+  return source;
+}
+
+}  // namespace
+
+namespace mojo {
+
+absl::optional<blink::ServiceWorkerRouterRule>
+TypeConverter<absl::optional<blink::ServiceWorkerRouterRule>,
+              blink::RouterRule*>::Convert(const blink::RouterRule* input) {
+  if (!input) {
+    return absl::nullopt;
+  }
+
+  blink::ServiceWorkerRouterRule rule;
+  absl::optional<blink::ServiceWorkerRouterCondition> condition =
+      RouterUrlPatternConditionToBlink(input->condition());
+  if (!condition) {
+    return absl::nullopt;
+  }
+  absl::optional<blink::ServiceWorkerRouterSource> source =
+      RouterSourceEnumToBlink(input->source());
+  if (!source) {
+    return absl::nullopt;
+  }
+
+  // TODO(crbug.com/1371756): support multiple conditions and sources.
+  // i.e. support full form shown in
+  // https://github.com/yoshisatoyanagisawa/service-worker-static-routing-api/blob/main/final-form.md
+  //
+  // https://github.com/yoshisatoyanagisawa/service-worker-static-routing-api/blob/main/README.md
+  // explains the first step. It does not cover cases sequence of conditions or
+  // sources are set. The current IDL has been implemented for this level, but
+  // the mojo IPC has been implemented to support the final form.
+  rule.conditions.emplace_back(*condition);
+  rule.sources.emplace_back(*source);
+  return rule;
+}
+
+}  // namespace mojo
diff --git a/third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter.h b/third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter.h
new file mode 100644
index 0000000..148aa80
--- /dev/null
+++ b/third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter.h
@@ -0,0 +1,31 @@
+// 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.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_SERVICE_WORKER_SERVICE_WORKER_ROUTER_TYPE_CONVERTER_H_
+#define THIRD_PARTY_BLINK_RENDERER_MODULES_SERVICE_WORKER_SERVICE_WORKER_ROUTER_TYPE_CONVERTER_H_
+
+#include "mojo/public/cpp/bindings/type_converter.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/blink/public/mojom/service_worker/service_worker_router_rule.mojom-blink.h"
+#include "third_party/blink/renderer/modules/modules_export.h"
+
+namespace blink {
+
+class RouterRule;
+
+}  // namespace blink
+
+namespace mojo {
+
+template <>
+struct MODULES_EXPORT
+    TypeConverter<absl::optional<blink::ServiceWorkerRouterRule>,
+                  blink::RouterRule*> {
+  static absl::optional<blink::ServiceWorkerRouterRule> Convert(
+      const blink::RouterRule* input);
+};
+
+}  // namespace mojo
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_MODULES_SERVICE_WORKER_SERVICE_WORKER_ROUTER_TYPE_CONVERTER_H_
diff --git a/third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter_test.cc b/third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter_test.cc
new file mode 100644
index 0000000..37437af4
--- /dev/null
+++ b/third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter_test.cc
@@ -0,0 +1,77 @@
+// 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 "third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_router_rule.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_router_source_enum.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_router_url_pattern_condition.h"
+#include "third_party/liburlpattern/parse.h"
+#include "third_party/liburlpattern/pattern.h"
+
+namespace blink {
+
+namespace {
+
+TEST(ServiceWorkerRouterTypeConverterTest, Basic) {
+  constexpr const char kFakeUrlPattern[] = "/fake";
+  auto* idl_rule = blink::RouterRule::Create();
+  auto* idl_url_pattern = blink::RouterUrlPatternCondition::Create();
+  idl_url_pattern->setUrlPattern(kFakeUrlPattern);
+  idl_rule->setCondition(idl_url_pattern);
+  idl_rule->setSource(blink::V8RouterSourceEnum::Enum::kNetwork);
+
+  blink::ServiceWorkerRouterRule expected_rule;
+  blink::ServiceWorkerRouterCondition expected_condition;
+  expected_condition.type =
+      blink::ServiceWorkerRouterCondition::ConditionType::kUrlPattern;
+  blink::UrlPattern expected_url_pattern;
+  auto parse_result = liburlpattern::Parse(
+      kFakeUrlPattern,
+      [](base::StringPiece input) { return std::string(input); });
+  ASSERT_TRUE(parse_result.ok());
+  expected_url_pattern.pathname = parse_result.value().PartList();
+  expected_condition.url_pattern = std::move(expected_url_pattern);
+  expected_rule.conditions.emplace_back(expected_condition);
+  blink::ServiceWorkerRouterSource expected_source;
+  expected_source.type = blink::ServiceWorkerRouterSource::SourceType::kNetwork;
+  expected_source.network_source.emplace();
+  expected_rule.sources.emplace_back(expected_source);
+
+  auto blink_rule =
+      mojo::ConvertTo<absl::optional<blink::ServiceWorkerRouterRule>>(idl_rule);
+  EXPECT_TRUE(blink_rule.has_value());
+  EXPECT_EQ(expected_rule, *blink_rule);
+}
+
+TEST(ServiceWorkerRouterTypeConverterTest, EmptyUrlPatternShouldBeNullopt) {
+  constexpr const char kFakeUrlPattern[] = "";
+  auto* idl_rule = blink::RouterRule::Create();
+  auto* idl_url_pattern = blink::RouterUrlPatternCondition::Create();
+  idl_url_pattern->setUrlPattern(kFakeUrlPattern);
+  idl_rule->setCondition(idl_url_pattern);
+  idl_rule->setSource(blink::V8RouterSourceEnum::Enum::kNetwork);
+
+  auto blink_rule =
+      mojo::ConvertTo<absl::optional<blink::ServiceWorkerRouterRule>>(idl_rule);
+  EXPECT_FALSE(blink_rule.has_value());
+}
+
+TEST(ServiceWorkerRouterTypeConverterTest, RegexpUrlPatternShouldBeNullopt) {
+  constexpr const char kFakeUrlPattern[] = "/fake/(\\\\d+)";
+  auto* idl_rule = blink::RouterRule::Create();
+  auto* idl_url_pattern = blink::RouterUrlPatternCondition::Create();
+  idl_url_pattern->setUrlPattern(kFakeUrlPattern);
+  idl_rule->setCondition(idl_url_pattern);
+  idl_rule->setSource(blink::V8RouterSourceEnum::Enum::kNetwork);
+
+  auto blink_rule =
+      mojo::ConvertTo<absl::optional<blink::ServiceWorkerRouterRule>>(idl_rule);
+  EXPECT_FALSE(blink_rule.has_value());
+}
+
+}  // namespace
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_buffer.idl b/third_party/blink/renderer/modules/webgpu/gpu_buffer.idl
index 4df0faf..0cd9ca65 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_buffer.idl
+++ b/third_party/blink/renderer/modules/webgpu/gpu_buffer.idl
@@ -4,6 +4,8 @@
 
 // https://gpuweb.github.io/gpuweb/
 
+typedef unsigned long long GPUSize64Out;
+
 [
     Exposed(Window WebGPU, DedicatedWorker WebGPU),
     SecureContext
@@ -18,7 +20,7 @@
     [CallWith=Isolate] void unmap();
     [CallWith=Isolate] void destroy();
 
-    readonly attribute GPUSize64 size;
+    readonly attribute GPUSize64Out size;
     readonly attribute GPUBufferUsageFlags usage;
     readonly attribute GPUBufferMapState mapState;
 };
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_sampler_descriptor.idl b/third_party/blink/renderer/modules/webgpu/gpu_sampler_descriptor.idl
index 08af733..b06159e 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_sampler_descriptor.idl
+++ b/third_party/blink/renderer/modules/webgpu/gpu_sampler_descriptor.idl
@@ -12,7 +12,7 @@
     GPUFilterMode minFilter = "nearest";
     GPUMipmapFilterMode mipmapFilter = "nearest";
     float lodMinClamp = 0;
-    float lodMaxClamp = 0xffffffff;
+    float lodMaxClamp = 32;
     GPUCompareFunction compare;
     [Clamp] unsigned short maxAnisotropy = 1;
 };
diff --git a/third_party/blink/renderer/platform/fonts/shaping/shape_result.cc b/third_party/blink/renderer/platform/fonts/shaping/shape_result.cc
index f5a4b50..fa90162c 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/shape_result.cc
+++ b/third_party/blink/renderer/platform/fonts/shaping/shape_result.cc
@@ -654,6 +654,16 @@
   return PositionForOffset(offset, adjust_mid_cluster);
 }
 
+bool ShapeResult::HasFallbackFonts() const {
+  const SimpleFontData* primary_font = PrimaryFont();
+  for (const scoped_refptr<RunInfo>& run : runs_) {
+    if (run->font_data_ != primary_font) {
+      return true;
+    }
+  }
+  return false;
+}
+
 void ShapeResult::GetRunFontData(Vector<RunFontData>* font_data) const {
   for (const auto& run : runs_) {
     font_data->push_back(
diff --git a/third_party/blink/renderer/platform/fonts/shaping/shape_result.h b/third_party/blink/renderer/platform/fonts/shaping/shape_result.h
index 1ae453f..41708913 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/shape_result.h
+++ b/third_party/blink/renderer/platform/fonts/shaping/shape_result.h
@@ -159,6 +159,7 @@
   unsigned NumCharacters() const { return num_characters_; }
   unsigned NumGlyphs() const { return num_glyphs_; }
   const SimpleFontData* PrimaryFont() const { return primary_font_.get(); }
+  bool HasFallbackFonts() const;
 
   // TODO(eae): Remove start_x and return value once ShapeResultBuffer has been
   // removed.
diff --git a/third_party/blink/renderer/platform/graphics/video_frame_resource_provider.cc b/third_party/blink/renderer/platform/graphics/video_frame_resource_provider.cc
index 9a645ae..1f844fb 100644
--- a/third_party/blink/renderer/platform/graphics/video_frame_resource_provider.cc
+++ b/third_party/blink/renderer/platform/graphics/video_frame_resource_provider.cc
@@ -48,8 +48,8 @@
   }
 
   resource_updater_ = std::make_unique<media::VideoResourceUpdater>(
-      nullptr, media_context_provider, shared_bitmap_reporter,
-      resource_provider_.get(), settings_.use_stream_video_draw_quad,
+      media_context_provider, shared_bitmap_reporter, resource_provider_.get(),
+      settings_.use_stream_video_draw_quad,
       settings_.resource_settings.use_gpu_memory_buffer_resources,
       settings_.resource_settings.use_r16_texture, max_texture_size);
 }
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 7675efc..06fe799 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -3225,8 +3225,9 @@
       public: true,
     },
     {
-      name: "ServiceWorkerRouter",
+      name: "ServiceWorkerStaticRouter",
       base_feature: "none",
+      public: true,
     },
     {
       name: "SharedArrayBuffer",
diff --git a/third_party/blink/renderer/platform/text/writing_mode_utils.h b/third_party/blink/renderer/platform/text/writing_mode_utils.h
index 89ff05fc..bf7c002 100644
--- a/third_party/blink/renderer/platform/text/writing_mode_utils.h
+++ b/third_party/blink/renderer/platform/text/writing_mode_utils.h
@@ -173,8 +173,9 @@
                                                   block_end)) {}
 
   Value InlineStart() const { return logical_.InlineStart(); }
-
   Value BlockStart() const { return logical_.BlockStart(); }
+  Value InlineEnd() const { return logical_.InlineEnd(); }
+  Value BlockEnd() const { return logical_.BlockEnd(); }
 
  private:
   LogicalToLogical(WritingDirectionMode to_writing_direction,
diff --git a/third_party/blink/renderer/platform/wtf/text/atomic_string.h b/third_party/blink/renderer/platform/wtf/text/atomic_string.h
index 9df540d..f932d708 100644
--- a/third_party/blink/renderer/platform/wtf/text/atomic_string.h
+++ b/third_party/blink/renderer/platform/wtf/text/atomic_string.h
@@ -160,9 +160,6 @@
       TextCaseSensitivity case_sensitivity = kTextCaseSensitive) const {
     return string_.StartsWith(prefix, case_sensitivity);
   }
-  bool StartsWithIgnoringCase(const StringView& prefix) const {
-    return string_.StartsWithIgnoringCase(prefix);
-  }
   bool StartsWithIgnoringASCIICase(const StringView& prefix) const {
     return string_.StartsWithIgnoringASCIICase(prefix);
   }
diff --git a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
index 1528d4f..7fa2fa0d 100755
--- a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
+++ b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
@@ -1875,6 +1875,19 @@
             'base::StringPiece16',
             'base::i18n::UTF16CharIterator',
         ]
+    },
+    {
+        'paths': [
+            'third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter.cc',
+            'third_party/blink/renderer/modules/service_worker/service_worker_router_type_converter_test.cc',
+            # TODO(crbug.com/1371756): consolidate code using liburlpattern.
+            # Especially, consolidate manifest and this code.
+        ],
+        'allowed': [
+            'liburlpattern::Parse',
+            'liburlpattern::Part',
+            'liburlpattern::PartType',
+        ]
     }
 ]
 
diff --git a/third_party/blink/tools/blinkpy/web_tests/port/base.py b/third_party/blink/tools/blinkpy/web_tests/port/base.py
index e0e7dab..1e4fbdb 100644
--- a/third_party/blink/tools/blinkpy/web_tests/port/base.py
+++ b/third_party/blink/tools/blinkpy/web_tests/port/base.py
@@ -1578,6 +1578,11 @@
     @memoized
     def args_for_test(self, test_name):
         args = self._lookup_virtual_test_args(test_name)
+
+        if self._is_in_allowlist_for_threaded_compositing(test_name):
+            if (ENABLE_THREADED_COMPOSITING_FLAG not in args):
+                args.append(ENABLE_THREADED_COMPOSITING_FLAG)
+
         pac_url = self.extract_wpt_pac(test_name)
         if pac_url is not None:
             args.append("--proxy-pac-url=" + pac_url)
@@ -1595,10 +1600,6 @@
                 self._filesystem.sanitize_filename(test_name), current_time)
             args.append('--trace-startup-file=' + file_name)
 
-        if self._is_in_allowlist_for_threaded_compositing(test_name):
-            if (ENABLE_THREADED_COMPOSITING_FLAG not in args):
-                args.append(ENABLE_THREADED_COMPOSITING_FLAG)
-
         return args
 
     @memoized
@@ -2558,7 +2559,7 @@
     def _get_blocked_tests_for_threaded_compositing_testing(self):
         path = self._filesystem.join(self.web_tests_dir(),
                                      'SmokeTests/SingleThreadedTests')
-        return self._filesystem.read_text_file(path).split('\n')
+        return set(self._filesystem.read_text_file(path).split('\n'))
 
     def _is_in_allowlist_for_threaded_compositing(self, test_name):
         # We currently only turn on threaded compositing tests for Linux
@@ -2570,15 +2571,15 @@
 
         block_list = self._get_blocked_tests_for_threaded_compositing_testing()
 
+        if test_name in block_list:
+            return False
+
         # We apply the setting of a base test to all of its virtual versions
         base_name = self.lookup_virtual_test_base(test_name)
         if base_name:
             if base_name in block_list:
                 return False
 
-        if test_name in block_list:
-            return False
-
         return True
 
     def build_path(self, *comps):
@@ -2724,7 +2725,13 @@
         self.platforms = [x.lower() for x in platforms]
         self.bases = bases
         self.exclusive_tests = exclusive_tests
-        self.args = args
+        self.args = sorted(args)
+        # always put --enable-threaded-compositing at the end of list, so that after appending
+        # this parameter due to crrev.com/c/4599846, we do not need to restart content shell
+        # if the parameter set is same.
+        if ENABLE_THREADED_COMPOSITING_FLAG in self.args:
+            self.args.remove(ENABLE_THREADED_COMPOSITING_FLAG)
+            self.args.append(ENABLE_THREADED_COMPOSITING_FLAG)
 
     def __repr__(self):
         return "VirtualTestSuite('%s', %s, %s, %s)" % (self.full_prefix,
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 97e1285c..92273e4 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -3525,9 +3525,7 @@
 crbug.com/764235 external/wpt/css/css-grid/alignment/grid-baseline-justify-001.html [ Failure ]
 
 # [css-subgrid]
-crbug.com/618969 external/wpt/css/css-grid/subgrid/baseline-001.html [ Failure ]
 crbug.com/618969 external/wpt/css/css-grid/subgrid/line-names-007.html [ Failure ]
-crbug.com/618969 external/wpt/css/css-grid/subgrid/subgrid-baseline-001.html [ Failure ]
 crbug.com/618969 external/wpt/css/css-grid/subgrid/subgrid-baseline-002.html [ Failure ]
 
 # [css-animations]
@@ -5528,8 +5526,6 @@
 crbug.com/1244896 fast/mediacapturefromelement/CanvasCaptureMediaStream-set-size-too-large.html [ Failure Pass Timeout ]
 
 # Sheriff 2022-04-07
-crbug.com/1314314 [ Debug Linux ] virtual/percent-based-scrolling/fast/scrolling/scrollbars/mouse-autoscrolling-on-deleted-scrollbar.html [ Crash Failure Pass Timeout ]
-crbug.com/1314314 [ Release Win ] virtual/percent-based-scrolling/fast/scrolling/scrollbars/mouse-autoscrolling-on-deleted-scrollbar.html [ Crash Failure Pass Timeout ]
 crbug.com/1314323 [ Win ] virtual/compositor-threaded-percent-based-scrolling/fast/scrolling/wheel-scrolling-over-custom-scrollbar.html [ Crash Failure Pass Timeout ]
 
 # Sheriff 2022-04-08
@@ -5746,11 +5742,9 @@
 crbug.com/1358383 [ Mac ] fast/scrolling/scrollbars/scrollbar-rtl-manipulation.html [ Failure ]
 crbug.com/1358383 virtual/compositor_threaded_scrollbar_scrolling/fast/scrolling/scrollbars/mouse-scrolling-on-root-scrollbar-thumb.html [ Failure ]
 crbug.com/1358383 virtual/threaded-prefer-compositing/fast/scrolling/scrollbars/mouse-scrolling-on-root-scrollbar-thumb.html [ Failure ]
-[ Linux ] virtual/percent-based-scrolling/fast/scrolling/scrollbars/touch-scrolling-on-root-scrollbar.html [ Pass ]
 virtual/main-threaded-percent-based-scrolling/fast/scrolling/scrollbars/touch-scrolling-on-root-scrollbar.html [ Pass ]
 virtual/compositor-threaded-percent-based-scrolling/fast/scrolling/scrollbars/touch-scrolling-on-root-scrollbar.html [ Pass ]
 virtual/fractional-scroll-offsets/fast/scrolling/scrollbars/touch-scrolling-on-root-scrollbar.html [ Pass ]
-[ Mac ] virtual/percent-based-scrolling/fast/scrolling/scrollbars/scrollbar-rtl-manipulation.html [ Pass ]
 [ Mac ] virtual/main-threaded-percent-based-scrolling/fast/scrolling/scrollbars/scrollbar-rtl-manipulation.html [ Pass ]
 [ Mac ] virtual/compositor-threaded-percent-based-scrolling/fast/scrolling/scrollbars/scrollbar-rtl-manipulation.html [ Pass ]
 [ Mac ] virtual/fractional-scroll-offsets/fast/scrolling/scrollbars/scrollbar-rtl-manipulation.html [ Pass ]
@@ -5913,7 +5907,6 @@
 
 crbug.com/1370050 http/tests/devtools/sources/debugger-ui/debugger-save-to-temp-var.js [ Crash Failure Pass Timeout ]
 
-crbug.com/1279220 [ Win10.20h2 ] virtual/percent-based-scrolling/fast/scrolling/scrollbars/touch-scrolling-on-root-scrollbar.html [ Pass Timeout ]
 crbug.com/1279220 [ Linux ] fast/scrolling/non-composited-scroller-in-position-fixed-scroller.html [ Failure Pass ]
 
 # Temporarily disable to land DevTools changes
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index 2bc59385..ec62348 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -466,7 +466,7 @@
   {
     "prefix": "percent-based-scrolling",
     "platforms": ["Linux", "Mac", "Win"],
-    "bases": ["fast/scrolling/scrollbars"],
+    "bases": [],
     "args": ["--enable-features=WindowsScrollingPersonality",
              "--enable-threaded-compositing",
              "--enable-prefer-compositing-to-lcd-text"],
@@ -485,8 +485,7 @@
     "prefix": "compositor-threaded-percent-based-scrolling",
     "platforms": ["Linux", "Mac", "Win"],
     "bases": ["fast/events/wheel",
-              "fast/scrolling",
-              "virtual/percent-based-scrolling"],
+              "fast/scrolling"],
     "args": ["--enable-features=WindowsScrollingPersonality",
              "--enable-threaded-compositing",
              "--enable-prefer-compositing-to-lcd-text"],
diff --git a/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-005.html b/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-005.html
new file mode 100644
index 0000000..455439f7
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-005.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-grid-2/">
+<style>
+.item {
+  inline-size: 40px;
+  box-sizing: border-box;
+  border: solid 5px hotpink;
+  line-height: 0;
+  margin-block-start: 3px;
+  margin-block-end: 5px;
+}
+.small {
+  width: 20px;
+  height: 20px;
+  border: solid 5px cyan;
+}
+.first {
+  align-self: baseline;
+}
+.last {
+  align-self: last baseline;
+}
+span {
+  width: 20px;
+  height: 20px;
+  box-sizing: border-box;
+  border: solid 5px orange;
+  display: inline-block;
+}
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+<body onload="checkLayout('.item')">
+
+<div style="position: relative; display: grid; grid-template: 150px 150px 150px / 100px 100px 100px 100px;">
+  <div style="display: grid;
+              gap: 10px;
+              grid-column: 1 / span 2;
+              grid-row: 1 / span 3;
+              grid-template: subgrid / subgrid;
+              margin-block-start: 5px; margin-block-end: 10px;
+              border: solid black 5px;
+              padding-block-start: 10px; padding-block-end: 20px;">
+    <div style="display: grid;
+                gap: 20px;
+                grid-column: 1 / span 2;
+                grid-row: 1 / span 2;
+                grid-template: subgrid / subgrid;
+                margin-block-start: 3px; margin-block-end: 7px;
+                border: solid black 5px;
+                padding-block-start: 5px; padding-block-end: 10px;">
+      <div data-offset-y="36" class="item first">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-y="85" class="item last">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-y="163" class="item first">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-y="218" class="item last">
+        <span></span><br><span></span>
+      </div>
+    </div>
+    <div data-offset-y="308" class="item first">
+      <span></span><br><span></span>
+    </div>
+    <div data-offset-y="360" class="item last">
+      <span></span><br><span></span>
+    </div>
+  </div>
+  <div data-offset-y="41" class="item small first"></div>
+  <div data-offset-y="110" class="item small last"></div>
+  <div data-offset-y="168" class="item small first"></div>
+  <div data-offset-y="243" class="item small last"></div>
+  <div data-offset-y="313" class="item small first"></div>
+  <div data-offset-y="385" class="item small last"></div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-006.html b/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-006.html
new file mode 100644
index 0000000..23d2b3b
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-006.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-grid-2/">
+<style>
+.item {
+  inline-size: 40px;
+  box-sizing: border-box;
+  border: solid 5px hotpink;
+  line-height: 0;
+  margin-block-start: 3px;
+  margin-block-end: 5px;
+}
+.small {
+  width: 30px;
+  height: 30px;
+  border: solid 5px cyan;
+}
+.first {
+  align-self: baseline;
+}
+.last {
+  align-self: last baseline;
+}
+span {
+  width: 20px;
+  height: 20px;
+  box-sizing: border-box;
+  border: solid 5px orange;
+  display: inline-block;
+}
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+<body onload="checkLayout('.item')">
+
+<div style="writing-mode: vertical-rl; width: 600px; position: relative; display: grid; grid-template: 150px 150px 150px / 100px 100px 100px 100px;">
+  <div style="display: grid;
+              gap: 10px;
+              grid-column: 1 / span 2;
+              grid-row: 1 / span 3;
+              grid-template: subgrid / subgrid;
+              margin-block-start: 5px; margin-block-end: 10px;
+              border: solid black 5px;
+              padding-block-start: 10px; padding-block-end: 20px;">
+    <div style="display: grid;
+                gap: 20px;
+                grid-column: 1 / span 2;
+                grid-row: 1 / span 2;
+                grid-template: subgrid / subgrid;
+                margin-block-start: 3px; margin-block-end: 7px;
+                border: solid black 5px;
+                padding-block-start: 5px; padding-block-end: 10px;">
+      <div data-offset-x="514" class="item first">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-x="465" class="item last">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-x="387" class="item first">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-x="332" class="item last">
+        <span></span><br><span></span>
+      </div>
+    </div>
+    <div data-offset-x="242" class="item first">
+      <span></span><br><span></span>
+    </div>
+    <div data-offset-x="190" class="item last">
+      <span></span><br><span></span>
+    </div>
+  </div>
+  <div data-offset-x="534" class="item small first"></div>
+  <div data-offset-x="465" class="item small last"></div>
+  <div data-offset-x="407" class="item small first"></div>
+  <div data-offset-x="332" class="item small last"></div>
+  <div data-offset-x="262" class="item small first"></div>
+  <div data-offset-x="190" class="item small last"></div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-007.html b/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-007.html
new file mode 100644
index 0000000..f8477be
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-007.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-grid-2/">
+<style>
+.item {
+  inline-size: 40px;
+  box-sizing: border-box;
+  border: solid 5px hotpink;
+  line-height: 0;
+  margin-block-start: 3px;
+  margin-block-end: 5px;
+}
+.small {
+  width: 20px;
+  height: 20px;
+  border: solid 5px cyan;
+}
+.first {
+  align-self: baseline;
+}
+.last {
+  align-self: last baseline;
+}
+.item.small.first {
+  block-size: 50px;
+}
+.item.small.last {
+  block-size: 100px;
+}
+span {
+  width: 20px;
+  height: 20px;
+  box-sizing: border-box;
+  border: solid 5px orange;
+  display: inline-block;
+}
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+<body onload="checkLayout('.item')">
+
+<div style="position: relative; display: grid; grid-template: auto auto auto / 100px 100px 100px 100px;">
+  <div style="display: grid;
+              gap: 10px;
+              grid-column: 1 / span 2;
+              grid-row: 1 / span 3;
+              grid-template: subgrid / subgrid;
+              margin-block-start: 5px; margin-block-end: 10px;
+              border: solid black 5px;
+              padding-block-start: 10px; padding-block-end: 20px;">
+    <div style="display: grid;
+                gap: 20px;
+                grid-column: 1 / span 2;
+                grid-row: 1 / span 2;
+                grid-template: subgrid / subgrid;
+                margin-block-start: 3px; margin-block-end: 7px;
+                border: solid black 5px;
+                padding-block-start: 5px; padding-block-end: 10px;">
+      <div data-offset-y="36" class="item first">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-y="58" class="item last">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-y="151" class="item first">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-y="181" class="item last">
+        <span></span><br><span></span>
+      </div>
+    </div>
+    <div data-offset-y="291" class="item first">
+      <span></span><br><span></span>
+    </div>
+    <div data-offset-y="321" class="item last">
+      <span></span><br><span></span>
+    </div>
+  </div>
+  <div data-offset-y="11" class="item small first"></div>
+  <div data-offset-y="3" class="item small last"></div>
+  <div data-offset-y="126" class="item small first"></div>
+  <div data-offset-y="126" class="item small last"></div>
+  <div data-offset-y="266" class="item small first"></div>
+  <div data-offset-y="266" class="item small last"></div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-008.html b/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-008.html
new file mode 100644
index 0000000..1ef2359
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-008.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-grid-2/">
+<style>
+.item {
+  inline-size: 40px;
+  box-sizing: border-box;
+  border: solid 5px hotpink;
+  line-height: 0;
+  margin-block-start: 3px;
+  margin-block-end: 5px;
+}
+.small {
+  width: 20px;
+  height: 20px;
+  border: solid 5px cyan;
+}
+.first {
+  align-self: baseline;
+}
+.last {
+  align-self: last baseline;
+}
+.item.small.first {
+  block-size: 50px;
+}
+.item.small.last {
+  block-size: 100px;
+}
+span {
+  width: 20px;
+  height: 20px;
+  box-sizing: border-box;
+  border: solid 5px orange;
+  display: inline-block;
+}
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+<body onload="checkLayout('.item')">
+
+<div style="writing-mode: vertical-rl; width: 600px; position: relative; display: grid; grid-template: auto auto auto / 100px 100px 100px 100px;">
+  <div style="display: grid;
+              gap: 10px;
+              grid-column: 1 / span 2;
+              grid-row: 1 / span 3;
+              grid-template: subgrid / subgrid;
+              margin-block-start: 5px; margin-block-end: 10px;
+              border: solid black 5px;
+              padding-block-start: 10px; padding-block-end: 20px;">
+    <div style="display: grid;
+                gap: 20px;
+                grid-column: 1 / span 2;
+                grid-row: 1 / span 2;
+                grid-template: subgrid / subgrid;
+                margin-block-start: 3px; margin-block-end: 7px;
+                border: solid black 5px;
+                padding-block-start: 5px; padding-block-end: 10px;">
+      <div data-offset-x="514" class="item first">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-x="428" class="item last">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-x="325" class="item first">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-x="234" class="item last">
+        <span></span><br><span></span>
+      </div>
+    </div>
+    <div data-offset-x="131" class="item first">
+      <span></span><br><span></span>
+    </div>
+    <div data-offset-x="40" class="item last">
+      <span></span><br><span></span>
+    </div>
+  </div>
+  <div data-offset-x="524" class="item small first"></div>
+  <div data-offset-x="393" class="item small last"></div>
+  <div data-offset-x="335" class="item small first"></div>
+  <div data-offset-x="199" class="item small last"></div>
+  <div data-offset-x="141" class="item small first"></div>
+  <div data-offset-x="5" class="item small last"></div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-009.html b/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-009.html
new file mode 100644
index 0000000..b7bf46d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-grid/subgrid/subgrid-baseline-009.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-grid-2/">
+<style>
+.item {
+  writing-mode: vertical-rl;
+  block-size: 80px;
+  box-sizing: border-box;
+  border: solid 5px hotpink;
+  line-height: 0;
+  margin-block-start: 10px;
+  margin-block-end: 15px;
+}
+.small {
+  width: 20px;
+  height: 20px;
+  border: solid 5px cyan;
+}
+.first {
+  justify-self: baseline;
+}
+.last {
+  justify-self: last baseline;
+}
+span {
+  width: 20px;
+  height: 20px;
+  box-sizing: border-box;
+  border: solid 5px orange;
+  display: inline-block;
+}
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+<body onload="checkLayout('.item')">
+
+<div style="width: 600px; position: relative; display: grid; grid-auto-flow: column; grid-template: 100px 100px 100px 100px / auto auto auto;">
+  <div style="display: grid;
+              grid-auto-flow: column;
+              gap: 10px;
+              grid-column: 1 / span 3;
+              grid-row: 1 / span 2;
+              grid-template: subgrid / subgrid;
+              margin-inline-start: 5px; margin-inline-end: 10px;
+              border: solid black 5px;
+              padding-inline-start: 10px; padding-inline-end: 20px;">
+    <div style="display: grid;
+                direction: rtl;
+                grid-auto-flow: column;
+                gap: 20px;
+                grid-column: 1 / span 2;
+                grid-row: 1 / span 2;
+                grid-template: subgrid / subgrid;
+                margin-inline-start: 3px; margin-inline-end: 7px;
+                border: solid black 5px;
+                padding-inline-start: 5px; padding-inline-end: 10px;">
+      <div data-offset-x="292" class="item first">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-x="237" class="item last">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-x="112" class="item first">
+        <span></span><br><span></span>
+      </div>
+      <div data-offset-x="57" class="item last">
+        <span></span><br><span></span>
+      </div>
+    </div>
+    <div data-offset-x="475" class="item first">
+      <span></span><br><span></span>
+    </div>
+    <div data-offset-x="420" class="item last">
+      <span></span><br><span></span>
+    </div>
+  </div>
+  <div data-offset-x="177" class="item small first"></div>
+  <div data-offset-x="102" class="item small last"></div>
+  <div data-offset-x="357" class="item small first"></div>
+  <div data-offset-x="282" class="item small last"></div>
+  <div data-offset-x="540" class="item small first"></div>
+  <div data-offset-x="465" class="item small last"></div>
+</div>
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/service-worker/service-worker-fetch-async-stacks.js b/third_party/blink/web_tests/http/tests/inspector-protocol/service-worker/service-worker-fetch-async-stacks.js
index 453c5f75..2b16d381 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/service-worker/service-worker-fetch-async-stacks.js
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/service-worker/service-worker-fetch-async-stacks.js
@@ -16,6 +16,16 @@
   };
   dp.Target.onAttachedToTarget(onAttached);
 
+  // Enable the debugger before registering a service worker so that
+  // the debugger can be attached even when the
+  // AllowDevToolsMainThreadDebuggerForMultipleMainFrames feature is disabled.
+  // When the feature is disabled, a renderer disallows the debugger when
+  // there are multiple browsing contexts. The following code will create a
+  // new browsing context, so enabling the debugger after registering a service
+  // worker will fail. Enabling the debugger earlier works around the issue.
+  // TODO(https://crbug.com/1434900): Remove this workaround.
+  await dp.Debugger.enable();
+
   await dp.ServiceWorker.enable();
   await session.navigate('resources/service-worker-fetch.html');
   testRunner.log('Navigated, registering service worker');
diff --git a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
index b70a1dc..1c3a7d3 100644
--- a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
+++ b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
@@ -1260,14 +1260,12 @@
     method replaceWith
     method substringData
     setter data
-interface ChildNodePart : Part
+interface ChildNodePart : PartRoot
     attribute @@toStringTag
     getter children
     getter nextSibling
     getter previousSibling
-    method clone
     method constructor
-    method getParts
     method replaceChildren
 interface Clipboard : EventTarget
     attribute @@toStringTag
@@ -6760,7 +6758,7 @@
     getter root
     method constructor
     method disconnect
-interface PartRoot
+interface PartRoot : Part
     attribute @@toStringTag
     method constructor
     method getParts
diff --git a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-001.html b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-001.html
index bd185585e..7e6e0a77 100644
--- a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-001.html
+++ b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-001.html
@@ -72,10 +72,9 @@
     target.getBoundingClientRect();
     axTarget = axElementById("target");
 
-    // Now #target is not locked, but expect the same 9 children ("target" text node, 4 whitespace
-    // nodes between divs, and 4 div nodes). That's because the nodes already exist, they were
-    // updated to become ignored but they are still included in the tree.
-    t.step(() => { assert_equals(axTarget.childrenCount, 9, "Children count when not locked"); });
+    // Now #target is not locked, expect 5 children ("target" text node, 4 divs).
+    // This is the same as it would be if content-visibility had never been used.
+    t.step(() => { assert_equals(axTarget.childrenCount, 5, "Children count when not locked"); });
 
     for (let i = 0; i < axTarget.childrenCount; ++i) {
       const axChild = axTarget.childAtIndex(i);
diff --git a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-002.html b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-002.html
index f1266eb..6e28e6d 100644
--- a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-002.html
+++ b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-002.html
@@ -45,18 +45,13 @@
   requestAnimationFrame(() => requestAnimationFrame(() => {
     // The ax object for #target got replaced since the layout object changed, so use the new ax object.
     axTarget = axElementById("target");
-    // It's still the same 5 children, but the non-whitespace nodes became unignored.
-    // Note whitespace nodes would not be there if the subtree was not hidden in first place, the total would be 3 children.
-    t.step(() => { assert_equals(axTarget.childrenCount, 5); });
+    // There are now 3 children, as the whitespace nodes are no longer included in the tree.
+    // This is the same as it would be if content-visibility had never been used.
+    t.step(() => { assert_equals(axTarget.childrenCount, 3); });
     t.step(() => { assert_equals(axTarget.name, "Label"); });
     for (let i = 0; i < axTarget.childrenCount; ++i) {
       const axChild = axTarget.childAtIndex(i);
-      if (i == 0 || axChild == axElementById("child") || axChild == axElementById("target_label")) {
-        t.step(() => { assert_false(axChild.isIgnored, "After update, isIgnored is false on non-whitespace child #" + i); });
-      } else {
-        // These are the whitespace nodes
-        t.step(() => { assert_true(axChild.isIgnored, "After update, isIgnored is true on whitespace child #" + i); });
-      }
+      t.step(() => { assert_false(axChild.isIgnored, "After update, isIgnored is false on child #" + i); });
     }
     t.done();
   }));
diff --git a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003.html b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003.html
index 99e00c7f..2dd5682 100644
--- a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003.html
+++ b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003.html
@@ -42,13 +42,10 @@
   // Wait for the next frame for the ax object to be recreated.
   requestAnimationFrame(() => {
     requestAnimationFrame(() => {
-      axHidden = axElementById("hidden");
-      // #hidden is now unlocked and saved as a normal AXLayoutObject, but it still has the same
-      // three child nodes. The whitespace node is still present because it was already created,
-      // but it's marked as ignored
-      t.step(() => { assert_equals(axHidden.childrenCount, 3, "Children count after activation"); });
-      const axChild = axHidden.childAtIndex(2);
-      t.step(() => { assert_true(axChild.isIgnored, "After update, isIgnored is true on whitespace node"); });
+      let axHidden = axElementById("hidden");
+      // Now #target is not locked, expect 2 children (no whitespace node).
+      // This is the same as it would be if content-visibility had never been used.
+      t.step(() => { assert_equals(axHidden.childrenCount, 2, "Children count after activation"); });
       t.done();
     });
   });
diff --git a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003a.html b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003a.html
index 504035ab..ca8056ee 100644
--- a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003a.html
+++ b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003a.html
@@ -31,9 +31,9 @@
   const listener = (e) => {
     if (!e.skipped) {
       let axHidden = axElementById("hidden");
-      t.step(() => { assert_equals(axHidden.childrenCount, 3, "Children count after activation"); });
-      const axChild = axHidden.childAtIndex(2);
-      t.step(() => { assert_true(axChild.isIgnored, "After update, isIgnored is true on whitespace node"); });
+      // Now #target is not locked, expect 2 children (no whitespace node).
+      // This is the same as it would be if content-visibility had never been used.
+      t.step(() => { assert_equals(axHidden.childrenCount, 2, "Children count after activation"); });
       t.done();
     }
   };
diff --git a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003b.html b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003b.html
index 01958da..8fe5062 100644
--- a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003b.html
+++ b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003b.html
@@ -31,9 +31,9 @@
   const listener = (e) => {
     if (!e.skipped) {
       let axHidden = axElementById("hidden");
-      t.step(() => { assert_equals(axHidden.childrenCount, 3, "Children count after activation"); });
-      const axChild = axHidden.childAtIndex(2);
-      t.step(() => { assert_true(axChild.isIgnored, "After update, isIgnored is true on whitespace node"); });
+      // Now #target is not locked, expect 2 children (no whitespace node).
+      // This is the same as it would be if content-visibility had never been used.
+      t.step(() => { assert_equals(axHidden.childrenCount, 2, "Children count after activation"); });
       t.done();
     }
   };
diff --git a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003c.html b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003c.html
index 93fa18d..0b5afda 100644
--- a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003c.html
+++ b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-003c.html
@@ -43,12 +43,9 @@
   requestAnimationFrame(() => {
     requestAnimationFrame(() => {
       axHidden = axElementById("hidden");
-      // #hidden is now unlocked and saved as a normal AXLayoutObject, but it still has the same
-      // three child nodes. The whitespace node is still present because it was already created,
-      // but it's marked as ignored
-      t.step(() => { assert_equals(axHidden.childrenCount, 3, "Children count after activation"); });
-      const axChild = axHidden.childAtIndex(2);
-      t.step(() => { assert_true(axChild.isIgnored, "After update, isIgnored is true on whitespace node"); });
+      // Now #target is not locked, expect 2 children (no whitespace node).
+      // This is the same as it would be if content-visibility had never been used.
+      t.step(() => { assert_equals(axHidden.childrenCount, 2, "Children count after activation"); });
       t.done();
     });
   });
diff --git a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-004.html b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-004.html
index 2629f2d..b7bb2392 100644
--- a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-004.html
+++ b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-004.html
@@ -43,12 +43,9 @@
   requestAnimationFrame(() => {
     requestAnimationFrame(() => {
       axHidden = axElementById("hidden");
-      // #hidden is now unlocked and saved as a normal AXLayoutObject, but it still has the same
-      // three child nodes. The whitespace node is still present because it was already created,
-      // but it's marked as ignored
-      t.step(() => { assert_equals(axHidden.childrenCount, 3, "Children count after activation"); });
-      const axChild = axHidden.childAtIndex(2);
-      t.step(() => { assert_true(axChild.isIgnored, "After update, isIgnored is true on whitespace node"); });
+      // There are now 2 children, as the whitespace nodes are no longer included in the tree.
+      // This is the same as it would be if content-visibility had never been used.
+      t.step(() => { assert_equals(axHidden.childrenCount, 2, "Children count after activation"); });
       t.done();
     });
   });
diff --git a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-005.html b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-005.html
index 159dcf6a..79996c8 100644
--- a/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-005.html
+++ b/third_party/blink/web_tests/wpt_internal/display-lock/css-content-visibility/accessibility/content-visibility-accessibility-005.html
@@ -47,15 +47,11 @@
     requestAnimationFrame(() => { // Frame 3.
       requestAnimationFrame(() => { // Frame 4.
         axHidden = axElementById("hidden");
-        // #hidden is now unlocked and saved as a normal AXLayoutObject but it still has the same
-        // three child nodes. The whitespace node is still present because it was already created,
-        // but it's marked as ignored
+        // There are now 2 children, as the whitespace nodes are no longer included in the tree.
+        // This is the same as it would be if content-visibility had never been used.
         t.step(() => {
-          assert_equals(axHidden.childrenCount, 3, "Child count after activation");
+          assert_equals(axHidden.childrenCount, 2, "Child count after activation");
         });
-        const axChild = axHidden.childAtIndex(2);
-        t.step(() => {
-          assert_true(axChild.isIgnored, "After update, isIgnored is true on whitespace node"); });
         t.done();
       });
     });
diff --git a/third_party/closure_compiler/externs/file_manager_private.js b/third_party/closure_compiler/externs/file_manager_private.js
index 503434c..28d7a69 100644
--- a/third_party/closure_compiler/externs/file_manager_private.js
+++ b/third_party/closure_compiler/externs/file_manager_private.js
@@ -458,7 +458,8 @@
  */
 chrome.fileManagerPrivate.BulkPinStage = {
   STOPPED: 'stopped',
-  PAUSED: 'paused',
+  PAUSED_OFFLINE: 'paused_offline',
+  PAUSED_BATTERY_SAVER: 'paused_battery_saver',
   GETTING_FREE_SPACE: 'getting_free_space',
   LISTING_FILES: 'listing_files',
   SYNCING: 'syncing',
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index e1c470ba..25c56c0 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -55146,13 +55146,6 @@
   </int>
 </enum>
 
-<enum name="JavaJsBridgeMethodInvocationError">
-  <int value="0" label="MethodInvocationAsConstructorDisallowed"/>
-  <int value="1" label="MethodInvocationNonexistentMethod"/>
-  <int value="2" label="MethodInvocationOnNonInjectedObjectDisallowed"/>
-  <int value="3" label="MethodInvocationErrorMessage"/>
-</enum>
-
 <enum name="JavaScriptDialogDismissalCause">
   <int value="0" label="Tab closed">The tab owning the dialog was closed</int>
   <int value="1" label="New dialog">
@@ -59571,6 +59564,7 @@
       label="OptimizationGuideInstallWideModelStore:enabled"/>
   <int value="-1776351704" label="DesktopPWAsOmniboxInstall:disabled"/>
   <int value="-1775842908" label="EnableOAuthIpp:disabled"/>
+  <int value="-1774921110" label="ServiceWorkerStaticRouter:enabled"/>
   <int value="-1774818943" label="VrWebInputEditing:enabled"/>
   <int value="-1774290918" label="webview-shadow-dom-fenced-frames"/>
   <int value="-1773925297" label="TimedHtmlParserBudget:disabled"/>
@@ -62402,6 +62396,7 @@
       label="CrossOriginMediaPlaybackRequiresUserGesture:disabled"/>
   <int value="-296762162" label="ExoOrdinalMotion:enabled"/>
   <int value="-296493265" label="ProjectorAppDebug:enabled"/>
+  <int value="-296208832" label="ServiceWorkerStaticRouter:disabled"/>
   <int value="-296179618" label="CookiesWithoutSameSiteMustBeSecure:enabled"/>
   <int value="-295237704" label="EnableRemovingAllThirdPartyCookies:enabled"/>
   <int value="-293546827" label="HideIncognitoMediaMetadata:enabled"/>
@@ -66226,6 +66221,7 @@
   <int value="1719958026" label="QueryTiles:enabled"/>
   <int value="1720399117" label="SkiaGraphite:enabled"/>
   <int value="1721854955" label="EnableEdidBasedDisplayIds:disabled"/>
+  <int value="1722521465" label="CrosBatterySaverAlwaysOn:disabled"/>
   <int value="1722748383" label="EnableAppReinstallZeroState:disabled"/>
   <int value="1723601083" label="enable-app-window-controls"/>
   <int value="1724247189" label="StylusHandwriting:enabled"/>
@@ -66650,6 +66646,7 @@
       label="EnableBluetoothVerboseLogsForGooglers:enabled"/>
   <int value="1930901873" label="disable-sync-app-list"/>
   <int value="1931309368" label="fill-on-account-select:disabled"/>
+  <int value="1932038907" label="CrosBatterySaverAlwaysOn:enabled"/>
   <int value="1932204471" label="SyncPseudoUSSThemes:disabled"/>
   <int value="1932732886" label="OpenVR:enabled"/>
   <int value="1933282728" label="OmniboxUICuesForSearchHistoryMatches:enabled"/>
@@ -100947,6 +100944,43 @@
   <int value="4" label="The trigger is in monitor mode"/>
 </enum>
 
+<enum name="SVCScalabilityMode">
+  <int value="0" label="L1T1"/>
+  <int value="1" label="L1T2"/>
+  <int value="2" label="L1T3"/>
+  <int value="3" label="L2T1"/>
+  <int value="4" label="L2T2"/>
+  <int value="5" label="L2T3"/>
+  <int value="6" label="L3T1"/>
+  <int value="7" label="L3T2"/>
+  <int value="8" label="L3T3"/>
+  <int value="9" label="L2T1h"/>
+  <int value="10" label="L2T2h"/>
+  <int value="11" label="L2T3h"/>
+  <int value="12" label="S2T1"/>
+  <int value="13" label="S2T2"/>
+  <int value="14" label="S2T3"/>
+  <int value="15" label="S2T1h"/>
+  <int value="16" label="S2T2h"/>
+  <int value="17" label="S2T3h"/>
+  <int value="18" label="S3T1"/>
+  <int value="19" label="S3T2"/>
+  <int value="20" label="S3T3"/>
+  <int value="21" label="S3T1h"/>
+  <int value="22" label="S3T2h"/>
+  <int value="23" label="S3T3h"/>
+  <int value="24" label="L2T1Key"/>
+  <int value="25" label="L2T2Key"/>
+  <int value="26" label="L2T2KeyShift"/>
+  <int value="27" label="L2T3Key"/>
+  <int value="28" label="L2T3KeyShift"/>
+  <int value="29" label="L3T1Key"/>
+  <int value="30" label="L3T2Key"/>
+  <int value="31" label="L3T2KeyShift"/>
+  <int value="32" label="L3T3Key"/>
+  <int value="33" label="L3T3KeyShift"/>
+</enum>
+
 <enum name="SwapchainFormat">
   <int value="0" label="B8G8R8A8"/>
   <int value="1" label="YUY2"/>
@@ -108364,6 +108398,13 @@
   <int value="18" label="OOP Video Decoder"/>
 </enum>
 
+<enum name="VideoEncoderUseCase">
+  <int value="0" label="CastMirroring"/>
+  <int value="1" label="MediaRecorder"/>
+  <int value="2" label="WebCodecs"/>
+  <int value="3" label="WebRTC"/>
+</enum>
+
 <enum name="VideoFormat">
   <obsolete>
     Deprecated as of 05/2015. Substituted by VideoFramePixelFormat.
diff --git a/tools/metrics/histograms/metadata/ash/histograms.xml b/tools/metrics/histograms/metadata/ash/histograms.xml
index 1791408..9d46c48 100644
--- a/tools/metrics/histograms/metadata/ash/histograms.xml
+++ b/tools/metrics/histograms/metadata/ash/histograms.xml
@@ -5761,23 +5761,6 @@
 </histogram>
 
 <histogram
-    name="Ash.Shelf.ShutdownConfirmationBubble.ActionDuration.{BubbleAction}"
-    units="ms" expires_after="2022-12-31">
-  <owner>sherrilin@google.com</owner>
-  <owner>cros-lurs@google.com</owner>
-  <summary>
-    Track time delta between a bubble opened and an action taken on the shutdown
-    confirmation bubble.
-  </summary>
-  <token key="BubbleAction">
-    <variant name="Cancel" summary="User clicked cancel button"/>
-    <variant name="Confirm" summary="User clicked confirm button"/>
-    <variant name="Dismiss"
-        summary="User dismissed the shutdown confirmation buble"/>
-  </token>
-</histogram>
-
-<histogram
     name="Ash.Shelf.ShutdownConfirmationBubble.TimeToNextBoot.LoginShutdownToPowerUpDuration"
     units="seconds" expires_after="2023-09-10">
   <owner>sherrilin@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/blink/histograms.xml b/tools/metrics/histograms/metadata/blink/histograms.xml
index 78a8e52..0cb8d9a6a 100644
--- a/tools/metrics/histograms/metadata/blink/histograms.xml
+++ b/tools/metrics/histograms/metadata/blink/histograms.xml
@@ -2227,13 +2227,6 @@
   </summary>
 </histogram>
 
-<histogram name="Blink.JavaJsBridge.MethodInvocationError"
-    enum="JavaJsBridgeMethodInvocationError" expires_after="2023-06-04">
-  <owner>torne@chromium.org</owner>
-  <owner>src/android_webview/OWNERS</owner>
-  <summary>Records the Java/JS bridge method invocation errors.</summary>
-</histogram>
-
 <histogram base="true" name="Blink.JavascriptDocumentUpdate.UpdateTime"
     units="microseconds" expires_after="2023-09-10">
 <!-- Name completed by histogram_suffixes name="BlinkUpdateTimePreFCPSuffixes" -->
diff --git a/tools/metrics/histograms/metadata/borealis/histograms.xml b/tools/metrics/histograms/metadata/borealis/histograms.xml
index 23458c19..31804e5 100644
--- a/tools/metrics/histograms/metadata/borealis/histograms.xml
+++ b/tools/metrics/histograms/metadata/borealis/histograms.xml
@@ -22,6 +22,16 @@
 
 <histograms>
 
+<histogram name="Borealis.Audio.UsedSubdevices" units="count"
+    expires_after="2023-11-09">
+  <owner>normanbt@google.com</owner>
+  <owner>src/chrome/browser/ash/borealis/OWNERS</owner>
+  <summary>
+    The number of audio subdevices that are used by games that are running on
+    proton. This is recorded periodically when the game is running.
+  </summary>
+</histogram>
+
 <histogram name="Borealis.Disk.HighestDirtyPagesDaily" units="KiB"
     expires_after="2023-10-09">
   <owner>danielng@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/net/histograms.xml b/tools/metrics/histograms/metadata/net/histograms.xml
index a95b0cf1..ed902e10 100644
--- a/tools/metrics/histograms/metadata/net/histograms.xml
+++ b/tools/metrics/histograms/metadata/net/histograms.xml
@@ -254,7 +254,7 @@
 </histogram>
 
 <histogram name="HttpCache.IsNoStore{Type}" enum="Boolean"
-    expires_after="2023-07-27">
+    expires_after="2024-06-13">
   <owner>bashi@chromium.org</owner>
   <owner>blink-network-stack@google.com</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/optimization/histograms.xml b/tools/metrics/histograms/metadata/optimization/histograms.xml
index c0621a28..cacce18 100644
--- a/tools/metrics/histograms/metadata/optimization/histograms.xml
+++ b/tools/metrics/histograms/metadata/optimization/histograms.xml
@@ -35,6 +35,8 @@
 
 <variants name="OptimizationTarget">
   <variant name="ClientSidePhishing" summary="Client Side Phishing"/>
+  <variant name="ClientSidePhishingImageEmbedder"
+      summary="Client Side Phishing Image Embedder"/>
   <variant name="ContextualPageActionPriceTracking"
       summary="Contextual Page Action: Show price tracking"/>
   <variant name="GeolocationPermissions" summary="Geolocation permissions"/>
diff --git a/tools/metrics/histograms/metadata/sb_client/histograms.xml b/tools/metrics/histograms/metadata/sb_client/histograms.xml
index d1fccb3..0f3f15b6 100644
--- a/tools/metrics/histograms/metadata/sb_client/histograms.xml
+++ b/tools/metrics/histograms/metadata/sb_client/histograms.xml
@@ -675,6 +675,17 @@
   </summary>
 </histogram>
 
+<histogram name="SBClientPhishing.ImageEmbeddingModelVersionMatch"
+    enum="BooleanMatched" expires_after="2024-06-05">
+  <owner>andysjlim@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Logs whether the image embedding model version is matched or not so that the
+    correctly paired trigger and image embedidng model are sent together to the
+    renderer process
+  </summary>
+</histogram>
+
 <histogram name="SBClientPhishing.LocalModelDetectsPhishing"
     enum="BooleanIsPhishing" expires_after="2023-12-04">
   <owner>drubery@chromium.org</owner>
@@ -704,8 +715,8 @@
 </histogram>
 
 <histogram name="SBClientPhishing.ModelDynamicUpdateSuccess"
-    enum="BooleanSuccess" expires_after="2023-12-04">
-  <owner>drubery@chromium.org</owner>
+    enum="BooleanSuccess" expires_after="2024-06-08">
+  <owner>andysjlim@chromium.org</owner>
   <owner>chrome-counter-abuse-alerts@google.com</owner>
   <summary>
     Records whether a dynamic update is successful or not. This is logged when a
@@ -713,6 +724,17 @@
   </summary>
 </histogram>
 
+<histogram name="SBClientPhishing.ModelDynamicUpdateSuccess.ImageEmbedding"
+    enum="BooleanSuccess" expires_after="2024-05-23">
+  <owner>andysjlim@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Records whether a dynamic update of the image embedding model is successful
+    or not. This is logged when a new image embedding model is fetched, or on
+    each startup.
+  </summary>
+</histogram>
+
 <histogram name="SBClientPhishing.NetworkRequestDuration" units="ms"
     expires_after="2024-03-19">
   <owner>andysjlim@chromium.org</owner>
diff --git a/tools/metrics/ukm/ukm.xml b/tools/metrics/ukm/ukm.xml
index 85fed87..598b52d 100644
--- a/tools/metrics/ukm/ukm.xml
+++ b/tools/metrics/ukm/ukm.xml
@@ -13515,6 +13515,113 @@
   </metric>
 </event>
 
+<event name="Media.VideoEncoderMetrics">
+  <owner>hiroh@chromium.org</owner>
+  <owner>media-dev@chromium.org</owner>
+  <summary>
+    A record of encoding usage metrics in chrome, for example, for cast
+    mirroring and webcodecs. This is recorded in the end of each video encoding
+    session.
+  </summary>
+  <metric name="Height">
+    <summary>
+      Integer value indicating the height of the encoded stream. This is
+      bucketed per 100 and capped by 8200.
+    </summary>
+    <aggregation>
+      <history>
+        <statistics>
+          <quantiles type="std-percentiles"/>
+        </statistics>
+      </history>
+    </aggregation>
+  </metric>
+  <metric name="IsHardware" enum="Boolean">
+    <summary>
+      Boolean value indicating whether the encoder implementation is hardware
+      accelerated one or not.
+    </summary>
+    <aggregation>
+      <history>
+        <statistics>
+          <enumeration/>
+        </statistics>
+      </history>
+    </aggregation>
+  </metric>
+  <metric name="NumEncodedFrames">
+    <summary>
+      Integer value indicating the number of the successfully encoded frames.
+      This is bucketed per 100.
+    </summary>
+  </metric>
+  <metric name="Profile" enum="VideoCodecProfile">
+    <summary>
+      media::VideoCodecProfile enum value. The codec profile configured to
+      encode the video.
+    </summary>
+    <aggregation>
+      <history>
+        <statistics>
+          <enumeration/>
+        </statistics>
+      </history>
+    </aggregation>
+  </metric>
+  <metric name="Status" enum="EncoderStatus">
+    <summary>
+      media::EncoderStatus::Codes enum value. The first error status code on
+      error, otherwise media::EncoderStatus::Codes::kOk.
+    </summary>
+    <aggregation>
+      <history>
+        <statistics>
+          <enumeration/>
+        </statistics>
+      </history>
+    </aggregation>
+  </metric>
+  <metric name="SVCMode" enum="SVCScalabilityMode">
+    <summary>
+      Integer value indicating the SVC configuration of the encoding. The value
+      corresponds to media::SVCScalabilityMode.
+    </summary>
+    <aggregation>
+      <history>
+        <statistics>
+          <enumeration/>
+        </statistics>
+      </history>
+    </aggregation>
+  </metric>
+  <metric name="UseCase" enum="VideoEncoderUseCase">
+    <summary>
+      Integer value indicating the video encoder use case. The value corresponds
+      to media:::mojom::VideoEncoderUseCase.
+    </summary>
+    <aggregation>
+      <history>
+        <statistics>
+          <enumeration/>
+        </statistics>
+      </history>
+    </aggregation>
+  </metric>
+  <metric name="Width">
+    <summary>
+      Integer value indicating the width of the encoded stream. This is bucketed
+      per 100 and capped by 8200.
+    </summary>
+    <aggregation>
+      <history>
+        <statistics>
+          <quantiles type="std-percentiles"/>
+        </statistics>
+      </history>
+    </aggregation>
+  </metric>
+</event>
+
 <event name="Media.WatchTime">
   <obsolete>
     Deprecated 8/2017 in favor Media.BasicPlayback
diff --git a/tools/traffic_annotation/summary/annotations.xml b/tools/traffic_annotation/summary/annotations.xml
index 4fd62e7..89ed8f6 100644
--- a/tools/traffic_annotation/summary/annotations.xml
+++ b/tools/traffic_annotation/summary/annotations.xml
@@ -375,7 +375,7 @@
  <item id="ntp_custom_background" added_in_milestone="109" content_hash_code="08080082" os_list="linux,windows,chromeos" file_path="chrome/browser/search/background/ntp_custom_background_service.cc" />
  <item id="iwa_policy_update_manifest" added_in_milestone="110" second_id="iwa_update_manifest_fetcher" content_hash_code="07d65fe9" os_list="chromeos" file_path="chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_manager.cc" />
  <item id="iwa_policy_signed_web_bundle" added_in_milestone="110" second_id="iwa_bundle_downloader" content_hash_code="00a27db9" os_list="chromeos" file_path="chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_manager.cc" />
- <item id="quick_start_challenge_bytes_fetcher" added_in_milestone="110" content_hash_code="050ba600" os_list="chromeos" file_path="chrome/browser/ash/login/oobe_quick_start/second_device_auth_broker.cc" />
+ <item id="quick_start_challenge_bytes_fetcher" added_in_milestone="110" content_hash_code="0546c0a2" os_list="chromeos" file_path="chrome/browser/ash/login/oobe_quick_start/second_device_auth_broker.cc" />
  <item id="customization_document" added_in_milestone="110" content_hash_code="007a3dda" os_list="chromeos" file_path="chrome/browser/ash/customization/customization_document.cc" />
  <item id="nearby_webrtc_connection" added_in_milestone="110" content_hash_code="070e1f72" os_list="chromeos" file_path="chrome/services/sharing/nearby/platform/webrtc.cc" />
  <item id="fenced_frame_reporting_beacon" added_in_milestone="110" content_hash_code="03fd07f7" os_list="linux,windows,android,chromeos" file_path="content/browser/fenced_frame/fenced_frame_reporter.cc" />
diff --git a/ui/accessibility/ax_tree.cc b/ui/accessibility/ax_tree.cc
index f59dd94..5a2da51 100644
--- a/ui/accessibility/ax_tree.cc
+++ b/ui/accessibility/ax_tree.cc
@@ -27,7 +27,6 @@
 #include "base/timer/elapsed_timer.h"
 #include "components/crash/core/common/crash_key.h"
 #include "ui/accessibility/accessibility_features.h"
-#include "ui/accessibility/accessibility_switches.h"
 #include "ui/accessibility/ax_enums.mojom.h"
 #include "ui/accessibility/ax_event.h"
 #include "ui/accessibility/ax_language_detection.h"
diff --git a/ui/accessibility/platform/ax_fragment_root_win_unittest.cc b/ui/accessibility/platform/ax_fragment_root_win_unittest.cc
index 7c6ee91..ba413e7 100644
--- a/ui/accessibility/platform/ax_fragment_root_win_unittest.cc
+++ b/ui/accessibility/platform/ax_fragment_root_win_unittest.cc
@@ -11,7 +11,6 @@
 #include "base/win/scoped_safearray.h"
 #include "base/win/scoped_variant.h"
 #include "testing/gtest/include/gtest/gtest.h"
-#include "ui/accessibility/accessibility_switches.h"
 #include "ui/accessibility/platform/ax_platform_node_win.h"
 #include "ui/accessibility/platform/ax_platform_node_win_unittest.h"
 #include "ui/accessibility/platform/test_ax_node_wrapper.h"
diff --git a/ui/base/ui_base_features.cc b/ui/base/ui_base_features.cc
index ed01cfd..a7a25be 100644
--- a/ui/base/ui_base_features.cc
+++ b/ui/base/ui_base_features.cc
@@ -438,7 +438,7 @@
 // Enables chrome color management wayland protocol for lacros.
 BASE_FEATURE(kLacrosColorManagement,
              "LacrosColorManagement",
-             base::FEATURE_DISABLED_BY_DEFAULT);
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 bool IsLacrosColorManagementEnabled() {
   return base::FeatureList::IsEnabled(kLacrosColorManagement);
diff --git a/ui/chromeos/file_manager_strings.grdp b/ui/chromeos/file_manager_strings.grdp
index 1e53071..02c8771 100644
--- a/ui/chromeos/file_manager_strings.grdp
+++ b/ui/chromeos/file_manager_strings.grdp
@@ -1780,6 +1780,9 @@
     Preparing to sync Drive files...
   </message>
   <!-- TODO(b/276126188): Remove the translateable="false" when the copy has been landed on. -->
+  <message name="IDS_FILE_BROWSER_BULK_PINNING_BATTERY_SAVER_LABEL" translateable="false" desc="The text used when the device is in battery saver mode and has paused syncing.">
+    Battery saver mode on. Sync will resume when battery saver mode is turned off.
+  </message>
   <message name="IDS_FILE_BROWSER_BULK_PINNING_OFFLINE_LABEL" translateable="false" desc="The text used when the device is in an offline mode and has paused syncing.">
     You are currently offline. Sync will resume when the connection is back on.
   </message>
@@ -1795,6 +1798,9 @@
   <message name="IDS_FILE_BROWSER_BULK_PINNING_VIEW_STORAGE" translateable="false" desc="Text of the 'View storage' link displayed next to the error message when there is not enough space to enable the Google Drive bulk pinning feature">
     View storage
   </message>
+  <message name="IDS_FILE_BROWSER_BULK_PINNING_BATTERY_SAVER" translateable="false" desc="Error message displayed when the Google Drive bulk pinning feature cannot be enabled because battery saver mode is active">
+    Battery saver mode on. Try again when battery saver mode is turned off.
+  </message>
   <message name="IDS_FILE_BROWSER_BULK_PINNING_OFFLINE" translateable="false" desc="Error message displayed when the Google Drive bulk pinning feature cannot be enabled because of lack of internet connection">
     You are currently offline. Try again when you are connected to the internet.
   </message>
diff --git a/ui/chromeos/file_manager_strings_grdp/IDS_FILE_BROWSER_BULK_PINNING_BATTERY_SAVER.png.sha1 b/ui/chromeos/file_manager_strings_grdp/IDS_FILE_BROWSER_BULK_PINNING_BATTERY_SAVER.png.sha1
new file mode 100644
index 0000000..e54a1905
--- /dev/null
+++ b/ui/chromeos/file_manager_strings_grdp/IDS_FILE_BROWSER_BULK_PINNING_BATTERY_SAVER.png.sha1
@@ -0,0 +1 @@
+88e6541e7c74fdbb07969259479cb7564c0a3fe1
\ No newline at end of file
diff --git a/ui/chromeos/file_manager_strings_grdp/IDS_FILE_BROWSER_BULK_PINNING_BATTERY_SAVER_LABEL.png.sha1 b/ui/chromeos/file_manager_strings_grdp/IDS_FILE_BROWSER_BULK_PINNING_BATTERY_SAVER_LABEL.png.sha1
new file mode 100644
index 0000000..8e3cc73
--- /dev/null
+++ b/ui/chromeos/file_manager_strings_grdp/IDS_FILE_BROWSER_BULK_PINNING_BATTERY_SAVER_LABEL.png.sha1
@@ -0,0 +1 @@
+6548f0874c7aa3bc47013bae4235773db45226b7
\ No newline at end of file
diff --git a/ui/file_manager/file_manager/BUILD.gn b/ui/file_manager/file_manager/BUILD.gn
index 920ee8c..4ac47fa 100644
--- a/ui/file_manager/file_manager/BUILD.gn
+++ b/ui/file_manager/file_manager/BUILD.gn
@@ -58,6 +58,7 @@
     "foreground/images/common/ic_selected.svg",
     "foreground/images/files/ui/arrow_right.svg",
     "foreground/images/files/ui/back.svg",
+    "foreground/images/files/ui/bulk_pinning_battery_saver.svg",
     "foreground/images/files/ui/bulk_pinning_done.svg",
     "foreground/images/files/ui/bulk_pinning_offline.svg",
     "foreground/images/files/ui/check.svg",
diff --git a/ui/file_manager/file_manager/common/js/util.js b/ui/file_manager/file_manager/common/js/util.js
index a4306d4..c0f3aff 100644
--- a/ui/file_manager/file_manager/common/js/util.js
+++ b/ui/file_manager/file_manager/common/js/util.js
@@ -1586,21 +1586,21 @@
     return false;
   }
 
+  const BulkPinStage = chrome.fileManagerPrivate.BulkPinStage;
   // If the stage is in progress and the bulk pinning preference is enabled,
   // then the cloud panel should not be visible.
   if (pref &&
-      (stage === chrome.fileManagerPrivate.BulkPinStage.GETTING_FREE_SPACE ||
-       stage === chrome.fileManagerPrivate.BulkPinStage.LISTING_FILES ||
-       stage === chrome.fileManagerPrivate.BulkPinStage.SYNCING)) {
+      (stage === BulkPinStage.GETTING_FREE_SPACE ||
+       stage === BulkPinStage.LISTING_FILES ||
+       stage === BulkPinStage.SYNCING)) {
     return true;
   }
 
-  // The `PAUSED` stage represents the user being offline and the
-  // `NOT_ENOUGH_SPACE` represents the user not having enough space on disk
-  // to download. For the former the preference should still be enabled,
-  // however, for the latter the preference will have been disabled.
-  if ((stage === chrome.fileManagerPrivate.BulkPinStage.PAUSED && pref) ||
-      stage === chrome.fileManagerPrivate.BulkPinStage.NOT_ENOUGH_SPACE) {
+  // For the PAUSED... states the preference should still be enabled, however,
+  // for the latter the preference will have been disabled.
+  if ((stage === BulkPinStage.PAUSED_OFFLINE && pref) ||
+      (stage === BulkPinStage.PAUSED_BATTERY_SAVER && pref) ||
+      stage === BulkPinStage.NOT_ENOUGH_SPACE) {
     return true;
   }
 
diff --git a/ui/file_manager/file_manager/containers/cloud_panel_container.ts b/ui/file_manager/file_manager/containers/cloud_panel_container.ts
index e13cd80..b8dcd584 100644
--- a/ui/file_manager/file_manager/containers/cloud_panel_container.ts
+++ b/ui/file_manager/file_manager/containers/cloud_panel_container.ts
@@ -97,12 +97,15 @@
     }
 
     // If the bulk pinning is paused, this indicates that it is currently
-    // offline. This could be from either the network not being connected or
-    // cellular being disabled for syncing.
-    if (bulkPinProgress.stage === BulkPinStage.PAUSED) {
+    // offline or battery saver mode is active.
+    if (bulkPinProgress.stage === BulkPinStage.PAUSED_OFFLINE) {
       this.updatePanelType_(CloudPanelType.OFFLINE);
       return;
     }
+    if (bulkPinProgress.stage === BulkPinStage.PAUSED_BATTERY_SAVER) {
+      this.updatePanelType_(CloudPanelType.BATTERY_SAVER);
+      return;
+    }
 
     // Not enough space indicates the available local storage is insufficient to
     // store all the files required by the users My drive.
diff --git a/ui/file_manager/file_manager/containers/cloud_panel_container_unittest.ts b/ui/file_manager/file_manager/containers/cloud_panel_container_unittest.ts
index 564f3f5..3244f9e 100644
--- a/ui/file_manager/file_manager/containers/cloud_panel_container_unittest.ts
+++ b/ui/file_manager/file_manager/containers/cloud_panel_container_unittest.ts
@@ -312,20 +312,31 @@
   // Pausing the bulk pinning operation does not update the attributes except
   // changing the type attribute to offline.
   store.dispatch(updateBulkPinProgress(
-      {...bulkPinning, pinnedBytes: 200, stage: BulkPinStage.PAUSED}));
+      {...bulkPinning, pinnedBytes: 200, stage: BulkPinStage.PAUSED_OFFLINE}));
   assertEquals(
       container!.updates, 2,
       'Bulk pin state stage should increment updates to 2');
   assertEquals(panel!.getAttribute('type'), 'offline');
   assertFalse(panel!.hasAttribute('items'));
   assertFalse(panel!.hasAttribute('percentage'));
+  store.dispatch(updateBulkPinProgress({
+    ...bulkPinning,
+    pinnedBytes: 200,
+    stage: BulkPinStage.PAUSED_BATTERY_SAVER,
+  }));
+  assertEquals(
+      container!.updates, 3,
+      'Bulk pin state stage should increment updates to 3');
+  assertEquals(panel!.getAttribute('type'), 'battery_saver');
+  assertFalse(panel!.hasAttribute('items'));
+  assertFalse(panel!.hasAttribute('percentage'));
 
   // Switching back into `SYNCING` with new pinned bytes removes the type
   // attribute and updates the attributes.
   store.dispatch(updateBulkPinProgress({...bulkPinning, pinnedBytes: 300}));
   assertEquals(
-      container!.updates, 3,
-      'Bulk pin state stage should increment updates to 3');
+      container!.updates, 4,
+      'Bulk pin state stage should increment updates to 4');
   assertFalse(panel!.hasAttribute('type'));
   assertEquals(panel!.getAttribute('items'), '10');
   assertEquals(panel!.getAttribute('percentage'), '30');
diff --git a/ui/file_manager/file_manager/foreground/css/file_manager_gm3.css b/ui/file_manager/file_manager/foreground/css/file_manager_gm3.css
index 6deccea..5534e9a5 100644
--- a/ui/file_manager/file_manager/foreground/css/file_manager_gm3.css
+++ b/ui/file_manager/file_manager/foreground/css/file_manager_gm3.css
@@ -1244,7 +1244,8 @@
 
 /* Only display outlines for entries with thumbnails. */
 #list-container.thumbnail-view li:has(.thumbnail) .inline-status .progress,
-#list-container.thumbnail-view li.pinned:has(.thumbnail) .inline-status xf-icon[type=offline] {
+#list-container.thumbnail-view li.pinned:has(.thumbnail) .inline-status xf-icon[type=offline],
+#list-container.thumbnail-view li.cant-pin:has(.thumbnail) .inline-status xf-icon[type=cant-pin] {
   --xf-icon-color-outline: var(--cros-sys-app_base);
 }
 
diff --git a/ui/file_manager/file_manager/foreground/images/files/ui/bulk_pinning_battery_saver.svg b/ui/file_manager/file_manager/foreground/images/files/ui/bulk_pinning_battery_saver.svg
new file mode 100644
index 0000000..7c5bcd29
--- /dev/null
+++ b/ui/file_manager/file_manager/foreground/images/files/ui/bulk_pinning_battery_saver.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
+  <path d="m38.1 38-3.6-3.6-16.2-16.2-2.9-2.9-8-8-2.8 2.9 7 7a10.8 10.8 0 0 0 2.7 21.2h18.5l5.1 5.1 2.8-2.8-2.6-2.7Zm-23.9-3.7a6.8 6.8 0 0 1 0-13.5h1l13.6 13.5Zm22.1-13.8a11.9 11.9 0 0 0-17.9-7.8l3 2.9a7.8 7.8 0 0 1 3.3-.7 7.9 7.9 0 0 1 7.9 7.6v2h3a5 5 0 0 1 5 4.9 4.9 4.9 0 0 1-1.7 3.7l2.8 2.8a9 9 0 0 0-5.3-15.4Z"/>
+</svg>
\ No newline at end of file
diff --git a/ui/file_manager/file_manager/foreground/images/files/ui/cant_pin.svg b/ui/file_manager/file_manager/foreground/images/files/ui/cant_pin.svg
index 979a6f7d..e2832e5 100644
--- a/ui/file_manager/file_manager/foreground/images/files/ui/cant_pin.svg
+++ b/ui/file_manager/file_manager/foreground/images/files/ui/cant_pin.svg
@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 16 16" viewBox="0 0 16 16"><path d="M2 6.2h12v3.6H2z"/><path d="M3 7.2h10v1.6H3z"/></svg>
\ No newline at end of file
+<svg id="cant_pin" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 16 16" viewBox="0 0 16 16">
+  <path d="M2 6.2h12v3.6H2z" style="fill: var(--xf-icon-color-outline, transparent);"/>
+  <path d="M3 7.2h10v1.6H3z" style="fill: var(--xf-icon-color);"/>
+</svg>
\ No newline at end of file
diff --git a/ui/file_manager/file_manager/foreground/js/constants.js b/ui/file_manager/file_manager/foreground/js/constants.js
index 8612eeae..d70c1846 100644
--- a/ui/file_manager/file_manager/foreground/js/constants.js
+++ b/ui/file_manager/file_manager/foreground/js/constants.js
@@ -131,6 +131,7 @@
   ARCHIVE: 'archive',
   AUDIO: 'audio',
   BRUSCHETTA: 'bruschetta',
+  BULK_PINNING_BATTERY_SAVER: 'bulk_pinning_battery_saver',
   BULK_PINNING_DONE: 'bulk_pinning_done',
   BULK_PINNING_OFFLINE: 'bulk_pinning_offline',
   CAMERA_FOLDER: 'camera-folder',
diff --git a/ui/file_manager/file_manager/foreground/js/toolbar_controller.js b/ui/file_manager/file_manager/foreground/js/toolbar_controller.js
index c2a1d50..fdcfdca 100644
--- a/ui/file_manager/file_manager/foreground/js/toolbar_controller.js
+++ b/ui/file_manager/file_manager/foreground/js/toolbar_controller.js
@@ -575,7 +575,9 @@
   updateBulkPinningFolderIndicator_(currentDirectory, stage) {
     if (currentDirectory?.rootType === VolumeManagerCommon.RootType.DRIVE &&
         (stage === chrome.fileManagerPrivate.BulkPinStage.SYNCING ||
-         stage === chrome.fileManagerPrivate.BulkPinStage.PAUSED)) {
+         stage === chrome.fileManagerPrivate.BulkPinStage.PAUSED_OFFLINE ||
+         stage ===
+             chrome.fileManagerPrivate.BulkPinStage.PAUSED_BATTERY_SAVER)) {
       this.cloudOfflineFolderIndicator_.hidden = false;
       return;
     }
@@ -605,12 +607,18 @@
         this.cloudStatusIcon_.setAttribute(
             'type', constants.ICON_TYPES.CLOUD_ERROR);
         break;
-      case chrome.fileManagerPrivate.BulkPinStage.PAUSED:
+      case chrome.fileManagerPrivate.BulkPinStage.PAUSED_OFFLINE:
         this.cloudButtonIcon_.setAttribute(
             'type', constants.ICON_TYPES.BULK_PINNING_OFFLINE);
         this.cloudStatusIcon_.removeAttribute('type');
         this.cloudStatusIcon_.removeAttribute('size');
         break;
+      case chrome.fileManagerPrivate.BulkPinStage.PAUSED_BATTERY_SAVER:
+        this.cloudButtonIcon_.setAttribute(
+            'type', constants.ICON_TYPES.BULK_PINNING_BATTERY_SAVER);
+        this.cloudStatusIcon_.removeAttribute('type');
+        this.cloudStatusIcon_.removeAttribute('size');
+        break;
       default:
         this.cloudButtonIcon_.setAttribute('type', constants.ICON_TYPES.CLOUD);
         this.cloudStatusIcon_.removeAttribute('type');
diff --git a/ui/file_manager/file_manager/widgets/xf_bulk_pinning_dialog.ts b/ui/file_manager/file_manager/widgets/xf_bulk_pinning_dialog.ts
index bd3d402..7ce5f1a 100644
--- a/ui/file_manager/file_manager/widgets/xf_bulk_pinning_dialog.ts
+++ b/ui/file_manager/file_manager/widgets/xf_bulk_pinning_dialog.ts
@@ -20,6 +20,9 @@
   // Currently offline. Cannot compute space requirement for the time being.
   OFFLINE,
 
+  // Currently not running due to battery saver mode active.
+  BATTERY_SAVER,
+
   // Listing files and computing space requirements.
   LISTING,
 
@@ -44,6 +47,7 @@
   @query('cr-dialog') private $dialog_!: CrDialogElement;
   @query('#continue-button') private $button_!: CrButtonElement;
   @query('#offline-footer') private $offlineFooter_!: HTMLElement;
+  @query('#battery-saver-footer') private $batterySaverFooter_!: HTMLElement;
   @query('#listing-footer') private $listingFooter_!: HTMLElement;
   @query('#error-footer') private $errorFooter_!: HTMLElement;
   @query('#not-enough-space-footer')
@@ -85,10 +89,14 @@
 
     this.stage_ = bpp.stage;
     switch (bpp.stage) {
-      case BulkPinStage.PAUSED:
+      case BulkPinStage.PAUSED_OFFLINE:
         this.state = DialogState.OFFLINE;
         break;
 
+      case BulkPinStage.PAUSED_BATTERY_SAVER:
+        this.state = DialogState.BATTERY_SAVER;
+        break;
+
       case BulkPinStage.GETTING_FREE_SPACE:
       case BulkPinStage.LISTING_FILES:
         this.state = DialogState.LISTING;
@@ -119,6 +127,8 @@
   set state(s: DialogState) {
     this.$offlineFooter_.style.display =
         s === DialogState.OFFLINE ? 'initial' : 'none';
+    this.$batterySaverFooter_.style.display =
+        s === DialogState.BATTERY_SAVER ? 'initial' : 'none';
     this.$listingFooter_.style.display =
         s === DialogState.LISTING ? 'flex' : 'none';
     this.$errorFooter_.style.display =
@@ -196,6 +206,9 @@
           <div id="offline-footer" class="offline-footer">
             ${str('BULK_PINNING_OFFLINE')}
           </div>
+          <div id="battery-saver-footer" class="battery-saver-footer">
+            ${str('BULK_PINNING_BATTERY_SAVER')}
+          </div>
           <div id="listing-footer" class="normal-footer">
             <files-spinner></files-spinner>
             ${str('BULK_PINNING_LISTING')}
@@ -304,7 +317,7 @@
         padding: 16px;
       }
 
-      .offline-footer {
+      .offline-footer, .battery-saver-footer {
         background-color: var(--cros-sys-surface_variant);
         border: 1px solid var(--cros-separator-color);
         border-radius: 0 0 12px 12px;
diff --git a/ui/file_manager/file_manager/widgets/xf_cloud_panel.ts b/ui/file_manager/file_manager/widgets/xf_cloud_panel.ts
index 1881643..018b2f0 100644
--- a/ui/file_manager/file_manager/widgets/xf_cloud_panel.ts
+++ b/ui/file_manager/file_manager/widgets/xf_cloud_panel.ts
@@ -21,6 +21,7 @@
  */
 export enum CloudPanelType {
   OFFLINE = 'offline',
+  BATTERY_SAVER = 'battery_saver',
   NOT_ENOUGH_SPACE = 'not_enough_space',
 }
 
@@ -28,8 +29,8 @@
  * The `<xf-cloud-panel>` represents the current state that the Drive bulk
  * pinning process is currently in. When files are being pinned and downloaded,
  * the `items` and `progress` attributes are used to signify that the panel is
- * in progress. The `type` attribute can be used with `not_enough_space` and
- * `offline` to signify possible error or paused states.
+ * in progress. The `type` attribute can be used with `not_enough_space`,
+ * `offline`, and `battery_saver` to signify possible error or paused states.
  */
 @customElement('xf-cloud-panel')
 export class XfCloudPanel extends XfBase {
@@ -205,6 +206,14 @@
             ${str('DRIVE_BULK_PINNING_OFFLINE')}
           </div>
         </div>
+        <div class="static" id="progress-battery-saver">
+        <xf-icon type="${
+        constants.ICON_TYPES
+            .BULK_PINNING_BATTERY_SAVER}" size="large"></xf-icon>
+          <div class="status-description">
+            ${str('DRIVE_BULK_PINNING_BATTERY_SAVER')}
+          </div>
+        </div>
         <div class="static" id="progress-not-enough-space">
         <xf-icon type="${
         constants.ICON_TYPES.ERROR_BANNER}" size="large"></xf-icon>
@@ -254,6 +263,10 @@
       display: none;
     }
 
+    :host(:not([type="battery_saver"])) #progress-battery-saver {
+      display: none;
+    }
+
     :host(:not([type="not_enough_space"])) #progress-not-enough-space {
       display: none;
     }
@@ -284,6 +297,10 @@
       --xf-icon-color: var(--cros-sys-secondary);
     }
 
+    xf-icon[type="bulk_pinning_battery_saver"] {
+      --xf-icon-color: var(--cros-sys-secondary);
+    }
+
     xf-icon[type="error_banner"] {
       --xf-icon-color: var(--cros-sys-error);
     }
diff --git a/ui/file_manager/file_manager/widgets/xf_icon.ts b/ui/file_manager/file_manager/widgets/xf_icon.ts
index f7bf728..76470b5 100644
--- a/ui/file_manager/file_manager/widgets/xf_icon.ts
+++ b/ui/file_manager/file_manager/widgets/xf_icon.ts
@@ -39,8 +39,8 @@
 
   static get multiColor() {
     return {
-      [constants.ICON_TYPES.OFFLINE]:
-          svg`<use xlink:href="foreground/images/files/ui/offline.svg#offline"></use>`,
+      [constants.ICON_TYPES.CANT_PIN]:
+          svg`<use xlink:href="foreground/images/files/ui/cant_pin.svg#cant_pin"></use>`,
       [constants.ICON_TYPES.CLOUD_DONE]:
           svg`<use xlink:href="foreground/images/files/ui/cloud_done.svg#cloud_done"></use>`,
       [constants.ICON_TYPES.CLOUD_ERROR]:
@@ -55,6 +55,8 @@
           svg`<use xlink:href="foreground/images/files/ui/encrypted.svg#encrypted"></use>`,
       [constants.ICON_TYPES.ERROR]:
           svg`<use xlink:href="foreground/images/files/ui/error.svg#error"></use>`,
+      [constants.ICON_TYPES.OFFLINE]:
+          svg`<use xlink:href="foreground/images/files/ui/offline.svg#offline"></use>`,
     };
   }
 
@@ -194,10 +196,6 @@
       -webkit-mask-image: url(../foreground/images/volumes/camera.svg);
     }
 
-    :host([type="cant-pin"]) span {
-      -webkit-mask-image: url(../foreground/images/files/ui/cant_pin.svg);
-    }
-
     :host([type="computer"]) span {
       -webkit-mask-image: url(../foreground/images/volumes/computer.svg);
     }
@@ -376,6 +374,10 @@
       -webkit-mask-image: url(../foreground/images/files/ui/check.svg);
     }
 
+    :host([type="bulk_pinning_battery_saver"]) span {
+      -webkit-mask-image: url(../foreground/images/files/ui/bulk_pinning_battery_saver.svg);
+    }
+
     :host([type="bulk_pinning_done"]) span {
       -webkit-mask-image: url(../foreground/images/files/ui/bulk_pinning_done.svg);
     }
diff --git a/ui/file_manager/integration_tests/file_manager/toolbar.js b/ui/file_manager/integration_tests/file_manager/toolbar.js
index ac52847e..d490bfb 100644
--- a/ui/file_manager/integration_tests/file_manager/toolbar.js
+++ b/ui/file_manager/integration_tests/file_manager/toolbar.js
@@ -586,7 +586,7 @@
   await sendTestMessage({name: 'forceBulkPinningCalculateRequiredSpace'});
 
   // Assert the stage is `PAUSED` and the cloud button is still hidden.
-  await remoteCall.waitForBulkPinningStage('Paused');
+  await remoteCall.waitForBulkPinningStage('PausedOffline');
   await remoteCall.waitForElement(appId, '#cloud-button[hidden]');
   await remoteCall.waitForElement(appId, '#offline-folder-indicator[hidden]');
 };
@@ -606,12 +606,13 @@
   await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: true});
 
   // Set the bulk pinning manager to enter offline mode. This will surface a
-  // `PAUSED` state which has a UI representation iff the pref is enabled.
+  // `PAUSED_OFFLINE` state which has a UI representation iff the pref is
+  // enabled.
   await sendTestMessage({name: 'setBulkPinningOnline', enabled: false});
 
-  // Assert the stage is `PAUSED`, the cloud button is visible and the icon is
-  // the offline icon.
-  await remoteCall.waitForBulkPinningStage('Paused');
+  // Assert the stage is `PAUSED_OFFLINE`, the cloud button is visible and the
+  // icon is the offline icon.
+  await remoteCall.waitForBulkPinningStage('PausedOffline');
   await remoteCall.waitForElement(appId, '#cloud-button:not([hidden])');
   await remoteCall.waitForElement(
       appId, '#offline-folder-indicator:not([hidden])');
diff --git a/ui/views/accessibility/view_ax_platform_node_delegate_win.cc b/ui/views/accessibility/view_ax_platform_node_delegate_win.cc
index e08110e..ed1ce78 100644
--- a/ui/views/accessibility/view_ax_platform_node_delegate_win.cc
+++ b/ui/views/accessibility/view_ax_platform_node_delegate_win.cc
@@ -14,7 +14,6 @@
 #include "base/strings/utf_string_conversions.h"
 #include "base/win/windows_version.h"
 #include "third_party/iaccessible2/ia2_api_all.h"
-#include "ui/accessibility/accessibility_switches.h"
 #include "ui/accessibility/ax_node_data.h"
 #include "ui/accessibility/ax_text_utils.h"
 #include "ui/accessibility/platform/ax_fragment_root_win.h"
diff --git a/ui/views/controls/textfield/textfield.cc b/ui/views/controls/textfield/textfield.cc
index 2166ed4..7323629 100644
--- a/ui/views/controls/textfield/textfield.cc
+++ b/ui/views/controls/textfield/textfield.cc
@@ -773,21 +773,18 @@
     case ui::ET_GESTURE_TAP: {
       RequestFocusForGesture(event->details());
       if (controller_ && controller_->HandleGestureEvent(this, *event)) {
-        selection_dragging_state_ = SelectionDraggingState::kNone;
+        StopSelectionDragging();
         event->SetHandled();
         return;
       }
-
+      if (HandleGestureForSelectionDragging(event)) {
+        return;
+      }
       const size_t tap_pos =
           GetRenderText()->FindCursorPosition(event->location()).caret_pos();
       const bool should_toggle_menu = event->details().tap_count() == 1 &&
                                       GetSelectedRange() == gfx::Range(tap_pos);
-      if (selection_dragging_state_ != SelectionDraggingState::kNone) {
-        // Selection has already been set in the preceding ET_GESTURE_TAP_DOWN
-        // event, so handles should be shown without changing the selection.
-        // Just need to cancel selection dragging.
-        selection_dragging_state_ = SelectionDraggingState::kNone;
-      } else if (event->details().tap_count() == 1) {
+      if (event->details().tap_count() == 1) {
         // If tap is on the selection and touch handles are not present,
         // handles should be shown without changing selection. Otherwise,
         // cursor should be moved to the tap location.
@@ -814,52 +811,45 @@
       break;
     }
     case ui::ET_GESTURE_TAP_DOWN: {
-      if (::features::IsTouchTextEditingRedesignEnabled() && HasFocus()) {
-        if (event->details().tap_down_count() == 2) {
-          OnBeforeUserAction();
-          SelectWordAt(event->location());
-          OnAfterUserAction();
-        } else if (event->details().tap_down_count() == 3) {
-          OnBeforeUserAction();
-          SelectAll(false);
-          OnAfterUserAction();
-        } else {
-          break;
+      if (HasFocus()) {
+        if (HandleGestureForSelectionDragging(event)) {
+          return;
         }
-        DestroyTouchSelection();
-        selection_dragging_state_ =
-            SelectionDraggingState::kDraggingSelectionExtent;
-        event->SetHandled();
       }
       break;
     }
     case ui::ET_GESTURE_LONG_PRESS:
-      if (!GetRenderText()->IsPointInSelection(event->location())) {
+      if (GetRenderText()->IsPointInSelection(event->location())) {
+        // If long-press happens on the selection, deactivate touch selection
+        // and try to initiate drag-drop. If drag-drop is not enabled, context
+        // menu will be shown. Event is not marked as handled to let Views
+        // handle drag-drop or context menu.
+        DestroyTouchSelection();
+        StopSelectionDragging();
+        initiating_drag_ = switches::IsTouchDragDropEnabled();
+        break;
+      } else {
         // If long-press happens outside selection, select word and try to
         // activate touch selection.
         OnBeforeUserAction();
         SelectWordAt(event->location());
         OnAfterUserAction();
         CreateTouchSelectionControllerAndNotifyIt();
-        if (::features::IsTouchTextEditingRedesignEnabled()) {
-          selection_dragging_state_ =
-              SelectionDraggingState::kDraggingSelectionExtent;
+
+        if (HandleGestureForSelectionDragging(event)) {
+          return;
         }
-        // If touch selection activated successfully, mark event as handled
-        // so that the regular context menu is not shown.
-        if (touch_selection_controller_)
+
+        // If touch selection is activated, mark the event as handled so that
+        // the regular context menu is not shown.
+        if (touch_selection_controller_) {
           event->SetHandled();
-      } else {
-        // If long-press happens on the selection, deactivate touch
-        // selection and try to initiate drag-drop. If drag-drop is not
-        // enabled, context menu will be shown. Event is not marked as
-        // handled to let Views handle drag-drop or context menu.
-        DestroyTouchSelection();
-        initiating_drag_ = switches::IsTouchDragDropEnabled();
+          return;
+        }
       }
       break;
     case ui::ET_GESTURE_LONG_TAP:
-      selection_dragging_state_ = SelectionDraggingState::kNone;
+      StopSelectionDragging();
       // If touch selection is enabled, the context menu on long tap will be
       // shown by the |touch_selection_controller_|, hence we mark the event
       // handled so Views does not try to show context menu on it.
@@ -868,63 +858,28 @@
       break;
     case ui::ET_GESTURE_SCROLL_BEGIN:
       if (HasFocus()) {
-        if (::features::IsTouchTextEditingRedesignEnabled()) {
-          MaybeStartSelectionDragging(event);
+        if (HandleGestureForSelectionDragging(event)) {
+          return;
         }
-        if (selection_dragging_state_ == SelectionDraggingState::kNone) {
-          drag_start_location_x_ = event->location().x();
-          drag_start_display_offset_ =
-              GetRenderText()->GetUpdatedDisplayOffset().x();
-          show_touch_handles_after_scroll_ =
-              touch_selection_controller_ != nullptr;
-          // Deactivate touch selection controller when scrolling.
-          DestroyTouchSelection();
-        } else {
-          // Create touch selection controller to show a magnifier when
-          // selection dragging.
-          CreateTouchSelectionControllerAndNotifyIt();
-          show_touch_handles_after_scroll_ = true;
-        }
+        OnGestureScrollBegin(event->location().x());
         event->SetHandled();
       }
       break;
     case ui::ET_GESTURE_SCROLL_UPDATE:
       if (HasFocus()) {
-        // Switch from selection dragging to default scrolling behaviour if
-        // scroll update has multiple touch points.
-        if (selection_dragging_state_ != SelectionDraggingState::kNone &&
-            event->details().touch_points() > 1) {
-          selection_dragging_state_ = SelectionDraggingState::kNone;
-          drag_start_location_x_ = event->location().x();
-          drag_start_display_offset_ =
-              GetRenderText()->GetUpdatedDisplayOffset().x();
-          DestroyTouchSelection();
-          show_touch_handles_after_scroll_ = true;
+        if (HandleGestureForSelectionDragging(event)) {
+          return;
         }
-        switch (selection_dragging_state_) {
-          case SelectionDraggingState::kDraggingSelectionExtent:
-            MoveRangeSelectionExtent(event->location() +
-                                     selection_dragging_offset_);
-            break;
-          case SelectionDraggingState::kDraggingCursor:
-            MoveCursorTo(event->location(), false);
-            break;
-          case SelectionDraggingState::kNone: {
-            int new_display_offset = drag_start_display_offset_ +
-                                     event->location().x() -
-                                     drag_start_location_x_;
-            GetRenderText()->SetDisplayOffset(new_display_offset);
-            SchedulePaint();
-            break;
-          }
-        }
+        GestureScroll(event->location().x());
         event->SetHandled();
       }
       break;
     case ui::ET_GESTURE_SCROLL_END:
     case ui::ET_SCROLL_FLING_START:
+      if (HandleGestureForSelectionDragging(event)) {
+        return;
+      }
       if (HasFocus()) {
-        selection_dragging_state_ = SelectionDraggingState::kNone;
         if (show_touch_handles_after_scroll_) {
           CreateTouchSelectionControllerAndNotifyIt();
           show_touch_handles_after_scroll_ = false;
@@ -933,7 +888,9 @@
       }
       break;
     case ui::ET_GESTURE_END:
-      selection_dragging_state_ = SelectionDraggingState::kNone;
+      if (HandleGestureForSelectionDragging(event)) {
+        return;
+      }
       break;
     default:
       return;
@@ -2961,18 +2918,108 @@
       ShapeContextTokens::kTextfieldRadius, size());
 }
 
-void Textfield::MaybeStartSelectionDragging(ui::GestureEvent* event) {
-  DCHECK_EQ(event->type(), ui::ET_GESTURE_SCROLL_BEGIN);
-  // Only start selection dragging if scrolling with one touch point.
-  if (event->details().touch_points() > 1) {
-    selection_dragging_state_ = SelectionDraggingState::kNone;
-    return;
+void Textfield::OnGestureScrollBegin(int drag_start_location_x) {
+  drag_start_location_x_ = drag_start_location_x;
+  drag_start_display_offset_ = GetRenderText()->GetUpdatedDisplayOffset().x();
+  show_touch_handles_after_scroll_ = touch_selection_controller_ != nullptr;
+  DestroyTouchSelection();
+}
+
+void Textfield::GestureScroll(int drag_location_x) {
+  int new_display_offset =
+      drag_start_display_offset_ + drag_location_x - drag_start_location_x_;
+  GetRenderText()->SetDisplayOffset(new_display_offset);
+  SchedulePaint();
+}
+
+bool Textfield::HandleGestureForSelectionDragging(ui::GestureEvent* event) {
+  if (!::features::IsTouchTextEditingRedesignEnabled()) {
+    return false;
   }
 
-  const float delta_x = event->details().scroll_x_hint();
-  const float delta_y = event->details().scroll_y_hint();
-  if (selection_dragging_state_ ==
-      SelectionDraggingState::kDraggingSelectionExtent) {
+  switch (event->type()) {
+    case ui::ET_GESTURE_TAP:
+      if (selection_dragging_state_ != SelectionDraggingState::kNone) {
+        // Selection has already been set in preceding events, so we can just
+        // cancel selection dragging and show touch handles without changing the
+        // selection.
+        StopSelectionDragging();
+        CreateTouchSelectionControllerAndNotifyIt();
+        event->SetHandled();
+        return true;
+      }
+      return false;
+    case ui::ET_GESTURE_TAP_DOWN:
+      if (event->details().tap_down_count() == 1) {
+        selection_dragging_state_ = SelectionDraggingState::kNone;
+        return false;
+      } else if (event->details().tap_down_count() == 2) {
+        OnBeforeUserAction();
+        SelectWordAt(event->location());
+        OnAfterUserAction();
+        selection_dragging_state_ = SelectionDraggingState::kSelectedWord;
+      } else if (event->details().tap_down_count() == 3) {
+        OnBeforeUserAction();
+        SelectAll(false);
+        OnAfterUserAction();
+        selection_dragging_state_ = SelectionDraggingState::kSelectedAll;
+      }
+      DestroyTouchSelection();
+      event->SetHandled();
+      return true;
+    case ui::ET_GESTURE_LONG_PRESS:
+      selection_dragging_state_ = SelectionDraggingState::kSelectedWord;
+      event->SetHandled();
+      return true;
+    case ui::ET_GESTURE_SCROLL_BEGIN:
+      // Only start selection dragging if scrolling with one touch point.
+      if (event->details().touch_points() == 1 &&
+          StartSelectionDragging(*event)) {
+        CreateTouchSelectionControllerAndNotifyIt();
+        show_touch_handles_after_scroll_ = true;
+        event->SetHandled();
+        return true;
+      }
+      StopSelectionDragging();
+      return false;
+    case ui::ET_GESTURE_SCROLL_UPDATE:
+      // Switch from selection dragging to default scrolling behaviour if scroll
+      // update has multiple touch points.
+      if (IsSelectionDragging() && event->details().touch_points() > 1) {
+        StopSelectionDragging();
+        OnGestureScrollBegin(event->location().x());
+        return false;
+      } else if (selection_dragging_state_ ==
+                 SelectionDraggingState::kDraggingSelectionExtent) {
+        MoveRangeSelectionExtent(event->location() +
+                                 selection_dragging_offset_);
+        event->SetHandled();
+        return true;
+      } else if (selection_dragging_state_ ==
+                 SelectionDraggingState::kDraggingCursor) {
+        MoveCursorTo(event->location(), false);
+        event->SetHandled();
+        return true;
+      }
+      return false;
+    case ui::ET_GESTURE_SCROLL_END:
+    case ui::ET_SCROLL_FLING_START:
+      StopSelectionDragging();
+      return false;
+    case ui::ET_GESTURE_END:
+      StopSelectionDragging();
+      return false;
+    default:
+      return false;
+  }
+}
+
+bool Textfield::StartSelectionDragging(const ui::GestureEvent& event) {
+  DCHECK_EQ(event.type(), ui::ET_GESTURE_SCROLL_BEGIN);
+
+  const float delta_x = event.details().scroll_x_hint();
+  const float delta_y = event.details().scroll_y_hint();
+  if (selection_dragging_state_ == SelectionDraggingState::kSelectedWord) {
     gfx::RenderText* render_text = GetRenderText();
     gfx::SelectionModel start_sel =
         render_text->GetSelectionModelForSelectionStart();
@@ -2984,12 +3031,12 @@
 
     gfx::LogicalCursorDirection drag_direction = gfx::CURSOR_FORWARD;
     if (std::fabs(delta_y) > std::fabs(delta_x)) {
-      // If the initial dragging motion is up/down, extend the
-      // selection backwards/forwards.
+      // If the initial dragging motion is up/down, extend the selection
+      // backwards/forwards.
       drag_direction = delta_y < 0 ? gfx::CURSOR_BACKWARD : gfx::CURSOR_FORWARD;
     } else {
-      // Otherwise, extend the selection in the direction of
-      // horizontal movement.
+      // Otherwise, extend the selection in the direction of horizontal
+      // movement.
       drag_direction = delta_x * (selection_end.x() - selection_start.x()) < 0
                            ? gfx::CURSOR_BACKWARD
                            : gfx::CURSOR_FORWARD;
@@ -3000,13 +3047,23 @@
     gfx::Point extent =
         drag_direction == gfx::CURSOR_FORWARD ? selection_end : selection_start;
     SelectBetweenCoordinates(base, extent);
-    selection_dragging_offset_ = extent - event->location();
-  } else if (std::fabs(delta_x) >= std::fabs(delta_y)) {
-    // Use the scroll sequence for cursor placement if it begins in a
-    // horizontal direction and is not already being used for dragging
-    // the selection.
+
+    selection_dragging_offset_ = extent - event.location();
+    selection_dragging_state_ =
+        SelectionDraggingState::kDraggingSelectionExtent;
+    return true;
+  } else if (selection_dragging_state_ == SelectionDraggingState::kNone &&
+             std::fabs(delta_x) >= std::fabs(delta_y)) {
+    // Only start dragging the cursor if the gesture begins in a horizontal
+    // direction.
     selection_dragging_state_ = SelectionDraggingState::kDraggingCursor;
+    return true;
   }
+  return false;
+}
+
+void Textfield::StopSelectionDragging() {
+  selection_dragging_state_ = SelectionDraggingState::kNone;
 }
 
 BEGIN_METADATA(Textfield, View)
diff --git a/ui/views/controls/textfield/textfield.h b/ui/views/controls/textfield/textfield.h
index e87e208..b486a86 100644
--- a/ui/views/controls/textfield/textfield.h
+++ b/ui/views/controls/textfield/textfield.h
@@ -668,12 +668,23 @@
   // Returns the corner radius of the text field.
   float GetCornerRadius();
 
-  // Checks and updates the selection dragging state for the upcoming scroll
-  // sequence, if required. If the scroll sequence starts while long pressing,
-  // it will be used for adjusting the text selection. Otherwise, if the scroll
-  // begins horizontally it will be used for cursor placement. Otherwise, the
-  // scroll sequence won't be used for selection dragging.
-  void MaybeStartSelectionDragging(ui::GestureEvent* event);
+  // Prepares the Textfield for gesture scrolling by setting the drag start
+  // state.
+  void OnGestureScrollBegin(int drag_start_location_x);
+
+  // Performs gesture scrolling.
+  void GestureScroll(int drag_location_x);
+
+  // Performs gesture handling needed for touch selection dragging. Sets `event`
+  // as handled and returns true if the event should not be processed further.
+  bool HandleGestureForSelectionDragging(ui::GestureEvent* event);
+
+  // Determines whether touch selection dragging should start and updates the
+  // selection dragging state if needed. Returns true if selection dragging
+  // starts.
+  bool StartSelectionDragging(const ui::GestureEvent& event);
+
+  void StopSelectionDragging();
 
   // The text model.
   std::unique_ptr<TextfieldModel> model_;
@@ -761,10 +772,13 @@
 
   SelectionController selection_controller_;
 
-  // Tracks when the current scroll sequence should be used for cursor placement
+  // Tracks the touch selection dragging state which is used when determining
+  // whether a dragging movement should be used for scrolling, cursor placement
   // or adjusting the text selection.
   enum class SelectionDraggingState {
     kNone,
+    kSelectedAll,
+    kSelectedWord,
     kDraggingCursor,
     kDraggingSelectionExtent
   };