diff --git a/AUTHORS b/AUTHORS
index 7a324d6..80e149f 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1449,6 +1449,7 @@
 Suyambulingam R M <suyambu.rm@samsung.com>
 Suyash Nayan <suyashnyn1@gmail.com>
 Suyash Sengar <suyash.s@samsung.com>
+Suyeon Ji <zeesuyeon@gmail.com>
 Swarali Raut <swarali.sr@samsung.com>
 Swati Jaiswal <swa.jaiswal@samsung.com>
 Syed Wajid <syed.wajid@samsung.com>
diff --git a/DEPS b/DEPS
index 7bdc90e..e7df693 100644
--- a/DEPS
+++ b/DEPS
@@ -299,7 +299,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
-  'skia_revision': 'dc1958e7118c351cef5adae548337f7f11746b95',
+  'skia_revision': 'f8cd9fe75f21d3be759cbf9491ddc582efcf1e2a',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
@@ -307,7 +307,7 @@
   # 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': 'c53c908c41a10167fabdf4f49c98b9d610d43216',
+  'angle_revision': '79ec8b3400ceeafc3e69b9bec29fa39a0e1a9a16',
   # 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': 'd7cb23eac0fda3c06441034ccea0c4bb9c947add',
+  'devtools_frontend_revision': 'fd0c28cbf06b695f677c525b6630f9f9e4a90eee',
   # 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.
@@ -415,7 +415,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': '9635124871e8ab6d38d416e78759a447e68c35e5',
+  'dawn_revision': '223318d090e36d901f7c7a83b764e0a336b19385',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -519,7 +519,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling llvm-libc
   # and whatever else without interference from each other.
-  'llvm_libc_revision':    '1f64d055dbb79dfcef1a63588341d171f327cd73',
+  'llvm_libc_revision':    'ff75100b45027c70e17770cb80e720193b742c76',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling llvm-libc
   # and whatever else without interference from each other.
@@ -1521,7 +1521,7 @@
 
   'src/clank': {
     'url': Var('chrome_git') + '/clank/internal/apps.git' + '@' +
-    'b62d38ffc9bd9bfc6011eabe6143a4a40e45bd9a',
+    '4e7cdb1917bb21338750038cdc8389b643f96cd5',
     'condition': 'checkout_android and checkout_src_internal',
   },
 
@@ -1680,7 +1680,7 @@
     'packages': [
       {
           'package': 'chromium/third_party/androidx',
-          'version': 'I9DcrpteFBl2d2LwjrGGN07Kcsom-8r_VGyqAfakS2cC',
+          'version': 'Qnm0rlf-dVJZJzD3cigXJErvHBVBFNFvsTmMgSPAosUC',
       },
     ],
     'condition': 'checkout_android and non_git_source',
@@ -1773,7 +1773,7 @@
       'packages': [
           {
                'package': 'chromium/third_party/android_build_tools/lint',
-               'version': 'Otd2S_y5ozK3q8Q7eMr1NqNH38ESBnUYT4I842UsU0UC',
+               'version': '3bWjs4NjBtTIXoWH03nPx8c--ehZzlDkL8PUE_GaPKUC',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -1784,7 +1784,7 @@
       'packages': [
           {
                'package': 'chromium/third_party/android_build_tools/manifest_merger',
-               'version': 'UrgRDTQRxa2KqkIGo6gwYOY7uf56hYmH-QAjov2N9NMC',
+               'version': 'AFWUMAcwcd0L1DG2-ib4ghtQYFsCvZjWuQkhuTJl4ToC',
           },
       ],
       'condition': 'checkout_android and non_git_source',
@@ -2559,7 +2559,7 @@
     Var('pdfium_git') + '/pdfium.git' + '@' +  Var('pdfium_revision'),
 
   'src/third_party/perfetto':
-    Var('chromium_git') + '/external/github.com/google/perfetto.git' + '@' + '3231268f2960b13cb52788389e88d7932117f57e',
+    Var('chromium_git') + '/external/github.com/google/perfetto.git' + '@' + 'd7984ebb216b56e0eb99ba29adfae91f37819602',
 
   'src/base/tracing/test/data': {
     'bucket': 'perfetto',
@@ -2878,16 +2878,16 @@
       'dep_type': 'cipd',
   },
 
-  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@222889ea039509b86fa6dd90fcb6acf47e79cdb9',
+  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@5f9183df4e99d153dcf2de173ddfd47034f72601',
   'src/third_party/glslang/src': '{chromium_git}/external/github.com/KhronosGroup/glslang@05cfcc1613c28c1274036f53616d66324f7cd383',
   'src/third_party/spirv-cross/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Cross@b8fcf307f1f347089e3c46eb4451d27f32ebc8d3',
   'src/third_party/spirv-headers/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Headers@50daff941d88609b4d2ad076eae558e727f8e5cd',
   'src/third_party/spirv-tools/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Tools@e0bad2825dacf274578ec6d3c0e64e406d5e4fd7',
-  'src/third_party/vulkan-headers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Headers@1d6c53f65443ceeb97d3bdc695aaecc7ea6cc441',
-  'src/third_party/vulkan-loader/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Loader@63b05d2b087952662623de7eddd44f2e57d71a1e',
-  'src/third_party/vulkan-tools/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Tools@fbe722654b7173da961398cf78bd4a62d1839b65',
-  'src/third_party/vulkan-utility-libraries/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Utility-Libraries@e48ae20a7938b01aee62806bfcdafe8a0883b1e4',
-  'src/third_party/vulkan-validation-layers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-ValidationLayers@6d6e6ec8e51cd219498359cbc48e4762d1a80616',
+  'src/third_party/vulkan-headers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Headers@b39ab380a44b6c8df462c34e976ea9ce2d2c336b',
+  'src/third_party/vulkan-loader/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Loader@22c0f133e6675f9313c12fb5e58337f8fa9b9195',
+  'src/third_party/vulkan-tools/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Tools@d671923090e4dc74c0ebdb10c6e09fa0826e1fe9',
+  'src/third_party/vulkan-utility-libraries/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Utility-Libraries@54c9baf20802a13279e23fa4cb0528dd5cf16064',
+  'src/third_party/vulkan-validation-layers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-ValidationLayers@4b8f3caab2cf0ace618ba857364346aed6297d29',
 
   'src/third_party/vulkan_memory_allocator':
     Var('chromium_git') + '/external/github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git' + '@' + '56300b29fbfcc693ee6609ddad3fdd5b7a449a21',
@@ -2932,7 +2932,7 @@
     Var('chromium_git') + '/webpagereplay.git' + '@' + Var('webpagereplay_revision'),
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + 'cf714279e2a0d54e03b4065f14748dea1dc24606',
+    Var('webrtc_git') + '/src.git' + '@' + '70347d915e9b3d74cfbcc1323f35edc834f097e0',
 
   # Wuffs' canonical repository is at github.com/google/wuffs, but we use
   # Skia's mirror of Wuffs, the same as in upstream Skia's DEPS file.
diff --git a/ash/constants/ash_features.cc b/ash/constants/ash_features.cc
index b293c89e..a3d8bd6 100644
--- a/ash/constants/ash_features.cc
+++ b/ash/constants/ash_features.cc
@@ -314,7 +314,7 @@
 // client for Spotlight within the Boca SWA.
 BASE_FEATURE(kBocaSpotlightRobotRequester,
              "BocaSpotlightRobotRequester",
-             base::FEATURE_DISABLED_BY_DEFAULT);
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 // Enables or disables enforcing sequential execution for Boca insert activity.
 BASE_FEATURE(kBocaSequentialInsertActivity,
diff --git a/ash/webui/boca_ui/boca_app_page_handler_unittest.cc b/ash/webui/boca_ui/boca_app_page_handler_unittest.cc
index acf3da2..9824365 100644
--- a/ash/webui/boca_ui/boca_app_page_handler_unittest.cc
+++ b/ash/webui/boca_ui/boca_app_page_handler_unittest.cc
@@ -404,8 +404,11 @@
  public:
   BocaAppPageHandlerTest() = default;
   void SetUp() override {
-    scoped_feature_list_.InitWithFeatures({ash::features::kBoca},
-                                          /*disabled_features=*/{});
+    scoped_feature_list_.InitWithFeatures(
+        {ash::features::kBoca},
+        // TODO:crbug.com/424867979 - Re-enable feature flag after adding unit
+        // tests.
+        /*disabled_features=*/{ash::features::kBocaSpotlightRobotRequester});
     // Set up UserManager related modules.
     user_manager::UserManagerImpl::RegisterPrefs(local_state_.registry());
     ash::boca_util::RegisterPrefs(local_state_.registry());
diff --git a/base/test/android/javatests/src/org/chromium/base/test/transit/Transition.java b/base/test/android/javatests/src/org/chromium/base/test/transit/Transition.java
index 9459896..356671a 100644
--- a/base/test/android/javatests/src/org/chromium/base/test/transit/Transition.java
+++ b/base/test/android/javatests/src/org/chromium/base/test/transit/Transition.java
@@ -288,9 +288,9 @@
             }
 
             if (primary.mTries != null) {
-                builder.withTimeout(primary.mTries);
+                builder.withTries(primary.mTries);
             } else if (secondary.mTries != null) {
-                builder.withTimeout(secondary.mTries);
+                builder.withTries(secondary.mTries);
             }
 
             if (primary.mPossiblyAlreadyFulfilled != null) {
@@ -339,8 +339,7 @@
              * timeout.
              */
             public Builder withRetry() {
-                mTries = 2;
-                return this;
+                return withTries(2);
             }
 
             /**
@@ -348,7 +347,11 @@
              * finish within the timeout.
              */
             public Builder withNoRetry() {
-                mTries = 1;
+                return withTries(1);
+            }
+
+            private Builder withTries(int tries) {
+                mTries = tries;
                 return this;
             }
 
diff --git a/base/tracing/protos/chrome_track_event.proto b/base/tracing/protos/chrome_track_event.proto
index 4a48f56..759fd6f 100644
--- a/base/tracing/protos/chrome_track_event.proto
+++ b/base/tracing/protos/chrome_track_event.proto
@@ -1026,6 +1026,9 @@
 
     V8_USER_VISIBLE_TQ = 56;
     V8_BEST_EFFORT_TQ = 57;
+
+    NETWORK_SERVICE_THREAD_HIGH_TQ = 58;
+    NETWORK_SERVICE_THREAD_DEFAULT_TQ = 59;
   }
 
   optional Priority priority = 1;
diff --git a/cc/layers/texture_layer_impl_unittest.cc b/cc/layers/texture_layer_impl_unittest.cc
index eeefaf7f..db61203a 100644
--- a/cc/layers/texture_layer_impl_unittest.cc
+++ b/cc/layers/texture_layer_impl_unittest.cc
@@ -27,12 +27,9 @@
 
   LayerTreeImplTestBase impl;
 
-  auto resource = viz::TransferableResource::MakeGpu(
-      gpu::Mailbox::Generate(), GL_TEXTURE_2D,
-      gpu::SyncToken(gpu::CommandBufferNamespace::GPU_IO,
-                     gpu::CommandBufferId::FromUnsafeValue(0x234), 0x456),
-      layer_bounds, viz::SinglePlaneFormat::kRGBA_8888,
-      false /* is_overlay_candidate */);
+  auto resource = viz::TransferableResource::Make(
+      gpu::ClientSharedImage::CreateForTesting(),
+      viz::TransferableResource::ResourceSource::kTest, gpu::SyncToken());
 
   TextureLayerImpl* layer = impl.AddLayerInActiveTree<TextureLayerImpl>();
   layer->SetBounds(layer_bounds);
@@ -61,12 +58,9 @@
 
   LayerTreeImplTestBase impl;
 
-  auto resource = viz::TransferableResource::MakeGpu(
-      gpu::Mailbox::Generate(), GL_TEXTURE_2D,
-      gpu::SyncToken(gpu::CommandBufferNamespace::GPU_IO,
-                     gpu::CommandBufferId::FromUnsafeValue(0x234), 0x456),
-      layer_size, viz::SinglePlaneFormat::kRGBA_8888,
-      false /* is_overlay_candidate */);
+  auto resource = viz::TransferableResource::Make(
+      gpu::ClientSharedImage::CreateForTesting(),
+      viz::TransferableResource::ResourceSource::kTest, gpu::SyncToken());
 
   TextureLayerImpl* texture_layer_impl =
       impl.AddLayerInActiveTree<TextureLayerImpl>();
diff --git a/cc/layers/texture_layer_unittest.cc b/cc/layers/texture_layer_unittest.cc
index c94066f..82d26cd 100644
--- a/cc/layers/texture_layer_unittest.cc
+++ b/cc/layers/texture_layer_unittest.cc
@@ -77,33 +77,32 @@
 namespace cc {
 namespace {
 
-gpu::Mailbox MailboxFromChar(char value) {
-  gpu::Mailbox mailbox;
-  memset(mailbox.name, value, sizeof(mailbox.name));
-  return mailbox;
+// Compares SyncToken ignoring verified_flush() bit.
+MATCHER_P(SameSyncToken, other, "") {
+  gpu::SyncToken a = arg;
+  gpu::SyncToken b = other;
+  a.SetVerifyFlush();
+  b.SetVerifyFlush();
+  return a == b;
 }
 
-gpu::SyncToken SyncTokenFromUInt(uint32_t value) {
+gpu::SyncToken GenSyncToken() {
+  static int next_release = 1;
   return gpu::SyncToken(gpu::CommandBufferNamespace::GPU_IO,
-                        gpu::CommandBufferId::FromUnsafeValue(0x123), value);
+                        gpu::CommandBufferId::FromUnsafeValue(0x234),
+                        next_release++);
 }
 
-void AllocateSharedImage(gpu::SharedImageInterface* shared_image_interface,
-                         const gfx::Size& size,
-                         scoped_refptr<gpu::ClientSharedImage>& shared_image,
-                         gpu::SyncToken& sync_token) {
-  CHECK(shared_image_interface);
-  viz::SharedImageFormat format = viz::SinglePlaneFormat::kBGRA_8888;
-  shared_image = shared_image_interface->CreateSharedImageForSoftwareCompositor(
-      {format, size, gfx::ColorSpace(), gpu::SHARED_IMAGE_USAGE_CPU_WRITE_ONLY,
-       "TextureLayerTest"});
-  sync_token = shared_image_interface->GenVerifiedSyncToken();
+viz::TransferableResource MakeFakeResource() {
+  return viz::TransferableResource::Make(
+      gpu::ClientSharedImage::CreateForTesting(),
+      viz::TransferableResource::ResourceSource::kTest, GenSyncToken());
 }
 
-void DeleteSharedImage(scoped_refptr<gpu::ClientSharedImage> shared_image,
-                       const gpu::SyncToken& sync_token,
-                       bool is_lost) {
-  shared_image->UpdateDestructionSyncToken(sync_token);
+viz::TransferableResource MakeFakeSoftwareResource() {
+  return viz::TransferableResource::Make(
+      gpu::ClientSharedImage::CreateSoftwareForTesting(),
+      viz::TransferableResource::ResourceSource::kTest, GenSyncToken());
 }
 
 class MockLayerTreeHost : public LayerTreeHost {
@@ -137,73 +136,53 @@
 
 class MockReleaseCallback {
  public:
-  MOCK_METHOD3(Release,
-               void(const gpu::Mailbox& mailbox,
-                    const gpu::SyncToken& sync_token,
-                    bool lost_resource));
-  MOCK_METHOD3(ReleaseSW,
-               void(scoped_refptr<gpu::ClientSharedImage>,
-                    const gpu::SyncToken& token,
-                    bool lost));
+  MOCK_METHOD2(Release,
+               void(const gpu::SyncToken& sync_token, bool lost_resource));
 };
 
 struct CommonResourceObjects {
-  CommonResourceObjects()
-      : mailbox_name1_(MailboxFromChar('1')),
-        mailbox_name2_(MailboxFromChar('2')),
-        sync_token1_(gpu::CommandBufferNamespace::GPU_IO,
-                     gpu::CommandBufferId::FromUnsafeValue(0x234),
-                     1),
-        sync_token2_(gpu::CommandBufferNamespace::GPU_IO,
-                     gpu::CommandBufferId::FromUnsafeValue(0x234),
-                     2),
-        sync_token_sw_(gpu::CommandBufferNamespace::GPU_IO,
-                       gpu::CommandBufferId::FromUnsafeValue(0x235),
-                       1) {
-    release_callback1_ =
-        base::BindRepeating(&MockReleaseCallback::Release,
-                            base::Unretained(&mock_callback_), mailbox_name1_);
-    release_callback2_ =
-        base::BindRepeating(&MockReleaseCallback::Release,
-                            base::Unretained(&mock_callback_), mailbox_name2_);
-    const uint32_t arbitrary_target1 = GL_TEXTURE_2D;
-    const uint32_t arbitrary_target2 = GL_TEXTURE_EXTERNAL_OES;
-    gfx::Size size(128, 128);
-    resource1_ = viz::TransferableResource::MakeGpu(
-        mailbox_name1_, arbitrary_target1, sync_token1_, size,
-        viz::SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
-    resource2_ = viz::TransferableResource::MakeGpu(
-        mailbox_name2_, arbitrary_target2, sync_token2_, size,
-        viz::SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
+  explicit CommonResourceObjects(bool software) {
+    if (software) {
+      resource = MakeFakeSoftwareResource();
+    } else {
+      resource = MakeFakeResource();
+    }
 
-    // For software rendering
-    shared_image_sw_ = gpu::ClientSharedImage::CreateForTesting();
-    resource_sw_ = viz::TransferableResource::MakeSoftwareSharedImage(
-        shared_image_sw_, sync_token_sw_, size,
-        viz::SinglePlaneFormat::kBGRA_8888,
-        viz::TransferableResource::ResourceSource::kTest);
-    release_callback_sw_ = base::BindRepeating(
-        &MockReleaseCallback::ReleaseSW, base::Unretained(&mock_callback_),
-        shared_image_sw_);
+    creation_sync_token = resource.sync_token();
+
+    release_callback = base::BindRepeating(&MockReleaseCallback::Release,
+                                           base::Unretained(&mock_callback));
   }
 
+  CommonResourceObjects& ExpectReleaseWithSyncToken(
+      const gpu::SyncToken& sync_token,
+      bool lost) {
+    EXPECT_CALL(mock_callback, Release(sync_token, lost)).Times(1);
+    return *this;
+  }
+
+  CommonResourceObjects& ExpectRelease() {
+    EXPECT_CALL(mock_callback,
+                Release(SameSyncToken(creation_sync_token), false))
+        .Times(1);
+    return *this;
+  }
+
+  CommonResourceObjects& ExpectNoRelease() {
+    EXPECT_CALL(mock_callback, Release(_, _)).Times(0);
+    return *this;
+  }
+
+  void Verify() { Mock::VerifyAndClearExpectations(&mock_callback); }
+
   using RepeatingReleaseCallback =
       base::RepeatingCallback<void(const gpu::SyncToken& sync_token,
                                    bool is_lost)>;
 
-  gpu::Mailbox mailbox_name1_;
-  gpu::Mailbox mailbox_name2_;
-  scoped_refptr<gpu::ClientSharedImage> shared_image_sw_;
-  MockReleaseCallback mock_callback_;
-  RepeatingReleaseCallback release_callback1_;
-  RepeatingReleaseCallback release_callback2_;
-  RepeatingReleaseCallback release_callback_sw_;
-  gpu::SyncToken sync_token1_;
-  gpu::SyncToken sync_token2_;
-  gpu::SyncToken sync_token_sw_;
-  viz::TransferableResource resource1_;
-  viz::TransferableResource resource2_;
-  viz::TransferableResource resource_sw_;
+  RepeatingReleaseCallback release_callback;
+  viz::TransferableResource resource;
+  gpu::SyncToken creation_sync_token;
+  MockReleaseCallback mock_callback;
 };
 
 class TextureLayerTest : public testing::Test {
@@ -240,7 +219,9 @@
   TestTaskGraphRunner task_graph_runner_;
   std::unique_ptr<LayerTreeFrameSink> layer_tree_frame_sink_;
   FakeLayerTreeHostImpl host_impl_;
-  CommonResourceObjects test_data_;
+  CommonResourceObjects test_resource1_{false};
+  CommonResourceObjects test_resource2_{false};
+  CommonResourceObjects test_resource_sw_{true};
 };
 
 TEST_F(TextureLayerTest, CheckPropertyChangeCausesCorrectBehavior) {
@@ -302,13 +283,11 @@
     scoped_refptr<TextureLayer> layer = TextureLayer::Create(nullptr);
     layer->SetIsDrawable(true);
     layer->SetBounds(gfx::Size(10, 10));
-    if (gpu) {
-      layer->SetTransferableResource(test_data_.resource1_,
-                                     test_data_.release_callback1_);
-    } else {
-      layer->SetTransferableResource(test_data_.resource_sw_,
-                                     test_data_.release_callback_sw_);
-    }
+
+    auto& test_resource = gpu ? test_resource1_ : test_resource_sw_;
+
+    layer->SetTransferableResource(test_resource.resource,
+                                   test_resource.release_callback);
 
     viz::ParentLocalSurfaceIdAllocator allocator;
     allocator.GenerateId();
@@ -334,16 +313,7 @@
     // Our LayerTreeHostClient makes a FakeLayerTreeFrameSink which returns all
     // resources when its detached, so the resources will not be in use in the
     // display compositor, and will be returned as not lost.
-    if (gpu) {
-      EXPECT_CALL(test_data_.mock_callback_,
-                  Release(test_data_.mailbox_name1_, _, false))
-          .Times(1);
-    } else {
-      EXPECT_CALL(test_data_.mock_callback_,
-                  ReleaseSW(test_data_.shared_image_sw_,
-                            test_data_.sync_token_sw_, false))
-          .Times(1);
-    }
+    test_resource.ExpectRelease();
     {
       base::RunLoop loop;
       loop.RunUntilIdle();
@@ -362,11 +332,8 @@
 class TextureLayerWithResourceTest : public TextureLayerTest {
  protected:
   void TearDown() override {
-    Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
-    EXPECT_CALL(
-        test_data_.mock_callback_,
-        Release(test_data_.mailbox_name1_, test_data_.sync_token1_, false))
-        .Times(1);
+    test_resource1_.Verify();
+    test_resource1_.ExpectRelease();
     TextureLayerTest::TearDown();
   }
 };
@@ -380,48 +347,38 @@
   Mock::VerifyAndClearExpectations(layer_tree_host_.get());
 
   EXPECT_CALL(*layer_tree_host_, SetNeedsCommit()).Times(AtLeast(1));
-  test_layer->SetTransferableResource(test_data_.resource1_,
-                                      test_data_.release_callback1_);
+  test_layer->SetTransferableResource(test_resource1_.resource,
+                                      test_resource1_.release_callback);
   Mock::VerifyAndClearExpectations(layer_tree_host_.get());
 
   EXPECT_CALL(*layer_tree_host_, SetNeedsCommit()).Times(AtLeast(1));
-  EXPECT_CALL(
-      test_data_.mock_callback_,
-      Release(test_data_.mailbox_name1_, test_data_.sync_token1_, false))
-      .Times(1);
-  test_layer->SetTransferableResource(test_data_.resource2_,
-                                      test_data_.release_callback2_);
+  test_resource1_.ExpectRelease();
+  test_layer->SetTransferableResource(test_resource2_.resource,
+                                      test_resource2_.release_callback);
   Mock::VerifyAndClearExpectations(layer_tree_host_.get());
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.Verify();
 
   EXPECT_CALL(*layer_tree_host_, SetNeedsCommit()).Times(AtLeast(1));
-  EXPECT_CALL(
-      test_data_.mock_callback_,
-      Release(test_data_.mailbox_name2_, test_data_.sync_token2_, false))
-      .Times(1);
+  test_resource2_.ExpectRelease();
   test_layer->ClearTexture();
   Mock::VerifyAndClearExpectations(layer_tree_host_.get());
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource2_.Verify();
 
   EXPECT_CALL(*layer_tree_host_, SetNeedsCommit()).Times(AtLeast(1));
-  test_layer->SetTransferableResource(test_data_.resource_sw_,
-                                      test_data_.release_callback_sw_);
+  test_layer->SetTransferableResource(test_resource1_.resource,
+                                      test_resource1_.release_callback);
   Mock::VerifyAndClearExpectations(layer_tree_host_.get());
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
 
   EXPECT_CALL(*layer_tree_host_, SetNeedsCommit()).Times(AtLeast(1));
-  EXPECT_CALL(
-      test_data_.mock_callback_,
-      ReleaseSW(test_data_.shared_image_sw_, test_data_.sync_token_sw_, false))
-      .Times(1);
+  test_resource1_.ExpectRelease();
   test_layer->ClearTexture();
   Mock::VerifyAndClearExpectations(layer_tree_host_.get());
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.Verify();
 
   // Test destructor.
   EXPECT_CALL(*layer_tree_host_, SetNeedsCommit()).Times(AtLeast(1));
-  test_layer->SetTransferableResource(test_data_.resource1_,
-                                      test_data_.release_callback1_);
+  test_layer->SetTransferableResource(test_resource1_.resource,
+                                      test_resource1_.release_callback);
 }
 
 TEST_F(TextureLayerWithResourceTest, AffectedByHdr) {
@@ -433,34 +390,28 @@
   EXPECT_CALL(*layer_tree_host_, SetNeedsCommit()).Times(AtLeast(1));
 
   // sRGB is unaffected by HDR parameters.
-  test_data_.resource1_.color_space = gfx::ColorSpace::CreateSRGB();
-  test_layer->SetTransferableResource(test_data_.resource1_,
-                                      test_data_.release_callback1_);
+  test_resource1_.resource.color_space = gfx::ColorSpace::CreateSRGB();
+  test_layer->SetTransferableResource(test_resource1_.resource,
+                                      test_resource1_.release_callback);
   Mock::VerifyAndClearExpectations(layer_tree_host_.get());
   EXPECT_FALSE(test_layer->RequiresSetNeedsDisplayOnHdrHeadroomChange());
   EXPECT_CALL(*layer_tree_host_, SetNeedsCommit()).Times(AtLeast(1));
-  EXPECT_CALL(
-      test_data_.mock_callback_,
-      Release(test_data_.mailbox_name1_, test_data_.sync_token1_, false))
-      .Times(1);
+  test_resource1_.ExpectRelease();
 
   // HDR10 is affected by HDR parameters.
-  test_data_.resource2_.color_space = gfx::ColorSpace::CreateHDR10();
-  test_layer->SetTransferableResource(test_data_.resource2_,
-                                      test_data_.release_callback2_);
+  test_resource2_.resource.color_space = gfx::ColorSpace::CreateHDR10();
+  test_layer->SetTransferableResource(test_resource2_.resource,
+                                      test_resource2_.release_callback);
   Mock::VerifyAndClearExpectations(layer_tree_host_.get());
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+
   EXPECT_TRUE(test_layer->RequiresSetNeedsDisplayOnHdrHeadroomChange());
-  EXPECT_CALL(
-      test_data_.mock_callback_,
-      Release(test_data_.mailbox_name2_, test_data_.sync_token2_, false))
-      .Times(1);
+  test_resource2_.ExpectRelease();
   EXPECT_CALL(*layer_tree_host_, SetNeedsCommit()).Times(AtLeast(1));
 
   // sRGB with extended range is affected by HDR parameters.
-  test_data_.resource1_.hdr_metadata.extended_range.emplace(5.f, 5.f);
-  test_layer->SetTransferableResource(test_data_.resource1_,
-                                      test_data_.release_callback1_);
+  test_resource1_.resource.hdr_metadata.extended_range.emplace(5.f, 5.f);
+  test_layer->SetTransferableResource(test_resource1_.resource,
+                                      test_resource1_.release_callback);
   Mock::VerifyAndClearExpectations(layer_tree_host_.get());
   EXPECT_TRUE(test_layer->RequiresSetNeedsDisplayOnHdrHeadroomChange());
 }
@@ -469,6 +420,8 @@
  public:
   TextureLayerMailboxHolderTest() : main_thread_("MAIN") {
     main_thread_.Start();
+    sync_token1_ = GenSyncToken();
+    sync_token2_ = GenSyncToken();
   }
 
   void Wait(const base::Thread& thread) {
@@ -481,8 +434,8 @@
   }
 
   void CreateMainRef() {
-    resource_holder_ = TestMailboxHolder::Create(test_data_.resource1_,
-                                                 test_data_.release_callback1_);
+    resource_holder_ = TestMailboxHolder::Create(
+        test_resource1_.resource, test_resource1_.release_callback);
   }
 
   void ReleaseMainRef() { resource_holder_ = nullptr; }
@@ -498,6 +451,8 @@
  protected:
   scoped_refptr<TextureLayer::TransferableResourceHolder> resource_holder_;
   base::Thread main_thread_;
+  gpu::SyncToken sync_token1_;
+  gpu::SyncToken sync_token2_;
 };
 
 TEST_F(TextureLayerMailboxHolderTest, TwoCompositors_BothReleaseThenMain) {
@@ -527,29 +482,26 @@
                                 main_thread_.task_runner()));
 
   Wait(main_thread_);
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.Verify();
 
   // The compositors both destroy their impl trees before the main thread layer
   // is destroyed.
-  std::move(compositor1).Run(SyncTokenFromUInt(100), false);
-  std::move(compositor2).Run(SyncTokenFromUInt(200), false);
+  std::move(compositor1).Run(sync_token1_, false);
+  std::move(compositor2).Run(sync_token2_, false);
 
   Wait(main_thread_);
 
-  EXPECT_CALL(test_data_.mock_callback_, Release(_, _, _)).Times(0);
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.ExpectNoRelease().Verify();
 
   // The main thread ref is the last one, so the resource is released back to
   // the embedder, with the last sync point provided by the impl trees.
-  EXPECT_CALL(test_data_.mock_callback_,
-              Release(test_data_.mailbox_name1_, SyncTokenFromUInt(200), false))
-      .Times(1);
+  test_resource1_.ExpectReleaseWithSyncToken(sync_token2_, false);
 
   main_thread_.task_runner()->PostTask(
       FROM_HERE, base::BindOnce(&TextureLayerMailboxHolderTest::ReleaseMainRef,
                                 base::Unretained(this)));
   Wait(main_thread_);
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.Verify();
 }
 
 TEST_F(TextureLayerMailboxHolderTest, TwoCompositors_MainReleaseBetween) {
@@ -579,10 +531,10 @@
                                 main_thread_.task_runner()));
 
   Wait(main_thread_);
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.ExpectNoRelease().Verify();
 
   // One compositor destroys their impl tree.
-  std::move(compositor1).Run(SyncTokenFromUInt(100), false);
+  std::move(compositor1).Run(sync_token1_, false);
 
   // Then the main thread reference is destroyed.
   main_thread_.task_runner()->PostTask(
@@ -591,18 +543,14 @@
 
   Wait(main_thread_);
 
-  EXPECT_CALL(test_data_.mock_callback_, Release(_, _, _)).Times(0);
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
-
+  test_resource1_.ExpectNoRelease().Verify();
   // The second impl reference is destroyed last, causing the resource to be
   // released back to the embedder with the last sync point from the impl tree.
-  EXPECT_CALL(test_data_.mock_callback_,
-              Release(test_data_.mailbox_name1_, SyncTokenFromUInt(200), true))
-      .Times(1);
+  test_resource1_.ExpectReleaseWithSyncToken(sync_token2_, true);
 
-  std::move(compositor2).Run(SyncTokenFromUInt(200), true);
+  std::move(compositor2).Run(sync_token2_, true);
   Wait(main_thread_);
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.Verify();
 }
 
 TEST_F(TextureLayerMailboxHolderTest, TwoCompositors_MainReleasedFirst) {
@@ -632,7 +580,7 @@
                                 main_thread_.task_runner()));
 
   Wait(main_thread_);
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.ExpectNoRelease().Verify();
 
   // The main thread reference is destroyed first.
   main_thread_.task_runner()->PostTask(
@@ -640,22 +588,19 @@
                                 base::Unretained(this)));
 
   // One compositor destroys their impl tree.
-  std::move(compositor2).Run(SyncTokenFromUInt(200), false);
+  std::move(compositor2).Run(sync_token2_, false);
 
   Wait(main_thread_);
 
-  EXPECT_CALL(test_data_.mock_callback_, Release(_, _, _)).Times(0);
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.ExpectNoRelease().Verify();
 
   // The second impl reference is destroyed last, causing the resource to be
   // released back to the embedder with the last sync point from the impl tree.
-  EXPECT_CALL(test_data_.mock_callback_,
-              Release(test_data_.mailbox_name1_, SyncTokenFromUInt(100), true))
-      .Times(1);
+  test_resource1_.ExpectReleaseWithSyncToken(sync_token1_, true);
 
-  std::move(compositor1).Run(SyncTokenFromUInt(100), true);
+  std::move(compositor1).Run(sync_token1_, true);
   Wait(main_thread_);
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.Verify();
 }
 
 class TextureLayerImplWithMailboxThreadedCallback : public LayerTreeTest {
@@ -683,7 +628,7 @@
       case 1:
         // Case #1: change resource before the commit. The old resource should
         // be released immediately.
-        SetMailbox('2');
+        SetNewFakeResource();
         EXPECT_EQ(1, callback_count_);
         PostSetNeedsCommitToMainThread();
 
@@ -694,7 +639,7 @@
         // Case #2: change resource after the commit (and draw), where the
         // layer draws. The old resource should be released during the next
         // commit.
-        SetMailbox('3');
+        SetNewFakeResource();
         EXPECT_EQ(1, callback_count_);
 
         // Cases 3-5 rely on a callback to advance.
@@ -705,7 +650,7 @@
         // Case #3: change resource when the layer doesn't draw. The old
         // resource should be released during the next commit.
         layer_->SetBounds(gfx::Size());
-        SetMailbox('4');
+        SetNewFakeResource();
         break;
       case 4:
         EXPECT_EQ(3, callback_count_);
@@ -716,7 +661,7 @@
       case 5:
         EXPECT_EQ(4, callback_count_);
         // Restore a resource for the next step.
-        SetMailbox('5');
+        SetNewFakeResource();
 
         // Cases 6 and 7 do not rely on callbacks to advance.
         pending_callback_ = false;
@@ -748,9 +693,7 @@
   }
 
   // Make sure callback is received on main and doesn't block the impl thread.
-  void ReleaseCallback(char mailbox_char,
-                       const gpu::SyncToken& sync_token,
-                       bool lost_resource) {
+  void ReleaseCallback(const gpu::SyncToken& sync_token, bool lost_resource) {
     EXPECT_EQ(true, main_thread_.CalledOnValidThread());
     EXPECT_FALSE(lost_resource);
     ++callback_count_;
@@ -768,17 +711,13 @@
     }
   }
 
-  void SetMailbox(char mailbox_char) {
+  void SetNewFakeResource() {
     EXPECT_EQ(true, main_thread_.CalledOnValidThread());
     viz::ReleaseCallback callback = base::BindOnce(
         &TextureLayerImplWithMailboxThreadedCallback::ReleaseCallback,
-        base::Unretained(this), mailbox_char);
+        base::Unretained(this));
 
-    const gfx::Size size(64, 64);
-    auto resource = viz::TransferableResource::MakeGpu(
-        MailboxFromChar(mailbox_char), GL_TEXTURE_2D,
-        SyncTokenFromUInt(static_cast<uint32_t>(mailbox_char)), size,
-        viz::SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
+    auto resource = MakeFakeResource();
     layer_->SetTransferableResource(resource, std::move(callback));
     // Damage the layer so we send a new frame with the new resource to the
     // Display compositor.
@@ -800,7 +739,7 @@
     layer_tree_host()->SetRootLayer(root_);
     layer_tree_host()->SetViewportRectAndScale(gfx::Rect(bounds), 1.f,
                                                viz::LocalSurfaceId());
-    SetMailbox('1');
+    SetNewFakeResource();
     EXPECT_EQ(0, callback_count_);
 
     // Setup is complete - advance to test case 1.
@@ -856,19 +795,11 @@
 // Test conditions for results of TextureLayerImpl::WillDraw under
 // different configurations of different mailbox, texture_id, and draw_mode.
 TEST_F(TextureLayerImplWithResourceTest, TestWillDraw) {
-  EXPECT_CALL(
-      test_data_.mock_callback_,
-      Release(test_data_.mailbox_name1_, test_data_.sync_token1_, false))
-      .Times(AnyNumber());
-  EXPECT_CALL(
-      test_data_.mock_callback_,
-      ReleaseSW(test_data_.shared_image_sw_, test_data_.sync_token_sw_, false))
-      .Times(AnyNumber());
   // Hardware mode.
   {
     std::unique_ptr<TextureLayerImpl> impl_layer = CreateTextureLayer();
-    impl_layer->SetTransferableResource(test_data_.resource1_,
-                                        test_data_.release_callback1_);
+    impl_layer->SetTransferableResource(test_resource1_.resource,
+                                        test_resource1_.release_callback);
     EXPECT_TRUE(WillDraw(impl_layer.get(), DRAW_MODE_HARDWARE));
   }
 
@@ -883,8 +814,8 @@
   // Software mode.
   {
     std::unique_ptr<TextureLayerImpl> impl_layer = CreateTextureLayer();
-    impl_layer->SetTransferableResource(test_data_.resource1_,
-                                        test_data_.release_callback1_);
+    impl_layer->SetTransferableResource(test_resource1_.resource,
+                                        test_resource1_.release_callback);
     EXPECT_FALSE(WillDraw(impl_layer.get(), DRAW_MODE_SOFTWARE));
   }
 
@@ -898,16 +829,16 @@
   {
     // Software resource.
     std::unique_ptr<TextureLayerImpl> impl_layer = CreateTextureLayer();
-    impl_layer->SetTransferableResource(test_data_.resource_sw_,
-                                        test_data_.release_callback_sw_);
+    impl_layer->SetTransferableResource(test_resource_sw_.resource,
+                                        test_resource_sw_.release_callback);
     EXPECT_TRUE(WillDraw(impl_layer.get(), DRAW_MODE_SOFTWARE));
   }
 
   // Resourceless software mode.
   {
     std::unique_ptr<TextureLayerImpl> impl_layer = CreateTextureLayer();
-    impl_layer->SetTransferableResource(test_data_.resource1_,
-                                        test_data_.release_callback1_);
+    impl_layer->SetTransferableResource(test_resource1_.resource,
+                                        test_resource1_.release_callback);
     EXPECT_FALSE(WillDraw(impl_layer.get(), DRAW_MODE_RESOURCELESS_SOFTWARE));
   }
 }
@@ -922,53 +853,43 @@
       pending_layer->CreateLayerImpl(host_impl_.active_tree()));
   ASSERT_TRUE(active_layer);
 
-  pending_layer->SetTransferableResource(test_data_.resource1_,
-                                         test_data_.release_callback1_);
+  pending_layer->SetTransferableResource(test_resource1_.resource,
+                                         test_resource1_.release_callback);
 
   // Test multiple commits without an activation. The resource wasn't used so
   // the original sync token is returned.
-  EXPECT_CALL(
-      test_data_.mock_callback_,
-      Release(test_data_.mailbox_name1_, test_data_.sync_token1_, false))
-      .Times(1);
-  pending_layer->SetTransferableResource(test_data_.resource2_,
-                                         test_data_.release_callback2_);
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.ExpectRelease();
+  pending_layer->SetTransferableResource(test_resource2_.resource,
+                                         test_resource2_.release_callback);
+  test_resource1_.Verify();
 
   // Test callback after activation.
   pending_layer->PushPropertiesTo(active_layer.get());
   active_layer->DidBecomeActive();
 
-  EXPECT_CALL(test_data_.mock_callback_, Release(_, _, _)).Times(0);
-  pending_layer->SetTransferableResource(test_data_.resource1_,
-                                         test_data_.release_callback1_);
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.ExpectNoRelease();
+  pending_layer->SetTransferableResource(test_resource1_.resource,
+                                         test_resource1_.release_callback);
+  test_resource1_.Verify();
 
-  EXPECT_CALL(test_data_.mock_callback_,
-              Release(test_data_.mailbox_name2_, _, false))
-      .Times(1);
+  test_resource2_.ExpectRelease();
   pending_layer->PushPropertiesTo(active_layer.get());
   active_layer->DidBecomeActive();
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource2_.Verify();
 
   // Test resetting the mailbox.
-  EXPECT_CALL(test_data_.mock_callback_,
-              Release(test_data_.mailbox_name1_, _, false))
-      .Times(1);
+  test_resource1_.ExpectRelease();
   pending_layer->SetTransferableResource(viz::TransferableResource(),
                                          viz::ReleaseCallback());
   pending_layer->PushPropertiesTo(active_layer.get());
   active_layer->DidBecomeActive();
-  Mock::VerifyAndClearExpectations(&test_data_.mock_callback_);
+  test_resource1_.Verify();
 
   // Test destructor. The resource wasn't used so the original sync token is
   // returned.
-  EXPECT_CALL(
-      test_data_.mock_callback_,
-      Release(test_data_.mailbox_name1_, test_data_.sync_token1_, false))
-      .Times(1);
-  pending_layer->SetTransferableResource(test_data_.resource1_,
-                                         test_data_.release_callback1_);
+  test_resource1_.ExpectRelease();
+  pending_layer->SetTransferableResource(test_resource1_.resource,
+                                         test_resource1_.release_callback);
 }
 
 TEST_F(TextureLayerImplWithResourceTest,
@@ -976,11 +897,9 @@
   std::unique_ptr<TextureLayerImpl> impl_layer = CreateTextureLayer();
   ASSERT_TRUE(impl_layer);
 
-  EXPECT_CALL(test_data_.mock_callback_,
-              Release(test_data_.mailbox_name1_, _, false))
-      .Times(1);
-  impl_layer->SetTransferableResource(test_data_.resource1_,
-                                      test_data_.release_callback1_);
+  test_resource1_.ExpectRelease();
+  impl_layer->SetTransferableResource(test_resource1_.resource,
+                                      test_resource1_.release_callback);
   impl_layer->DidBecomeActive();
   EXPECT_TRUE(impl_layer->WillDraw(
       DRAW_MODE_HARDWARE, host_impl_.active_tree()->resource_provider()));
@@ -1004,10 +923,7 @@
       return true;
     }
 
-    constexpr gfx::Size size(64, 64);
-    *resource = viz::TransferableResource::MakeGpu(
-        MailboxFromChar('1'), GL_TEXTURE_2D, SyncTokenFromUInt(0x123), size,
-        viz::SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
+    *resource = MakeFakeResource();
     *release_callback = base::BindOnce(
         &TextureLayerNoExtraCommitForMailboxTest::ResourceReleased,
         base::Unretained(this));
@@ -1080,11 +996,7 @@
   }
 
   viz::TransferableResource MakeResource(char name) {
-    constexpr gfx::Size size(64, 64);
-    return viz::TransferableResource::MakeGpu(
-        MailboxFromChar(name), GL_TEXTURE_2D,
-        SyncTokenFromUInt(static_cast<uint32_t>(name)), size,
-        viz::SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
+    return MakeFakeResource();
   }
 
   void ResourceReleased(const gpu::SyncToken& sync_token, bool lost_resource) {
@@ -1239,10 +1151,7 @@
   bool PrepareTransferableResource(
       viz::TransferableResource* resource,
       viz::ReleaseCallback* release_callback) override {
-    constexpr gfx::Size size(64, 64);
-    *resource = viz::TransferableResource::MakeGpu(
-        MailboxFromChar('1'), GL_TEXTURE_2D, SyncTokenFromUInt(1), size,
-        viz::SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
+    *resource = MakeFakeResource();
     *release_callback =
         base::BindOnce(&TextureLayerReleaseResourcesBase::ResourceReleased,
                        base::Unretained(this));
@@ -1311,16 +1220,12 @@
     EndTest();
   }
 
-  void SetMailbox(char mailbox_char) {
+  void SetNewFakeResource() {
     EXPECT_EQ(true, main_thread_.CalledOnValidThread());
     viz::ReleaseCallback callback = base::BindOnce(
         &TextureLayerWithResourceMainThreadDeleted::ReleaseCallback,
         base::Unretained(this));
-    constexpr gfx::Size size(64, 64);
-    auto resource = viz::TransferableResource::MakeGpu(
-        MailboxFromChar(mailbox_char), GL_TEXTURE_2D,
-        SyncTokenFromUInt(static_cast<uint32_t>(mailbox_char)), size,
-        viz::SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
+    auto resource = MakeFakeResource();
     layer_->SetTransferableResource(resource, std::move(callback));
   }
 
@@ -1345,7 +1250,7 @@
     callback_count_ = 0;
 
     // Set the resource on the main thread.
-    SetMailbox('1');
+    SetNewFakeResource();
     EXPECT_EQ(0, callback_count_);
 
     PostSetNeedsCommitToMainThread();
@@ -1382,16 +1287,12 @@
     EndTest();
   }
 
-  void SetMailbox(char mailbox_char) {
+  void SetNewFakeResource() {
     EXPECT_EQ(true, main_thread_.CalledOnValidThread());
     viz::ReleaseCallback callback = base::BindOnce(
         &TextureLayerWithResourceImplThreadDeleted::ReleaseCallback,
         base::Unretained(this));
-    constexpr gfx::Size size(64, 64);
-    auto resource = viz::TransferableResource::MakeGpu(
-        MailboxFromChar(mailbox_char), GL_TEXTURE_2D,
-        SyncTokenFromUInt(static_cast<uint32_t>(mailbox_char)), size,
-        viz::SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
+    auto resource = MakeFakeResource();
     layer_->SetTransferableResource(resource, std::move(callback));
   }
 
@@ -1416,7 +1317,7 @@
     callback_count_ = 0;
 
     // Set the resource on the main thread.
-    SetMailbox('1');
+    SetNewFakeResource();
     EXPECT_EQ(0, callback_count_);
 
     PostSetNeedsCommitToMainThread();
@@ -1554,21 +1455,8 @@
         // The test starts by inserting the TextureLayer to the tree.
         root_->AddChild(texture_layer_);
 
-        scoped_refptr<gpu::ClientSharedImage> shared_image_;
-        gpu::SyncToken sync_token_;
-        gfx::Size size(1, 1);
-        AllocateSharedImage(frame_sink_->GetSharedImageInterface(), size,
-                            shared_image_, sync_token_);
-        auto transferable_resource =
-            viz::TransferableResource::MakeSoftwareSharedImage(
-                shared_image_, sync_token_, shared_image_->size(),
-                viz::SinglePlaneFormat::kBGRA_8888,
-                viz::TransferableResource::ResourceSource::kTileRasterTask);
-        auto release_callback =
-            base::BindOnce(&DeleteSharedImage, std::move(shared_image_));
-
-        texture_layer_->SetTransferableResource(
-            std::move(transferable_resource), std::move(release_callback));
+        texture_layer_->SetTransferableResource(MakeFakeSoftwareResource(),
+                                                base::DoNothing());
       } break;
       case 2:
         // When the layer is removed from the tree, the layer should be
@@ -1621,21 +1509,8 @@
         // The test starts by inserting the TextureLayer to the tree.
         root_->AddChild(texture_layer_);
 
-        scoped_refptr<gpu::ClientSharedImage> shared_image_;
-        gpu::SyncToken sync_token_;
-        gfx::Size size(1, 1);
-        AllocateSharedImage(frame_sink_->GetSharedImageInterface(), size,
-                            shared_image_, sync_token_);
-        auto transferable_resource =
-            viz::TransferableResource::MakeSoftwareSharedImage(
-                shared_image_, sync_token_, shared_image_->size(),
-                viz::SinglePlaneFormat::kBGRA_8888,
-                viz::TransferableResource::ResourceSource::kTileRasterTask);
-        auto release_callback =
-            base::BindOnce(&DeleteSharedImage, std::move(shared_image_));
-
-        texture_layer_->SetTransferableResource(
-            std::move(transferable_resource), std::move(release_callback));
+        texture_layer_->SetTransferableResource(MakeFakeSoftwareResource(),
+                                                base::DoNothing());
       }
 
       break;
@@ -1695,38 +1570,10 @@
         // The test starts by inserting the TextureLayer to the tree.
         root_->AddChild(texture_layer_);
 
-        scoped_refptr<gpu::ClientSharedImage> shared_image1_;
-        gpu::SyncToken sync_token1_;
-        scoped_refptr<gpu::ClientSharedImage> shared_image2_;
-        gpu::SyncToken sync_token2_;
-        gfx::Size size(1, 1);
-        AllocateSharedImage(frame_sink_->GetSharedImageInterface(), size,
-                            shared_image1_, sync_token1_);
-        AllocateSharedImage(frame_sink_->GetSharedImageInterface(), size,
-                            shared_image2_, sync_token2_);
-        auto transferable_resource1 =
-            viz::TransferableResource::MakeSoftwareSharedImage(
-                shared_image1_, sync_token1_, shared_image1_->size(),
-                viz::SinglePlaneFormat::kBGRA_8888,
-                viz::TransferableResource::ResourceSource::kTileRasterTask);
-
-        auto transferable_resource2 =
-            viz::TransferableResource::MakeSoftwareSharedImage(
-                shared_image2_, sync_token2_, shared_image2_->size(),
-                viz::SinglePlaneFormat::kBGRA_8888,
-                viz::TransferableResource::ResourceSource::kTileRasterTask);
-
-        // Give the TextureLayer a resource so it contributes to the frame. It
-        // doesn't need to register the layer otherwise.
-        auto release_callback1 =
-            base::BindOnce(&DeleteSharedImage, std::move(shared_image1_));
-        auto release_callback2 =
-            base::BindOnce(&DeleteSharedImage, std::move(shared_image2_));
-
-        texture_layer_->SetTransferableResource(
-            std::move(transferable_resource1), std::move(release_callback1));
-        texture_layer_->SetTransferableResource(
-            std::move(transferable_resource2), std::move(release_callback2));
+        texture_layer_->SetTransferableResource(MakeFakeSoftwareResource(),
+                                                base::DoNothing());
+        texture_layer_->SetTransferableResource(MakeFakeSoftwareResource(),
+                                                base::DoNothing());
       } break;
       case 2:
         // Force a commit and SubmitCompositorFrame so that we can see it.
@@ -1779,22 +1626,12 @@
         // The test starts by inserting the TextureLayer to the tree.
         root_->AddChild(texture_layer_);
 
-        scoped_refptr<gpu::ClientSharedImage> shared_image_;
-        gpu::SyncToken sync_token_;
-        gfx::Size size(1, 1);
-        AllocateSharedImage(frame_sink_->GetSharedImageInterface(), size,
-                            shared_image_, sync_token_);
-        auto transferable_resource =
-            viz::TransferableResource::MakeSoftwareSharedImage(
-                shared_image_, sync_token_, shared_image_->size(),
-                viz::SinglePlaneFormat::kBGRA_8888,
-                viz::TransferableResource::ResourceSource::kTileRasterTask);
         auto release_callback = base::BindOnce(
             &SoftwareTextureLayerLoseFrameSinkTest::ReleaseCallback,
-            base::Unretained(this), std::move(shared_image_));
+            base::Unretained(this));
 
-        texture_layer_->SetTransferableResource(
-            std::move(transferable_resource), std::move(release_callback));
+        texture_layer_->SetTransferableResource(MakeFakeSoftwareResource(),
+                                                std::move(release_callback));
 
         EXPECT_FALSE(released_);
       } break;
@@ -1830,14 +1667,11 @@
     source_frame_number_ = commit_state.source_frame_number;
   }
 
-  void ReleaseCallback(scoped_refptr<gpu::ClientSharedImage> shared_image,
-                       const gpu::SyncToken& sync_token,
-                       bool lost) {
+  void ReleaseCallback(const gpu::SyncToken& sync_token, bool lost) {
     // The software resource is not released when the LayerTreeFrameSink is lost
     // since software resources are not destroyed by the GPU process dying. It
     // is released only after we call TextureLayer::ClearClient().
 
-    shared_image->UpdateDestructionSyncToken(sync_token);
     EXPECT_EQ(source_frame_number_, 3);
     released_ = true;
     EndTest();
diff --git a/cc/test/fake_ui_resource_layer_tree_host_impl.cc b/cc/test/fake_ui_resource_layer_tree_host_impl.cc
index ee2e863..dd79beb 100644
--- a/cc/test/fake_ui_resource_layer_tree_host_impl.cc
+++ b/cc/test/fake_ui_resource_layer_tree_host_impl.cc
@@ -29,10 +29,9 @@
   UIResourceData data;
 
   data.resource_id_for_export = resource_provider()->ImportResource(
-      viz::TransferableResource::MakeGpu(
-          gpu::Mailbox::Generate(), GL_TEXTURE_2D, gpu::SyncToken(),
-          bitmap.GetSize(), viz::SinglePlaneFormat::kRGBA_8888,
-          false /* is_overlay_candidate */),
+      viz::TransferableResource::Make(
+          gpu::ClientSharedImage::CreateForTesting(),
+          viz::TransferableResource::ResourceSource::kTest, gpu::SyncToken()),
       base::DoNothing());
 
   data.opaque = bitmap.GetOpaque();
diff --git a/cc/test/pixel_test.cc b/cc/test/pixel_test.cc
index 0d76248..100d0622ae 100644
--- a/cc/test/pixel_test.cc
+++ b/cc/test/pixel_test.cc
@@ -284,10 +284,9 @@
   CHECK_GE(mem.size(), info.computeByteSize(row_bytes));
   source.readPixels(info, mem.data(), row_bytes, 0, 0);
 
-  auto transferable_resource =
-      viz::TransferableResource::MakeSoftwareSharedImage(
-          shared_image, sync_token, size, viz::SinglePlaneFormat::kBGRA_8888,
-          viz::TransferableResource::ResourceSource::kTileRasterTask);
+  auto transferable_resource = viz::TransferableResource::Make(
+      shared_image, viz::TransferableResource::ResourceSource::kTileRasterTask,
+      sync_token);
   auto release_callback =
       base::BindOnce(&DeleteSharedImage, std::move(shared_image));
 
diff --git a/cc/test/render_pass_test_utils.cc b/cc/test/render_pass_test_utils.cc
index 4d30c62..9141b9d 100644
--- a/cc/test/render_pass_test_utils.cc
+++ b/cc/test/render_pass_test_utils.cc
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-
 #include "cc/test/render_pass_test_utils.h"
 
 #include <stdint.h>
@@ -27,6 +26,7 @@
 #include "components/viz/common/resources/returned_resource.h"
 #include "components/viz/common/resources/transferable_resource.h"
 #include "components/viz/service/display/display_resource_provider.h"
+#include "gpu/command_buffer/client/client_shared_image.h"
 #include "gpu/command_buffer/common/sync_token.h"
 #include "third_party/skia/include/core/SkColor.h"
 #include "third_party/skia/include/core/SkImageFilter.h"
@@ -39,15 +39,12 @@
 
 viz::ResourceId CreateAndImportResource(
     viz::ClientResourceProvider* resource_provider,
-    const gpu::SyncToken& sync_token,
-    gfx::ColorSpace color_space = gfx::ColorSpace::CreateSRGB()) {
-  constexpr gfx::Size size(64, 64);
-  auto transfer_resource = viz::TransferableResource::MakeGpu(
-      gpu::Mailbox::Generate(), GL_TEXTURE_2D, sync_token, size,
-      viz::SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
-  transfer_resource.color_space = std::move(color_space);
-  return resource_provider->ImportResource(transfer_resource,
-                                           base::DoNothing());
+    const gpu::SyncToken& sync_token) {
+  auto resource = viz::TransferableResource::Make(
+      gpu::ClientSharedImage::CreateForTesting(),
+      viz::TransferableResource::ResourceSource::kTest, sync_token);
+
+  return resource_provider->ImportResource(resource, base::DoNothing());
 }
 
 }  // anonymous namespace
diff --git a/cc/trees/layer_tree_host_impl_unittest.cc b/cc/trees/layer_tree_host_impl_unittest.cc
index cae8622..1b6d0a4 100644
--- a/cc/trees/layer_tree_host_impl_unittest.cc
+++ b/cc/trees/layer_tree_host_impl_unittest.cc
@@ -10717,11 +10717,9 @@
              gfx::ColorSpace(), gpu::SHARED_IMAGE_USAGE_CPU_WRITE_ONLY,
              "BlendStateCheckLayerTest"});
     auto sync_token = shared_image_interface_->GenVerifiedSyncToken();
-    viz::TransferableResource resource =
-        viz::TransferableResource::MakeSoftwareSharedImage(
-            shared_image, sync_token, gfx::Size(1, 1),
-            viz::SinglePlaneFormat::kBGRA_8888,
-            viz::TransferableResource::ResourceSource::kTileRasterTask);
+    viz::TransferableResource resource = viz::TransferableResource::Make(
+        shared_image,
+        viz::TransferableResource::ResourceSource::kTileRasterTask, sync_token);
 
     resource_id_ = resource_provider_->ImportResource(std::move(resource),
                                                       base::DoNothing());
diff --git a/cc/trees/layer_tree_host_perftest.cc b/cc/trees/layer_tree_host_perftest.cc
index 87389e63..9a48829 100644
--- a/cc/trees/layer_tree_host_perftest.cc
+++ b/cc/trees/layer_tree_host_perftest.cc
@@ -332,10 +332,9 @@
                                    next_fence_sync_);
     next_sync_token.SetVerifyFlush();
 
-    constexpr gfx::Size size(64, 64);
-    viz::TransferableResource resource = viz::TransferableResource::MakeGpu(
-        gpu_mailbox, GL_TEXTURE_2D, next_sync_token, size,
-        viz::SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
+    viz::TransferableResource resource = viz::TransferableResource::Make(
+        gpu::ClientSharedImage::CreateForTesting(),
+        viz::TransferableResource::ResourceSource::kTest, next_sync_token);
     next_fence_sync_++;
 
     tab_contents_->SetTransferableResource(resource, std::move(callback));
diff --git a/cc/trees/layer_tree_host_unittest_context.cc b/cc/trees/layer_tree_host_unittest_context.cc
index 60a6dc3..cd48664 100644
--- a/cc/trees/layer_tree_host_unittest_context.cc
+++ b/cc/trees/layer_tree_host_unittest_context.cc
@@ -872,10 +872,9 @@
     scoped_refptr<TextureLayer> texture = TextureLayer::Create(nullptr);
     texture->SetBounds(gfx::Size(10, 10));
     texture->SetIsDrawable(true);
-    constexpr gfx::Size size(64, 64);
-    auto resource = viz::TransferableResource::MakeGpu(
-        shared_image, GL_TEXTURE_2D, sync_token, size,
-        viz::SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
+    auto resource = viz::TransferableResource::Make(
+        shared_image, viz::TransferableResource::ResourceSource::kTest,
+        sync_token);
     texture->SetTransferableResource(
         resource, base::BindOnce(&LayerTreeHostContextTestDontUseLostResources::
                                      EmptyReleaseCallback));
diff --git a/cc/trees/proxy_main.cc b/cc/trees/proxy_main.cc
index 604cc01..b4340002 100644
--- a/cc/trees/proxy_main.cc
+++ b/cc/trees/proxy_main.cc
@@ -13,6 +13,7 @@
 #include "base/functional/bind.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/notreached.h"
+#include "base/profiler/sample_metadata.h"
 #include "base/synchronization/waitable_event.h"
 #include "base/trace_event/trace_event.h"
 #include "base/trace_event/trace_id_helper.h"
@@ -143,6 +144,8 @@
                     perfetto::protos::pbzero::MainFramePipeline::Step::
                         BEGIN_MAIN_FRAME);
               });
+  base::ScopedSampleMetadata metadata("ProxyMain::BeginMainFrame", 1,
+                                      base::SampleMetadataScope::kProcess);
 
   // This needs to run unconditionally, so do it before any early-returns.
   if (layer_tree_host_->scheduling_client())
diff --git a/chrome/VERSION b/chrome/VERSION
index a57d8975..2641cf0 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=139
 MINOR=0
-BUILD=7238
+BUILD=7239
 PATCH=0
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java
index 6b53c85..050e69f0 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java
@@ -151,7 +151,6 @@
     }
 
     private @Nullable LayoutStateProvider mLayoutStateProvider;
-    protected Runnable mAppMenuInvalidator;
 
     /**
      * Construct a new {@link AppMenuPropertiesDelegateImpl}.
@@ -216,7 +215,7 @@
         return mReadAloudAppMenuResetter;
     }
 
-    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     @Nullable
     public ModelList getModelList() {
         return mModelList;
@@ -268,16 +267,16 @@
     }
 
     @Override
-    public final ModelList getMenuItems(AppMenuHandler handler) {
+    public final ModelList getMenuItems() {
         mReadAloudPos = -1;
         mHasReadAloudInserted = false;
-        mModelList = buildMenuModelList(handler);
+        mModelList = buildMenuModelList();
         return mModelList;
     }
 
     /** Construct the ModelList for the appropriate current state of the menu. */
     @VisibleForTesting
-    public abstract ModelList buildMenuModelList(AppMenuHandler handler);
+    public abstract ModelList buildMenuModelList();
 
     /**
      * Builds a property model for a divider item type.
@@ -299,8 +298,20 @@
      * @return A Builder object that forms the basis for text menu item models.
      */
     public PropertyModel.Builder buildBaseModelForTextItem(@IdRes int id) {
-        return new PropertyModel.Builder(AppMenuItemProperties.ALL_KEYS)
-                .with(AppMenuItemProperties.MENU_ITEM_ID, id)
+        return populateBaseModelForTextItem(
+                new PropertyModel.Builder(AppMenuItemProperties.ALL_KEYS), id);
+    }
+
+    /**
+     * Populates the PropertyModel.Builder with the common properties for a text menu item.
+     *
+     * @param builder The builder to populate with data.
+     * @param id The id of the text menu item.
+     * @return A Builder object that forms the basis for text menu item models.
+     */
+    public PropertyModel.Builder populateBaseModelForTextItem(
+            PropertyModel.Builder builder, @IdRes int id) {
+        return builder.with(AppMenuItemProperties.MENU_ITEM_ID, id)
                 .with(AppMenuItemProperties.ENABLED, true)
                 .with(AppMenuItemProperties.ICON_COLOR_RES, getMenuItemIconColorRes(id))
                 .with(AppMenuItemProperties.ICON_SHOW_BADGE, shouldShowBadgeOnMenuItemIcon(id))
@@ -1018,6 +1029,34 @@
      */
     @Nullable
     protected ListItem maybeBuildPriceTrackingListItem(@Nullable Tab currentTab, boolean showIcon) {
+        Boolean show = getPriceTrackingMenuItemInfo(currentTab);
+        if (show == null) return null;
+
+        if (show) {
+            return new ListItem(
+                    AppMenuItemType.STANDARD,
+                    buildModelForStandardMenuItem(
+                            R.id.enable_price_tracking_menu_id,
+                            R.string.enable_price_tracking_menu_item,
+                            showIcon ? R.drawable.price_tracking_disabled : 0));
+        } else {
+            return new ListItem(
+                    AppMenuItemType.STANDARD,
+                    buildModelForStandardMenuItem(
+                            R.id.disable_price_tracking_menu_id,
+                            R.string.disable_price_tracking_menu_item,
+                            showIcon ? R.drawable.price_tracking_enabled_filled : 0));
+        }
+    }
+
+    /**
+     * Determine which menu to show for price tracking feature.
+     *
+     * @param currentTab The currently selected tab.
+     * @return {@code true} to show 'enable'. Shows no option if {@code null}.
+     */
+    @Nullable
+    public Boolean getPriceTrackingMenuItemInfo(@Nullable Tab currentTab) {
         if (currentTab == null || currentTab.getWebContents() == null) {
             return null;
         }
@@ -1049,21 +1088,7 @@
             showStartPriceTracking = !isSubscribed;
         }
 
-        if (showStartPriceTracking) {
-            return new ListItem(
-                    AppMenuItemType.STANDARD,
-                    buildModelForStandardMenuItem(
-                            R.id.enable_price_tracking_menu_id,
-                            R.string.enable_price_tracking_menu_item,
-                            showIcon ? R.drawable.price_tracking_disabled : 0));
-        } else {
-            return new ListItem(
-                    AppMenuItemType.STANDARD,
-                    buildModelForStandardMenuItem(
-                            R.id.disable_price_tracking_menu_id,
-                            R.string.disable_price_tracking_menu_item,
-                            showIcon ? R.drawable.price_tracking_enabled_filled : 0));
-        }
+        return showStartPriceTracking;
     }
 
     /**
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java
index 60b50ca..f0aa72e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java
@@ -444,7 +444,10 @@
         mToolbarCoordinator.get().onToolbarInitialized(mToolbarManager, null);
 
         CustomTabToolbar toolbar = mActivity.findViewById(R.id.toolbar);
-        toolbar.calculateToolbarWidthBeforeMeasure(mActivity, mIntentDataProvider.get());
+        toolbar.initVisibilityRule(
+                mActivity,
+                () -> mAppMenuCoordinator.getAppMenuHandler(),
+                mIntentDataProvider.get());
         if (ChromeFeatureList.sCctIntentFeatureOverrides.isEnabled()) {
             toolbar.setFeatureOverridesManager(mFeatureOverridesManagerSupplier.get());
         }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabAppMenuPropertiesDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabAppMenuPropertiesDelegate.java
index 9757b75..9d12067 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabAppMenuPropertiesDelegate.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabAppMenuPropertiesDelegate.java
@@ -120,7 +120,7 @@
 
     @Override
     @VisibleForTesting
-    public MVCListAdapter.ModelList buildMenuModelList(AppMenuHandler handler) {
+    public MVCListAdapter.ModelList buildMenuModelList() {
         MVCListAdapter.ModelList modelList = new MVCListAdapter.ModelList();
 
         Tab currentTab = mActivityTabProvider.get();
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbar.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbar.java
index a6c922e3..4f66433 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbar.java
@@ -68,6 +68,7 @@
 import org.chromium.base.task.PostTask;
 import org.chromium.base.task.TaskTraits;
 import org.chromium.chrome.R;
+import org.chromium.chrome.browser.app.appmenu.AppMenuPropertiesDelegateImpl;
 import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
 import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
 import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider.CustomTabProfileType;
@@ -108,6 +109,7 @@
 import org.chromium.chrome.browser.toolbar.LocationBarModel;
 import org.chromium.chrome.browser.toolbar.ToolbarProgressBar;
 import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarButtonVariant;
+import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarFeatures;
 import org.chromium.chrome.browser.toolbar.menu_button.MenuButton;
 import org.chromium.chrome.browser.toolbar.optional_button.ButtonData;
 import org.chromium.chrome.browser.toolbar.optional_button.OptionalButtonCoordinator;
@@ -118,6 +120,8 @@
 import org.chromium.chrome.browser.toolbar.top.ToolbarPhone;
 import org.chromium.chrome.browser.toolbar.top.ToolbarSnapshotDifference;
 import org.chromium.chrome.browser.toolbar.top.TopToolbarCoordinator.ToolbarColorObserver;
+import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
+import org.chromium.chrome.browser.ui.appmenu.AppMenuObserver;
 import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityClient;
 import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityExtras.ResolutionType;
 import org.chromium.chrome.browser.ui.theme.BrandedColorScheme;
@@ -187,6 +191,7 @@
     private boolean mShouldHighlightCookieControlsIcon;
     private int mBlockingStatus3pcd;
     private BrowserServicesIntentDataProvider mIntentDataProvider;
+    private Supplier<AppMenuHandler> mAppMenuHandler;
 
     private final Handler mTaskHandler = new Handler();
     private final ButtonVisibilityRule mButtonVisibilityRule =
@@ -408,15 +413,19 @@
     }
 
     /**
-     * Calculate toolbar with before {@link onMeasure(int, int)} is called.
+     * initialize the toolbar with menu,.
      *
      * @param activity The {@link Activity} that the toolbar is attached to.
+     * @param appMenuHandler Supplief of {@link AppMenuHandler}.
      * @param intentDataProvider {@link BrowserServicesIntentDataProvider} for accessing CCT intent
      *     data.
      */
     @ExperimentalOpenInBrowser
-    public void calculateToolbarWidthBeforeMeasure(
-            Activity activity, BrowserServicesIntentDataProvider intentDataProvider) {
+    public void initVisibilityRule(
+            Activity activity,
+            Supplier<AppMenuHandler> appMenuHandler,
+            BrowserServicesIntentDataProvider intentDataProvider) {
+        mAppMenuHandler = appMenuHandler;
         if (mIntentDataProvider == null) {
             mIntentDataProvider = intentDataProvider;
             @CustomTabsButtonState
@@ -1520,8 +1529,52 @@
                 return;
             }
 
-            boolean show = buttonVariant != AdaptiveToolbarButtonVariant.READER_MODE;
+            boolean show =
+                    AdaptiveToolbarFeatures.isDynamicAction(buttonVariant)
+                            && buttonVariant != AdaptiveToolbarButtonVariant.READER_MODE;
             mMenuButton.findViewById(R.id.menu_dot).setVisibility(show ? View.VISIBLE : View.GONE);
+            if (!show) return;
+
+            int menuId = getHighlightMenuId(buttonVariant);
+            if (menuId < 0) return;
+
+            mAppMenuHandler.get().setMenuHighlight(menuId, false);
+            AppMenuObserver menuObserver =
+                    new AppMenuObserver() {
+                        @Override
+                        public void onMenuVisibilityChanged(boolean isVisible) {
+                            // TODO(crbug.com/424807997): Do this toggling in MenuButton MVC. Do it
+                            // upon page navigation as well.
+                            if (isVisible) {
+                                mMenuButton.findViewById(R.id.menu_dot).setVisibility(View.GONE);
+                                mAppMenuHandler.get().removeObserver(this);
+                            }
+                        }
+
+                        @Override
+                        public void onMenuHighlightChanged(boolean highlighting) {}
+                    };
+            mAppMenuHandler.get().addObserver(menuObserver);
+        }
+
+        private int getHighlightMenuId(@AdaptiveToolbarButtonVariant int buttonVariant) {
+            return switch (buttonVariant) {
+                case AdaptiveToolbarButtonVariant.PRICE_TRACKING -> {
+                    // Figure out which of the two menu items (enable/disable) appears and needs
+                    // highlighting.
+                    // TODO(crbug.com/424807997): Avoid casting.
+                    var appMenuDelegate =
+                            (AppMenuPropertiesDelegateImpl)
+                                    mAppMenuHandler.get().getMenuPropertiesDelegate();
+                    var showEnabled = appMenuDelegate.getPriceTrackingMenuItemInfo(getCurrentTab());
+                    if (showEnabled == null) yield -1;
+                    yield showEnabled
+                            ? R.id.enable_price_tracking_menu_id
+                            : R.id.disable_price_tracking_menu_id;
+                }
+                case AdaptiveToolbarButtonVariant.PRICE_INSIGHTS -> R.id.price_insights_menu_id;
+                default -> -1;
+            };
         }
 
         private void updateOptionalButtonTint() {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegate.java
index 09915bd..375746a 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegate.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegate.java
@@ -5,6 +5,7 @@
 package org.chromium.chrome.browser.tabbed_mode;
 
 import android.content.Context;
+import android.graphics.drawable.Drawable;
 import android.util.SparseArray;
 import android.view.View;
 
@@ -13,6 +14,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
 import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.core.graphics.drawable.DrawableCompat;
 
 import org.chromium.base.BuildInfo;
 import org.chromium.base.CallbackController;
@@ -50,6 +53,7 @@
 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
 import org.chromium.chrome.browser.tinker_tank.TinkerTankDelegate;
 import org.chromium.chrome.browser.toolbar.ToolbarManager;
+import org.chromium.chrome.browser.toolbar.menu_button.MenuItemState;
 import org.chromium.chrome.browser.ui.appmenu.AppMenuDelegate;
 import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
 import org.chromium.chrome.browser.ui.appmenu.AppMenuItemProperties;
@@ -102,6 +106,8 @@
      */
     private @Nullable IncognitoReauthController mIncognitoReauthController;
 
+    private @Nullable Runnable mUpdateStateChangeObserver;
+
     private final CallbackController mIncognitoReauthCallbackController = new CallbackController();
 
     public TabbedAppMenuPropertiesDelegate(
@@ -161,11 +167,11 @@
     }
 
     @Override
-    public MVCListAdapter.ModelList buildMenuModelList(AppMenuHandler handler) {
+    public MVCListAdapter.ModelList buildMenuModelList() {
         int menuGroup = getMenuGroup();
         MVCListAdapter.ModelList modelList = new MVCListAdapter.ModelList();
         if (menuGroup == MenuGroup.PAGE_MENU) {
-            populatePageModeMenu(modelList, handler);
+            populatePageModeMenu(modelList);
         } else if (menuGroup == MenuGroup.OVERVIEW_MODE_MENU) {
             populateOverviewModeMenu(modelList);
         } else if (menuGroup == MenuGroup.TABLET_EMPTY_MODE_MENU) {
@@ -174,7 +180,7 @@
         return modelList;
     }
 
-    private void populatePageModeMenu(MVCListAdapter.ModelList modelList, AppMenuHandler handler) {
+    private void populatePageModeMenu(MVCListAdapter.ModelList modelList) {
         Tab currentTab = mActivityTabProvider.get();
 
         GURL url = currentTab != null ? currentTab.getUrl() : GURL.emptyGURL();
@@ -202,9 +208,9 @@
         mUpdateMenuItemVisible = shouldShowUpdateMenuItem();
         if (mUpdateMenuItemVisible) {
             modelList.add(buildUpdateItem());
-            mAppMenuInvalidator = handler::invalidateAppMenu;
+            mUpdateStateChangeObserver = buildUpdateStateChangedObserver();
             UpdateMenuItemHelper.getInstance(mTabModelSelector.getModel(false).getProfile())
-                    .registerObserver(mAppMenuInvalidator);
+                    .registerObserver(mUpdateStateChangeObserver);
         }
 
         // New Tab
@@ -357,6 +363,22 @@
         }
     }
 
+    private Runnable buildUpdateStateChangedObserver() {
+        return () -> {
+            MVCListAdapter.ModelList modelList = getModelList();
+            if (modelList == null) {
+                assert false : "ModelList should not be null";
+                return;
+            }
+            for (MVCListAdapter.ListItem listItem : getModelList()) {
+                if (listItem.model.get(AppMenuItemProperties.MENU_ITEM_ID) == R.id.update_menu_id) {
+                    updateUpdateItemData(listItem.model);
+                    return;
+                }
+            }
+        };
+    }
+
     private void maybeAddDividerLine(MVCListAdapter.ModelList modelList, @IdRes int id) {
         if (modelList.get(modelList.size() - 1).type == AppMenuHandler.AppMenuItemType.DIVIDER) {
             return;
@@ -389,14 +411,40 @@
     private MVCListAdapter.ListItem buildUpdateItem() {
         assert shouldShowUpdateMenuItem();
         PropertyModel model =
-                buildModelForStandardMenuItem(
-                        R.id.update_menu_id, R.string.menu_update, R.drawable.menu_update);
-        model.set(
-                AppMenuItemProperties.CUSTOM_ITEM_DATA,
+                populateBaseModelForTextItem(
+                                new PropertyModel.Builder(UpdateMenuItemViewBinder.ALL_KEYS),
+                                R.id.update_menu_id)
+                        .with(AppMenuItemProperties.TITLE, mContext.getString(R.string.menu_update))
+                        .with(
+                                AppMenuItemProperties.ICON,
+                                AppCompatResources.getDrawable(mContext, R.drawable.menu_update))
+                        .build();
+        updateUpdateItemData(model);
+        return new MVCListAdapter.ListItem(TabbedAppMenuItemType.UPDATE_ITEM, model);
+    }
+
+    private void updateUpdateItemData(PropertyModel model) {
+        MenuItemState itemState =
                 UpdateMenuItemHelper.getInstance(mTabModelSelector.getModel(false).getProfile())
                         .getUiState()
-                        .itemState);
-        return new MVCListAdapter.ListItem(TabbedAppMenuItemType.UPDATE_ITEM, model);
+                        .itemState;
+        if (itemState == null) {
+            assert false : "The update state should be non-null";
+            model.set(AppMenuItemProperties.ENABLED, false);
+            return;
+        }
+        model.set(UpdateMenuItemViewBinder.SUMMARY, itemState.summary);
+        model.set(AppMenuItemProperties.TITLE, mContext.getString(itemState.title));
+        model.set(UpdateMenuItemViewBinder.TITLE_COLOR_ID, itemState.titleColorId);
+        Drawable icon = null;
+        if (itemState.icon != 0) {
+            icon = AppCompatResources.getDrawable(mContext, itemState.icon);
+        }
+        if (icon != null && itemState.iconTintId != 0) {
+            DrawableCompat.setTint(icon, mContext.getColor(itemState.iconTintId));
+        }
+        model.set(AppMenuItemProperties.ICON, icon);
+        model.set(AppMenuItemProperties.ENABLED, itemState.enabled);
     }
 
     private MVCListAdapter.ListItem buildNewTabItem() {
@@ -944,9 +992,9 @@
                     UpdateMenuItemHelper.getInstance(
                             mTabModelSelector.getModel(false).getProfile());
             updateHelper.onMenuDismissed();
-            updateHelper.unregisterObserver(mAppMenuInvalidator);
+            updateHelper.unregisterObserver(mUpdateStateChangeObserver);
             mUpdateMenuItemVisible = false;
-            mAppMenuInvalidator = null;
+            mUpdateStateChangeObserver = null;
         }
     }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/UpdateMenuItemViewBinder.java b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/UpdateMenuItemViewBinder.java
index 5b4d089..d23f1e7 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/UpdateMenuItemViewBinder.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/UpdateMenuItemViewBinder.java
@@ -12,12 +12,9 @@
 import android.widget.TextView;
 
 import androidx.appcompat.content.res.AppCompatResources;
-import androidx.core.graphics.drawable.DrawableCompat;
 
 import org.chromium.build.annotations.NullMarked;
-import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.R;
-import org.chromium.chrome.browser.toolbar.menu_button.MenuItemState;
 import org.chromium.chrome.browser.ui.appmenu.AppMenuItemProperties;
 import org.chromium.chrome.browser.ui.appmenu.AppMenuUtil;
 import org.chromium.ui.modelutil.PropertyKey;
@@ -26,65 +23,55 @@
 /** A custom binder used to bind the update menu item. */
 @NullMarked
 class UpdateMenuItemViewBinder {
+    /** Summary for the Update menu item. */
+    public static final PropertyModel.WritableObjectPropertyKey<String> SUMMARY =
+            new PropertyModel.WritableObjectPropertyKey<>("SUMMARY");
+
+    /** The color to be applied to the title text. */
+    public static final PropertyModel.WritableIntPropertyKey TITLE_COLOR_ID =
+            new PropertyModel.WritableIntPropertyKey("TITLE_COLOR_ID");
+
+    /** All the applicable property keys for the update menu item. */
+    public static final PropertyKey[] ALL_KEYS =
+            PropertyModel.concatKeys(
+                    AppMenuItemProperties.ALL_KEYS, new PropertyKey[] {SUMMARY, TITLE_COLOR_ID});
+
     /** Handles binding the view and models changes. */
     public static void bind(PropertyModel model, View view, PropertyKey key) {
         AppMenuUtil.bindStandardItemEnterAnimation(model, view, key);
 
-        @Nullable MenuItemState itemState =
-                ((MenuItemState) model.get(AppMenuItemProperties.CUSTOM_ITEM_DATA));
-
         if (key == AppMenuItemProperties.MENU_ITEM_ID) {
             int id = model.get(AppMenuItemProperties.MENU_ITEM_ID);
             assert id == R.id.update_menu_id;
             view.setId(id);
-
-            if (itemState != null) {
-                TextView summary = view.findViewById(R.id.menu_item_summary);
-                if (!TextUtils.isEmpty(itemState.summary)) {
-                    summary.setText(itemState.summary);
-                    summary.setVisibility(View.VISIBLE);
-                } else {
-                    summary.setText("");
-                    summary.setVisibility(View.GONE);
-                }
+        } else if (key == SUMMARY) {
+            TextView summary = view.findViewById(R.id.menu_item_summary);
+            String summaryText = model.get(SUMMARY);
+            if (!TextUtils.isEmpty(summaryText)) {
+                summary.setText(summaryText);
+                summary.setVisibility(View.VISIBLE);
+            } else {
+                summary.setText("");
+                summary.setVisibility(View.GONE);
             }
         } else if (key == AppMenuItemProperties.TITLE) {
             TextView text = view.findViewById(R.id.menu_item_text);
-            if (itemState == null) {
-                text.setText(model.get(AppMenuItemProperties.TITLE));
-            } else {
-                text.setText(itemState.title);
-                text.setTextColor(
-                        AppCompatResources.getColorStateList(
-                                view.getContext(), itemState.titleColorId));
-            }
-        } else if (key == AppMenuItemProperties.TITLE_CONDENSED) {
+            text.setText(model.get(AppMenuItemProperties.TITLE));
+            text.setContentDescription(model.get(AppMenuItemProperties.TITLE));
+        } else if (key == TITLE_COLOR_ID) {
             TextView text = view.findViewById(R.id.menu_item_text);
-            if (itemState == null) {
-                CharSequence titleCondensed = model.get(AppMenuItemProperties.TITLE_CONDENSED);
-                text.setContentDescription(titleCondensed);
-            } else {
-                text.setContentDescription(view.getResources().getString(itemState.title));
-            }
+            text.setTextColor(
+                    AppCompatResources.getColorStateList(
+                            view.getContext(), model.get(TITLE_COLOR_ID)));
         } else if (key == AppMenuItemProperties.ICON) {
             ImageView image = view.findViewById(R.id.menu_item_icon);
-
-            if (itemState == null) {
-                Drawable icon = model.get(AppMenuItemProperties.ICON);
-                image.setImageDrawable(icon);
-                image.setVisibility(View.VISIBLE);
-                return;
-            }
-
-            image.setImageResource(itemState.icon);
-            if (itemState.iconTintId != 0) {
-                DrawableCompat.setTint(
-                        image.getDrawable(), view.getContext().getColor(itemState.iconTintId));
-            }
+            Drawable icon = model.get(AppMenuItemProperties.ICON);
+            image.setVisibility(icon == null ? View.GONE : View.VISIBLE);
+            image.setImageDrawable(icon);
         } else if (key == AppMenuItemProperties.ENABLED) {
             view.findViewById(R.id.menu_item_text)
                     .setEnabled(model.get(AppMenuItemProperties.ENABLED));
-            if (itemState != null) view.setEnabled(itemState.enabled);
+            view.setEnabled(model.get(AppMenuItemProperties.ENABLED));
         } else if (key == AppMenuItemProperties.CLICK_HANDLER) {
             view.setOnClickListener(
                     v -> model.get(AppMenuItemProperties.CLICK_HANDLER).onItemClick(model));
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/partnercustomizations/PartnerDisableIncognitoModeIntegrationTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/partnercustomizations/PartnerDisableIncognitoModeIntegrationTest.java
index f710140..4f8c3af3 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/partnercustomizations/PartnerDisableIncognitoModeIntegrationTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/partnercustomizations/PartnerDisableIncognitoModeIntegrationTest.java
@@ -63,10 +63,7 @@
                         () ->
                                 AppMenuTestSupport.getAppMenuPropertiesDelegate(
                                                 mActivityTestRule.getAppMenuCoordinator())
-                                        .getMenuItems(
-                                                mActivityTestRule
-                                                        .getAppMenuCoordinator()
-                                                        .getAppMenuHandler()));
+                                        .getMenuItems());
         MVCListAdapter.ListItem newIncognitoItem = null;
         for (MVCListAdapter.ListItem item : modelList) {
             if (item.model.get(AppMenuItemProperties.MENU_ITEM_ID)
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java
index d5e481a..e49710a 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java
@@ -68,7 +68,6 @@
 import org.chromium.chrome.browser.toolbar.menu_button.MenuUiState;
 import org.chromium.chrome.browser.translate.TranslateBridge;
 import org.chromium.chrome.browser.translate.TranslateBridgeJni;
-import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
 import org.chromium.chrome.browser.ui.appmenu.AppMenuItemProperties;
 import org.chromium.chrome.browser.webapps.WebappRegistry;
 import org.chromium.components.bookmarks.BookmarkId;
@@ -211,8 +210,7 @@
                                 mBookmarkModelSupplier,
                                 mReadAloudControllerSupplier) {
                             @Override
-                            public MVCListAdapter.ModelList buildMenuModelList(
-                                    AppMenuHandler handler) {
+                            public MVCListAdapter.ModelList buildMenuModelList() {
                                 return new MVCListAdapter.ModelList();
                             }
                         });
@@ -466,16 +464,14 @@
     public void readAloud_CanBeAddedOnMultipleCreatedMenus() {
         when(mReadAloudController.isReadable(any(Tab.class))).thenReturn(true);
 
-        MVCListAdapter.ModelList modelList =
-                mAppMenuPropertiesDelegate.getMenuItems(mock(AppMenuHandler.class));
+        MVCListAdapter.ModelList modelList = mAppMenuPropertiesDelegate.getMenuItems();
         mAppMenuPropertiesDelegate.observeAndMaybeAddReadAloud(modelList, mTab);
         Assert.assertEquals(1, modelList.size());
         Assert.assertEquals(
                 R.id.readaloud_menu_id,
                 modelList.get(0).model.get(AppMenuItemProperties.MENU_ITEM_ID));
 
-        MVCListAdapter.ModelList modelList2 =
-                mAppMenuPropertiesDelegate.getMenuItems(mock(AppMenuHandler.class));
+        MVCListAdapter.ModelList modelList2 = mAppMenuPropertiesDelegate.getMenuItems();
         mAppMenuPropertiesDelegate.observeAndMaybeAddReadAloud(modelList2, mTab);
         Assert.assertEquals(1, modelList2.size());
         Assert.assertEquals(
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabAppMenuPropertiesDelegateUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabAppMenuPropertiesDelegateUnitTest.java
index 9c608683..82345000 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabAppMenuPropertiesDelegateUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabAppMenuPropertiesDelegateUnitTest.java
@@ -164,7 +164,7 @@
                         mReadAloudControllerSupplier,
                         /* contextualPageActionControllerSupplier */ () -> null,
                         /* hasClientPackage= */ false);
-        MVCListAdapter.ModelList modelList = delegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = delegate.getMenuItems();
         assertTrue(isMenuItemPresent(modelList, R.id.enable_price_tracking_menu_id));
         assertFalse(isMenuItemPresent(modelList, R.id.disable_price_tracking_menu_id));
     }
@@ -200,7 +200,7 @@
                         mReadAloudControllerSupplier,
                         () -> cpac,
                         /* hasClientPackage= */ false);
-        MVCListAdapter.ModelList modelList = delegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = delegate.getMenuItems();
         assertTrue(isMenuItemPresent(modelList, R.id.price_insights_menu_id));
     }
 
@@ -231,7 +231,7 @@
                         mReadAloudControllerSupplier,
                         /* contextualPageActionControllerSupplier */ () -> null,
                         /* hasClientPackage= */ false);
-        MVCListAdapter.ModelList modelList = delegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = delegate.getMenuItems();
 
         assertTrue(isMenuItemPresent(modelList, R.id.find_in_page_id));
 
@@ -270,7 +270,7 @@
                         mReadAloudControllerSupplier,
                         /* contextualPageActionControllerSupplier */ () -> null,
                         /* hasClientPackage= */ false);
-        MVCListAdapter.ModelList modelList = delegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = delegate.getMenuItems();
 
         assertTrue(isMenuItemPresent(modelList, R.id.find_in_page_id));
 
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbarUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbarUnitTest.java
index 644d232..95d1355 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbarUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/CustomTabToolbarUnitTest.java
@@ -15,6 +15,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -52,6 +53,8 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnit;
@@ -98,6 +101,8 @@
 import org.chromium.chrome.browser.toolbar.top.NavigationPopup.HistoryDelegate;
 import org.chromium.chrome.browser.toolbar.top.ToggleTabStackButtonCoordinator;
 import org.chromium.chrome.browser.toolbar.top.ToolbarSnapshotDifference;
+import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
+import org.chromium.chrome.browser.ui.appmenu.AppMenuObserver;
 import org.chromium.chrome.browser.user_education.UserEducationHelper;
 import org.chromium.components.content_settings.CookieBlocking3pcdStatus;
 import org.chromium.components.content_settings.CookieControlsState;
@@ -145,10 +150,12 @@
     @Mock Callback<Integer> mContainerVisibilityChangeObserver;
     @Mock View mParentView;
     @Mock WindowAndroid mWindowAndroid;
+    @Mock AppMenuHandler mAppMenuHandler;
     private @Mock PageInfoIphController mPageInfoIphController;
     @Mock private CustomTabFeatureOverridesManager mFeatureOverridesManager;
     @Mock private BrowserServicesIntentDataProvider mIntentDataProvider;
     @Mock private CustomTabMinimizeDelegate mMinimizeDelegate;
+    @Captor ArgumentCaptor<AppMenuObserver> mAppMenuObserverCaptor;
 
     private Activity mActivity;
     private CustomTabToolbar mToolbar;
@@ -212,7 +219,7 @@
                 null,
                 /* homeButtonDisplay= */ null);
         if (!ChromeFeatureList.sCctToolbarRefactor.isEnabled()) {
-            mToolbar.calculateToolbarWidthBeforeMeasure(mActivity, mIntentDataProvider);
+            mToolbar.initVisibilityRule(mActivity, () -> mAppMenuHandler, mIntentDataProvider);
             mToolbar.setFeatureOverridesManager(mFeatureOverridesManager);
         }
         mLocationBar =
@@ -662,7 +669,7 @@
         var connection = spy(CustomTabsConnection.getInstance());
         Mockito.doReturn(true).when(connection).shouldEnableOmniboxForIntent(any());
         CustomTabsConnection.setInstanceForTesting(connection);
-        mToolbar.updateOptionalButton(getDataForTranslateIconButton());
+        mToolbar.updateOptionalButton(getDataForPriceInsightsIconButton());
         assertNull(mToolbar.getOptionalButtonCoordinatorForTesting());
         assertEquals(View.GONE, mToolbar.findViewById(R.id.menu_dot).getVisibility());
     }
@@ -680,9 +687,17 @@
                 "bookmark",
                 mock(OnClickListener.class),
                 ButtonType.OTHER);
-        mToolbar.updateOptionalButton(getDataForTranslateIconButton());
+        mToolbar.updateOptionalButton(getDataForPriceInsightsIconButton());
         assertNull(mToolbar.getOptionalButtonCoordinatorForTesting());
         assertEquals(View.VISIBLE, mToolbar.findViewById(R.id.menu_dot).getVisibility());
+        verify(mAppMenuHandler).addObserver(mAppMenuObserverCaptor.capture());
+
+        // Verify that the corresponding menu item gets highlighted.
+        verify(mAppMenuHandler).setMenuHighlight(eq(R.id.price_insights_menu_id), eq(false));
+
+        // Verify the menu dot disappears as the overflow menu show up.
+        mAppMenuObserverCaptor.getValue().onMenuVisibilityChanged(/* isVisible= */ true);
+        assertEquals(View.GONE, mToolbar.findViewById(R.id.menu_dot).getVisibility());
     }
 
     private void assertUrlAndTitleVisible(boolean titleVisible, boolean urlVisible) {
@@ -730,11 +745,12 @@
         Mockito.doReturn(UrlBarData.forUrl(url)).when(mLocationBarModel).getUrlBarData();
     }
 
-    private ButtonDataImpl getDataForTranslateIconButton() {
-        Drawable iconDrawable = AppCompatResources.getDrawable(mActivity, R.drawable.ic_translate);
+    private ButtonDataImpl getDataForPriceInsightsIconButton() {
+        Drawable iconDrawable =
+                AppCompatResources.getDrawable(mActivity, R.drawable.ic_trending_down_24dp);
         OnClickListener clickListener = mock(OnClickListener.class);
         OnLongClickListener longClickListener = mock(OnLongClickListener.class);
-        String contentDescription = mActivity.getString(R.string.menu_translate);
+        String contentDescription = mActivity.getString(R.string.price_insights_title);
 
         // Whether a button is static or dynamic is determined by the button variant.
         ButtonSpec buttonSpec =
@@ -745,7 +761,7 @@
                         contentDescription,
                         true,
                         null,
-                        /* buttonVariant= */ AdaptiveToolbarButtonVariant.TRANSLATE,
+                        /* buttonVariant= */ AdaptiveToolbarButtonVariant.PRICE_INSIGHTS,
                         /* actionChipLabelResId= */ Resources.ID_NULL,
                         /* tooltipTextResId= */ Resources.ID_NULL,
                         /* hasErrorBadge= */ false);
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegateUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegateUnitTest.java
index 40b9c39..8a05889 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegateUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegateUnitTest.java
@@ -80,6 +80,7 @@
 import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
 import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
 import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
+import org.chromium.chrome.browser.omaha.UpdateMenuItemHelper;
 import org.chromium.chrome.browser.pdf.PdfPage;
 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
@@ -97,6 +98,8 @@
 import org.chromium.chrome.browser.tabmodel.TabModel;
 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
 import org.chromium.chrome.browser.toolbar.ToolbarManager;
+import org.chromium.chrome.browser.toolbar.menu_button.MenuItemState;
+import org.chromium.chrome.browser.toolbar.menu_button.MenuUiState;
 import org.chromium.chrome.browser.translate.TranslateBridge;
 import org.chromium.chrome.browser.translate.TranslateBridgeJni;
 import org.chromium.chrome.browser.ui.appmenu.AppMenuDelegate;
@@ -205,8 +208,8 @@
     @Mock private PrefService mPrefService;
     @Mock private SyncService mSyncService;
     @Mock private WebFeedBridge.Natives mWebFeedBridgeJniMock;
-    @Mock private AppMenuHandler mAppMenuHandler;
     @Mock private TranslateBridge.Natives mTranslateBridgeJniMock;
+    @Mock private UpdateMenuItemHelper mUpdateMenuItemHelper;
 
     private ShadowPackageManager mShadowPackageManager;
 
@@ -220,6 +223,7 @@
             new ObservableSupplierImpl<>();
 
     private TabbedAppMenuPropertiesDelegate mTabbedAppMenuPropertiesDelegate;
+    private MenuUiState mUpdateAvailableMenuUiState;
 
     // Boolean flags to test multi-window menu visibility for various combinations.
     private boolean mIsMultiInstance;
@@ -284,6 +288,15 @@
         Mockito.when(mTranslateBridgeJniMock.canManuallyTranslate(any(), anyBoolean()))
                 .thenReturn(false);
 
+        UpdateMenuItemHelper.setInstanceForTesting(mUpdateMenuItemHelper);
+        doReturn(new MenuUiState()).when(mUpdateMenuItemHelper).getUiState();
+
+        mUpdateAvailableMenuUiState = new MenuUiState();
+        mUpdateAvailableMenuUiState.itemState = new MenuItemState();
+        mUpdateAvailableMenuUiState.itemState.title = R.string.menu_update;
+        mUpdateAvailableMenuUiState.itemState.titleColorId = R.color.default_text_color_error;
+        mUpdateAvailableMenuUiState.itemState.icon = R.drawable.menu_update;
+
         PowerBookmarkUtils.setPriceTrackingEligibleForTesting(false);
         PowerBookmarkUtils.setPowerBookmarkMetaForTesting(PowerBookmarkMeta.newBuilder().build());
         TabbedAppMenuPropertiesDelegate delegate =
@@ -462,8 +475,7 @@
                 .shouldShowTranslateMenuItem(any(Tab.class));
 
         assertEquals(MenuGroup.PAGE_MENU, mTabbedAppMenuPropertiesDelegate.getMenuGroup());
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         Integer[] expectedItems = {
             R.id.icon_row_menu_id,
@@ -496,8 +508,7 @@
                 .shouldShowTranslateMenuItem(any(Tab.class));
 
         assertEquals(MenuGroup.PAGE_MENU, mTabbedAppMenuPropertiesDelegate.getMenuGroup());
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         Integer[] expectedItems = {
             R.id.icon_row_menu_id,
@@ -533,8 +544,7 @@
                         .withAutoDarkEnabled());
 
         assertEquals(MenuGroup.PAGE_MENU, mTabbedAppMenuPropertiesDelegate.getMenuGroup());
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         List<Integer> expectedItems = new ArrayList<>();
         List<Integer> expectedTitles = new ArrayList<>();
@@ -607,8 +617,7 @@
                         .withAutoDarkEnabled());
 
         assertEquals(MenuGroup.PAGE_MENU, mTabbedAppMenuPropertiesDelegate.getMenuGroup());
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         List<Integer> expectedItems = new ArrayList<>();
         List<Integer> expectedTitles = new ArrayList<>();
@@ -680,8 +689,7 @@
                         .withAutoDarkEnabled());
 
         assertEquals(MenuGroup.PAGE_MENU, mTabbedAppMenuPropertiesDelegate.getMenuGroup());
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         Integer[] expectedItems = {
             R.id.icon_row_menu_id,
@@ -716,8 +724,7 @@
         doReturn(false).when(mTabbedAppMenuPropertiesDelegate).shouldShowIconBeforeItem();
 
         assertEquals(MenuGroup.PAGE_MENU, mTabbedAppMenuPropertiesDelegate.getMenuGroup());
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         Integer[] expectedItems = {R.id.update_menu_id, R.id.reader_mode_prefs_id};
         assertMenuItemsHaveIcons(modelList, expectedItems);
@@ -736,8 +743,7 @@
         doReturn(true).when(mTabbedAppMenuPropertiesDelegate).shouldShowIconBeforeItem();
 
         assertEquals(MenuGroup.PAGE_MENU, mTabbedAppMenuPropertiesDelegate.getMenuGroup());
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         Integer[] expectedItems = {
             R.id.update_menu_id,
@@ -769,8 +775,7 @@
         Assert.assertFalse(mTabbedAppMenuPropertiesDelegate.shouldShowPageMenu());
         assertEquals(MenuGroup.OVERVIEW_MODE_MENU, mTabbedAppMenuPropertiesDelegate.getMenuGroup());
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         Integer[] expectedItems = {
             R.id.new_tab_menu_id,
@@ -792,8 +797,7 @@
         Assert.assertFalse(mTabbedAppMenuPropertiesDelegate.shouldShowPageMenu());
         assertEquals(MenuGroup.OVERVIEW_MODE_MENU, mTabbedAppMenuPropertiesDelegate.getMenuGroup());
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         Integer[] expectedItems = {
             R.id.new_tab_menu_id,
@@ -818,8 +822,7 @@
                 MenuGroup.TABLET_EMPTY_MODE_MENU, mTabbedAppMenuPropertiesDelegate.getMenuGroup());
         Assert.assertFalse(mTabbedAppMenuPropertiesDelegate.shouldShowPageMenu());
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         Integer[] expectedItems = {
             R.id.new_tab_menu_id,
@@ -850,8 +853,7 @@
         ThreadUtils.hasSubtleSideEffectsSetThreadAssertsDisabledForTesting(true);
         AccessibilityState.setIsKnownScreenReaderEnabledForTesting(true);
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         ArrayList<Integer> expectedItems =
                 new ArrayList<>(
@@ -892,7 +894,7 @@
         when(mPrefService.getBoolean(Pref.ACCESSIBILITY_IMAGE_LABELS_ENABLED_ANDROID))
                 .thenReturn(true);
 
-        modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         assertEquals(
                 "Stop image descriptions",
                 findItemById(modelList, R.id.get_image_descriptions_id)
@@ -906,7 +908,7 @@
         when(mPrefService.getBoolean(Pref.ACCESSIBILITY_IMAGE_LABELS_ONLY_ON_WIFI))
                 .thenReturn(true);
 
-        modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         assertEquals(
                 "Get image descriptions",
                 findItemById(modelList, R.id.get_image_descriptions_id)
@@ -924,8 +926,7 @@
                 .when(mTabbedAppMenuPropertiesDelegate)
                 .shouldShowManagedByMenuItem(any(Tab.class));
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         ArrayList<Integer> expectedItems =
                 new ArrayList<>(
@@ -1079,8 +1080,7 @@
                 .shouldShowManagedByMenuItem(any(Tab.class));
 
         Assert.assertEquals(MenuGroup.PAGE_MENU, mTabbedAppMenuPropertiesDelegate.getMenuGroup());
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         assertTrue(isMenuVisible(modelList, R.id.managed_by_menu_id));
     }
@@ -1097,8 +1097,7 @@
         doReturn(true).when(mIncognitoReauthControllerMock).isReauthPageShowing();
         doReturn(mIncognitoTabModel).when(mTabModelSelector).getCurrentModel();
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         verify(mIncognitoReauthControllerMock).isReauthPageShowing();
 
         MVCListAdapter.ListItem item = findItemById(modelList, R.id.new_incognito_tab_menu_id);
@@ -1115,8 +1114,7 @@
                         .withAutoDarkEnabled());
 
         doReturn(mTabModel).when(mTabModelSelector).getCurrentModel();
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         verifyNoMoreInteractions(mIncognitoReauthControllerMock);
 
         MVCListAdapter.ListItem item = findItemById(modelList, R.id.new_incognito_tab_menu_id);
@@ -1130,8 +1128,7 @@
         when(mTab.getUrl()).thenReturn(JUnitTestGURLs.EXAMPLE_URL);
         doReturn(mTabModel).when(mTabModelSelector).getCurrentModel();
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         assertFalse(isMenuVisible(modelList, R.id.reader_mode_menu_id));
     }
@@ -1143,8 +1140,7 @@
         when(mTab.getUrl()).thenReturn(JUnitTestGURLs.EXAMPLE_URL);
         doReturn(mTabModel).when(mTabModelSelector).getCurrentModel();
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         assertTrue(isMenuVisible(modelList, R.id.reader_mode_menu_id));
     }
@@ -1155,8 +1151,7 @@
         prepareMocksForGroupTabsOnTabModel(mIncognitoTabModel);
         doReturn(isShowing).when(mIncognitoReauthControllerMock).isReauthPageShowing();
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         verify(mIncognitoReauthControllerMock, atLeastOnce()).isReauthPageShowing();
         return modelList;
     }
@@ -1183,8 +1178,7 @@
         when(mTabModelSelector.getCurrentModel()).thenReturn(mTabModel);
         prepareMocksForGroupTabsOnTabModel(mTabModel);
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         // Check group tabs enabled decision in regular mode doesn't depend on re-auth.
         verify(mIncognitoReauthControllerMock, times(0)).isReauthPageShowing();
 
@@ -1200,8 +1194,7 @@
 
         when(mTabModelSelector.isTabStateInitialized()).thenReturn(false);
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         // Check group tabs enabled decision in regular mode doesn't depend on re-auth.
         verify(mIncognitoReauthControllerMock, times(0)).isReauthPageShowing();
 
@@ -1217,8 +1210,7 @@
         Tab mockTab1 = mock(Tab.class);
         when(mTabModel.getTabAt(0)).thenReturn(mockTab1);
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         // Check group tabs enabled decision in regular mode doesn't depend on re-auth.
         verify(mIncognitoReauthControllerMock, times(0)).isReauthPageShowing();
 
@@ -1231,8 +1223,7 @@
         setUpMocksForOverviewMenu();
         when(mTabModelSelector.getCurrentModel()).thenReturn(mTabModel);
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         // Check group tabs enabled decision in regular mode doesn't depend on re-auth.
         verify(mIncognitoReauthControllerMock, times(0)).isReauthPageShowing();
 
@@ -1250,8 +1241,7 @@
         setMenuOptions(new MenuOptions());
         when(mActivityTabProvider.get()).thenReturn(ntpTab);
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         MVCListAdapter.ListItem item = findItemById(modelList, R.id.ntp_customization_id);
         assertTrue(item.model.get(AppMenuItemProperties.ENABLED));
@@ -1538,8 +1528,7 @@
         mReadAloudControllerSupplier.set(null);
         when(mTab.getUrl()).thenReturn(JUnitTestGURLs.EXAMPLE_URL);
         setUpMocksForPageMenu();
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         assertFalse(isMenuVisible(modelList, R.id.readaloud_menu_id));
     }
 
@@ -1548,8 +1537,7 @@
         when(mTab.getUrl()).thenReturn(JUnitTestGURLs.EXAMPLE_URL);
         when(mReadAloudController.isReadable(any())).thenReturn(false);
         setUpMocksForPageMenu();
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         assertFalse(isMenuVisible(modelList, R.id.readaloud_menu_id));
     }
 
@@ -1558,8 +1546,7 @@
         when(mTab.getUrl()).thenReturn(JUnitTestGURLs.EXAMPLE_URL);
         when(mReadAloudController.isReadable(any())).thenReturn(true);
         setUpMocksForPageMenu();
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
         assertTrue(isMenuVisible(modelList, R.id.readaloud_menu_id));
     }
 
@@ -1572,8 +1559,7 @@
         when(mTab.getUrl()).thenReturn(JUnitTestGURLs.URL_1);
         setUpMocksForPageMenu();
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         assertTrue(
                 "AI Web menu item should be visible",
@@ -1596,8 +1582,7 @@
         when(mTab.getNativePage()).thenReturn(pdfNativePage);
         when(mTab.isNativePage()).thenReturn(true);
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         assertFalse(
                 "AI Web menu item should not be visible",
@@ -1613,8 +1598,7 @@
         when(mTab.getUrl()).thenReturn(JUnitTestGURLs.URL_1);
         setUpMocksForPageMenu();
 
-        MVCListAdapter.ModelList modelList =
-                mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        MVCListAdapter.ModelList modelList = mTabbedAppMenuPropertiesDelegate.getMenuItems();
 
         assertFalse(
                 "AI Web menu item should not be visible",
@@ -1648,7 +1632,7 @@
         when(mTab.getUrl()).thenReturn(JUnitTestGURLs.EXAMPLE_URL);
         when(mReadAloudController.isReadable(mTab)).thenReturn(initiallyReadable);
         setUpMocksForPageMenu();
-        mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        mTabbedAppMenuPropertiesDelegate.getMenuItems();
         // When menu is created, the visibility should match readability state at that time
         assertEquals(initiallyReadable, hasReadAloudInMenu());
 
@@ -1810,7 +1794,7 @@
         doReturn(mIsMoveToOtherWindowSupported)
                 .when(mMultiWindowModeStateDispatcher)
                 .isMoveToOtherWindowSupported(mTabModelSelector);
-        return mTabbedAppMenuPropertiesDelegate.getMenuItems(mAppMenuHandler);
+        return mTabbedAppMenuPropertiesDelegate.getMenuItems();
     }
 
     private void testWindowMenu(
@@ -2082,9 +2066,9 @@
         doReturn(options.showTranslate())
                 .when(mTabbedAppMenuPropertiesDelegate)
                 .shouldShowTranslateMenuItem(any(Tab.class));
-        doReturn(options.showUpdate())
-                .when(mTabbedAppMenuPropertiesDelegate)
-                .shouldShowUpdateMenuItem();
+        doReturn(options.showUpdate() ? mUpdateAvailableMenuUiState : new MenuUiState())
+                .when(mUpdateMenuItemHelper)
+                .getUiState();
         doReturn(options.showMoveToOtherWindow())
                 .when(mTabbedAppMenuPropertiesDelegate)
                 .shouldShowMoveToOtherWindow();
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 2cd42d2..11b893ea 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -4263,8 +4263,6 @@
       "upgrade_detector/build_state.cc",
       "upgrade_detector/build_state.h",
       "upgrade_detector/upgrade_detector.cc",
-      "upgrade_detector/upgrade_detector.h",
-      "upgrade_detector/upgrade_observer.h",
       "upgrade_detector/version_history_client.cc",
       "upgrade_detector/version_history_client.h",
       "usb/usb_connection_tracker.cc",
@@ -4483,6 +4481,7 @@
       "//chrome/browser/ui/webui/top_chrome:impl",
       "//chrome/browser/ui/webui/whats_new",
       "//chrome/browser/ui/webui/whats_new:impl",
+      "//chrome/browser/upgrade_detector",
       "//chrome/browser/upgrade_detector:build_state_observer",
       "//chrome/browser/web_applications",
       "//chrome/browser/web_applications:features",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 4f7b349..c61c0d2 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -6880,6 +6880,10 @@
      flag_descriptions::kLensOverlayOmniboxEntryPointDescription, kOsDesktop,
      FEATURE_VALUE_TYPE(lens::features::kLensOverlayOmniboxEntryPoint)},
 
+    {"omnibox-toolbelt", flag_descriptions::kOmniboxToolbeltName,
+     flag_descriptions::kOmniboxToolbeltDescription, kOsDesktop,
+     FEATURE_VALUE_TYPE(omnibox_feature_configs::Toolbelt::kOmniboxToolbelt)},
+
     {"omnibox-domain-suggestions",
      flag_descriptions::kOmniboxDomainSuggestionsName,
      flag_descriptions::kOmniboxDomainSuggestionsDescription, kOsDesktop,
@@ -6932,7 +6936,7 @@
 
     {"omnibox-url-suggestions-on-focus",
      flag_descriptions::kOmniboxUrlSuggestionsOnFocus,
-     flag_descriptions::kOmniboxUrlSuggestionsOnFocusDecription, kOsDesktop,
+     flag_descriptions::kOmniboxUrlSuggestionsOnFocusDescription, kOsDesktop,
      FEATURE_WITH_PARAMS_VALUE_TYPE(
          omnibox_feature_configs::OmniboxUrlSuggestionsOnFocus::
              kOmniboxUrlSuggestionsOnFocus,
diff --git a/chrome/browser/ash/app_list/app_service/app_service_app_icon_loader.cc b/chrome/browser/ash/app_list/app_service/app_service_app_icon_loader.cc
index fb05a2d..1e12ebd 100644
--- a/chrome/browser/ash/app_list/app_service/app_service_app_icon_loader.cc
+++ b/chrome/browser/ash/app_list/app_service/app_service_app_icon_loader.cc
@@ -63,7 +63,7 @@
     Profile* profile,
     int resource_size_in_dip,
     AppIconLoaderDelegate* delegate)
-    : AppIconLoader(profile, resource_size_in_dip, delegate) {
+    : AppIconLoader(resource_size_in_dip, delegate), profile_(profile) {
   app_registry_cache_observer_.Observe(
       &apps::AppServiceProxyFactory::GetForProfile(profile)
            ->AppRegistryCache());
diff --git a/chrome/browser/ash/app_list/app_service/app_service_app_icon_loader.h b/chrome/browser/ash/app_list/app_service/app_service_app_icon_loader.h
index e5af369..ecc96e41 100644
--- a/chrome/browser/ash/app_list/app_service/app_service_app_icon_loader.h
+++ b/chrome/browser/ash/app_list/app_service/app_service_app_icon_loader.h
@@ -9,6 +9,7 @@
 #include <set>
 #include <string>
 
+#include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "base/scoped_observation.h"
 #include "chrome/browser/ui/app_icon_loader.h"
@@ -59,7 +60,11 @@
   // Returns true if the app_id does exist in icon_map_.
   bool Exist(const std::string& app_id);
 
+  Profile* profile() { return profile_; }
+
  private:
+  const raw_ptr<Profile, DanglingUntriaged> profile_ = nullptr;
+
   // Maps from an app id to shelf app ids.
   AppIDToShelfAppId shelf_app_id_map_;
 
diff --git a/chrome/browser/ash/app_list/app_service/app_service_promise_app_icon_loader.cc b/chrome/browser/ash/app_list/app_service/app_service_promise_app_icon_loader.cc
index af1064c0..d00f2cd 100644
--- a/chrome/browser/ash/app_list/app_service/app_service_promise_app_icon_loader.cc
+++ b/chrome/browser/ash/app_list/app_service/app_service_promise_app_icon_loader.cc
@@ -27,7 +27,7 @@
     Profile* profile,
     int resource_size_in_dip,
     AppIconLoaderDelegate* delegate)
-    : AppIconLoader(profile, resource_size_in_dip, delegate) {
+    : AppIconLoader(resource_size_in_dip, delegate), profile_(profile) {
   promise_app_registry_cache_observation_.Observe(
       apps::AppServiceProxyFactory::GetForProfile(profile)
           ->PromiseAppRegistryCache());
diff --git a/chrome/browser/ash/app_list/app_service/app_service_promise_app_icon_loader.h b/chrome/browser/ash/app_list/app_service/app_service_promise_app_icon_loader.h
index 0ceca93ce..1c54679 100644
--- a/chrome/browser/ash/app_list/app_service/app_service_promise_app_icon_loader.h
+++ b/chrome/browser/ash/app_list/app_service/app_service_promise_app_icon_loader.h
@@ -8,6 +8,7 @@
 #include <map>
 #include <string>
 
+#include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "base/scoped_observation.h"
 #include "chrome/browser/apps/app_service/promise_apps/promise_app.h"
@@ -47,6 +48,8 @@
 
   static bool CanLoadImage(Profile* profile, const std::string& id);
 
+  Profile* profile() { return profile_; }
+
  private:
   base::ScopedObservation<apps::PromiseAppRegistryCache,
                           apps::PromiseAppRegistryCache::Observer>
@@ -60,6 +63,8 @@
   void OnLoadIcon(const apps::PackageId& package_id,
                   apps::IconValuePtr icon_value);
 
+  const raw_ptr<Profile, DanglingUntriaged> profile_ = nullptr;
+
   base::WeakPtrFactory<AppServicePromiseAppIconLoader> weak_ptr_factory_{this};
 };
 
diff --git a/chrome/browser/ash/policy/handlers/BUILD.gn b/chrome/browser/ash/policy/handlers/BUILD.gn
index 6194923..68e7e8c 100644
--- a/chrome/browser/ash/policy/handlers/BUILD.gn
+++ b/chrome/browser/ash/policy/handlers/BUILD.gn
@@ -68,6 +68,7 @@
     "//chrome/browser/ash/settings",
     "//chrome/browser/ash/tpm",
     "//chrome/browser/profiles:profile",
+    "//chrome/browser/upgrade_detector",
     "//chrome/browser/web_applications",
     "//chrome/common:chrome_features",
     "//chrome/common:constants",
diff --git a/chrome/browser/bookmarks/android/BUILD.gn b/chrome/browser/bookmarks/android/BUILD.gn
index fa85404..0ede0b5a018 100644
--- a/chrome/browser/bookmarks/android/BUILD.gn
+++ b/chrome/browser/bookmarks/android/BUILD.gn
@@ -87,7 +87,6 @@
     "java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarItemsLayoutManager.java",
     "java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarItemsProvider.java",
     "java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarProperties.java",
-    "java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarSettingProvider.java",
     "java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarUtils.java",
     "java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarViewBinder.java",
     "java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarVisibilityProvider.java",
@@ -205,7 +204,6 @@
     "junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarCoordinatorTest.java",
     "junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarItemsLayoutManagerTest.java",
     "junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarItemsProviderTest.java",
-    "junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarSettingProviderTest.java",
     "junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarUtilsTest.java",
     "junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarVisibilityProviderTest.java",
   ]
diff --git a/chrome/browser/bookmarks/android/java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarSettingProvider.java b/chrome/browser/bookmarks/android/java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarSettingProvider.java
deleted file mode 100644
index 81c39e0..0000000
--- a/chrome/browser/bookmarks/android/java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarSettingProvider.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2025 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package org.chromium.chrome.browser.bookmarks.bar;
-
-import org.chromium.base.Callback;
-import org.chromium.base.supplier.ObservableSupplier;
-import org.chromium.build.annotations.NullMarked;
-import org.chromium.build.annotations.Nullable;
-import org.chromium.chrome.browser.preferences.PrefServiceUtil;
-import org.chromium.chrome.browser.profiles.Profile;
-import org.chromium.components.prefs.PrefChangeRegistrar;
-
-/**
- * A provider which observes and propagates events on changes to the bookmark bar user setting in a
- * profile-agnostic way. This circumvents the need for clients to explicitly observe both profile
- * and preference change events in order to track the current state of the bookmark bar user
- * setting.
- */
-@NullMarked
-public class BookmarkBarSettingProvider {
-
-    private final Callback<Boolean> mCallback;
-    private final ObservableSupplier<Profile> mProfileSupplier;
-    private final Callback<Profile> mProfileSupplierObserver;
-
-    private @Nullable PrefChangeRegistrar mPrefChangeRegistrar;
-
-    /**
-     * Constructor.
-     *
-     * @param profileSupplier The supplier of the profile for which to observe the user setting.
-     * @param callback The callback to notify of changes to the user setting.
-     */
-    public BookmarkBarSettingProvider(
-            ObservableSupplier<Profile> profileSupplier, Callback<Boolean> callback) {
-        mCallback = callback;
-
-        mProfileSupplier = profileSupplier;
-        mProfileSupplierObserver = this::onProfileChange;
-        mProfileSupplier.addObserver(mProfileSupplierObserver);
-    }
-
-    /** Destroys the setting provider. */
-    public void destroy() {
-        destroyPrefChangeRegistrar();
-        mProfileSupplier.removeObserver(mProfileSupplierObserver);
-    }
-
-    private void onProfileChange(@Nullable Profile profile) {
-        destroyPrefChangeRegistrar();
-
-        if (profile != null) {
-            mPrefChangeRegistrar = PrefServiceUtil.createFor(profile);
-            BookmarkBarUtils.addSettingObserver(mPrefChangeRegistrar, this::updateSetting);
-        }
-
-        // The callback is now called directly twice - once when registering the observer above,
-        // and again here to cover null |profile| cases.
-        // TODO(crbug.com/424456738): Update setting provider to not have extra calls.
-        updateSetting();
-    }
-
-    private void destroyPrefChangeRegistrar() {
-        if (mPrefChangeRegistrar != null) {
-            mPrefChangeRegistrar.destroy();
-            mPrefChangeRegistrar = null;
-        }
-    }
-
-    private void updateSetting() {
-        mCallback.onResult(BookmarkBarUtils.isSettingEnabled(mProfileSupplier.get()));
-    }
-}
diff --git a/chrome/browser/bookmarks/android/java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarVisibilityProvider.java b/chrome/browser/bookmarks/android/java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarVisibilityProvider.java
index ace061b..008714e3 100644
--- a/chrome/browser/bookmarks/android/java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarVisibilityProvider.java
+++ b/chrome/browser/bookmarks/android/java/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarVisibilityProvider.java
@@ -9,17 +9,26 @@
 
 import androidx.annotation.NonNull;
 
+import org.chromium.base.Callback;
 import org.chromium.base.ObserverList;
 import org.chromium.base.supplier.ObservableSupplier;
 import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
 import org.chromium.chrome.browser.bookmarks.R;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
 import org.chromium.chrome.browser.lifecycle.ConfigurationChangedObserver;
+import org.chromium.chrome.browser.preferences.Pref;
+import org.chromium.chrome.browser.preferences.PrefServiceUtil;
 import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.components.prefs.PrefChangeRegistrar;
+import org.chromium.components.prefs.PrefChangeRegistrar.PrefObserver;
 
 /**
- * A provider which observes changes to device configuration state and the bookmark bar user setting
- * in order to propagate visibility change events.
+ * A provider which observes changes to device configuration state and the Bookmark Bar user setting
+ * in order to propagate visibility change events. This class allows observers to get notifications
+ * of changes in visibility in a Profile-agnostic way. This circumvents the need for clients to
+ * explicitly observe both profile and preference change events, as well as the Configuration, in
+ * order to track the current visibility state of the Bookmark Bar.
  */
 @NullMarked
 public class BookmarkBarVisibilityProvider {
@@ -49,9 +58,12 @@
     private final ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
     private final ConfigurationChangedObserver mConfigurationChangedListener;
     private final ObservableSupplier<Profile> mProfileSupplier;
-    private final BookmarkBarSettingProvider mSettingProvider;
+    private final Callback<Profile> mProfileSupplierObserver;
     private final ObserverList<BookmarkBarVisibilityObserver> mObservers;
 
+    private @Nullable PrefChangeRegistrar mPrefChangeRegistrar;
+    private final @Nullable PrefObserver mPrefObserver;
+
     /**
      * Constructor.
      *
@@ -72,8 +84,16 @@
         mConfigurationChangedListener = this::processConfigurationChange;
         mActivityLifecycleDispatcher.register(mConfigurationChangedListener);
 
-        mSettingProvider =
-                new BookmarkBarSettingProvider(mProfileSupplier, this::processSettingChange);
+        mProfileSupplierObserver = this::processProfileChange;
+        mProfileSupplier.addObserver(mProfileSupplierObserver);
+
+        mPrefObserver =
+                new PrefObserver() {
+                    @Override
+                    public void onPreferenceChange() {
+                        processPrefChange();
+                    }
+                };
     }
 
     /**
@@ -98,11 +118,12 @@
     /** Destroys the visibility provider. */
     public void destroy() {
         mActivityLifecycleDispatcher.unregister(mConfigurationChangedListener);
-        mSettingProvider.destroy();
+        mProfileSupplier.removeObserver(mProfileSupplierObserver);
+        destroyPrefChangeRegistrar();
         mObservers.clear();
     }
 
-    private void processSettingChange(Boolean isSettingEnabled) {
+    private void notifyVisibilityChange() {
         boolean visibility = BookmarkBarUtils.isFeatureVisible(mActivity, mProfileSupplier.get());
         for (BookmarkBarVisibilityObserver observer : mObservers) {
             observer.onVisibilityChanged(visibility);
@@ -117,6 +138,38 @@
         }
 
         // Configuration changes can also result in visibility changes (e.g. window size change).
-        processSettingChange(BookmarkBarUtils.isSettingEnabled(mProfileSupplier.get()));
+        notifyVisibilityChange();
+    }
+
+    private void processProfileChange(@Nullable Profile profile) {
+        // On a profile change, we may have either received a profile for the first time, or we
+        // have received a new profile, in which case we want to destroy the previous pref change
+        // registrar and create a new one.
+        destroyPrefChangeRegistrar();
+
+        if (profile != null) {
+            mPrefChangeRegistrar = PrefServiceUtil.createFor(profile);
+            mPrefChangeRegistrar.addObserver(Pref.SHOW_BOOKMARK_BAR, this::processPrefChange);
+        }
+
+        // Profile changes can also result in visibility changes (e.g. different setting prefs).
+        notifyVisibilityChange();
+    }
+
+    private void processPrefChange() {
+        // On any pref change, we need to notify all observers of visibility change.
+        notifyVisibilityChange();
+    }
+
+    private void destroyPrefChangeRegistrar() {
+        if (mPrefChangeRegistrar != null) {
+            mPrefChangeRegistrar.removeObserver(Pref.SHOW_BOOKMARK_BAR);
+            mPrefChangeRegistrar.destroy();
+            mPrefChangeRegistrar = null;
+        }
+    }
+
+    @Nullable PrefObserver getPrefObserverForTesting() {
+        return mPrefObserver;
     }
 }
diff --git a/chrome/browser/bookmarks/android/junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarSettingProviderTest.java b/chrome/browser/bookmarks/android/junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarSettingProviderTest.java
deleted file mode 100644
index 6b8ebac..0000000
--- a/chrome/browser/bookmarks/android/junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarSettingProviderTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-// Copyright 2025 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package org.chromium.chrome.browser.bookmarks.bar;
-
-import static org.mockito.Mockito.anyBoolean;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.test.filters.SmallTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.mockito.stubbing.Answer;
-import org.robolectric.Robolectric;
-
-import org.chromium.base.Callback;
-import org.chromium.base.supplier.ObservableSupplierImpl;
-import org.chromium.base.test.BaseRobolectricTestRunner;
-import org.chromium.chrome.browser.preferences.Pref;
-import org.chromium.chrome.browser.profiles.Profile;
-import org.chromium.components.prefs.PrefChangeRegistrar.PrefObserver;
-import org.chromium.components.prefs.PrefChangeRegistrarJni;
-import org.chromium.components.prefs.PrefService;
-import org.chromium.components.user_prefs.UserPrefsJni;
-
-import java.util.HashSet;
-import java.util.Set;
-
-/** Unit tests for {@link BookmarkBarSettingProvider}. */
-@RunWith(BaseRobolectricTestRunner.class)
-public class BookmarkBarSettingProviderTest {
-
-    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
-
-    @Mock private Callback<Boolean> mCallback;
-    @Mock private PrefChangeRegistrarJni mPrefChangeRegistrarJni;
-    @Mock private PrefService mPrefService;
-    @Mock private Profile mProfile;
-    @Mock private UserPrefsJni mUserPrefsJni;
-
-    private final ObservableSupplierImpl<Profile> mProfileSupplier = new ObservableSupplierImpl<>();
-    private final Set<PrefObserver> mSettingObserverCache = new HashSet<>();
-    private final ObservableSupplierImpl<Boolean> mSettingSupplier = new ObservableSupplierImpl<>();
-
-    private @Nullable BookmarkBarSettingProvider mProvider;
-
-    @Before
-    public void setUp() {
-        mProfileSupplier.set(mProfile);
-
-        // Set up mocks.
-        when(mProfile.getOriginalProfile()).thenReturn(mProfile);
-        when(mUserPrefsJni.get(mProfile)).thenReturn(mPrefService);
-
-        // Set up natives.
-        PrefChangeRegistrarJni.setInstanceForTesting(mPrefChangeRegistrarJni);
-        UserPrefsJni.setInstanceForTesting(mUserPrefsJni);
-
-        // Cache setting observers.
-        BookmarkBarUtils.setSettingObserverCacheForTesting(mSettingObserverCache);
-
-        // Update setting and notify observers when profile changes.
-        mProfileSupplier.addSyncObserver(
-                profile -> {
-                    final boolean enabled = profile != null ? mSettingSupplier.get() : false;
-                    BookmarkBarUtils.setSettingEnabledForTesting(enabled);
-                    mSettingObserverCache.stream().forEach(PrefObserver::onPreferenceChange);
-                });
-
-        // Update setting and notify observers when supplier changes.
-        mSettingSupplier.addObserver(
-                enabled -> {
-                    BookmarkBarUtils.setSettingEnabledForTesting(enabled);
-                    mSettingObserverCache.stream().forEach(PrefObserver::onPreferenceChange);
-                });
-
-        // Update supplier when setting changes.
-        doAnswer(runCallbackWithValueAtIndex(mSettingSupplier::set, 1))
-                .when(mPrefService)
-                .setBoolean(eq(Pref.SHOW_BOOKMARK_BAR), anyBoolean());
-    }
-
-    @After
-    public void tearDown() {
-        PrefChangeRegistrarJni.setInstanceForTesting(null);
-        UserPrefsJni.setInstanceForTesting(null);
-    }
-
-    @Test
-    @SmallTest
-    public void testConstructAndDestroy() {
-        // Case: Construct w/ setting disabled.
-        mSettingSupplier.set(false);
-        mProvider = new BookmarkBarSettingProvider(mProfileSupplier, mCallback);
-        Robolectric.flushForegroundThreadScheduler();
-        verify(mCallback).onResult(false);
-        verifyNoMoreInteractions(mCallback);
-        clearInvocations(mCallback);
-
-        // Case: Destroy w/ setting disabled.
-        mProvider.destroy();
-        verifyNoMoreInteractions(mCallback);
-
-        // Case: Construct w/ setting enabled.
-        mSettingSupplier.set(true);
-        mProvider = new BookmarkBarSettingProvider(mProfileSupplier, mCallback);
-        Robolectric.flushForegroundThreadScheduler();
-        verify(mCallback, times(2)).onResult(true);
-        clearInvocations(mCallback);
-
-        // Case: Destroy w/ setting enabled.
-        mProvider.destroy();
-        verifyNoMoreInteractions(mCallback);
-    }
-
-    @Test
-    @SmallTest
-    public void testProfileChange() {
-        // Set up.
-        mSettingSupplier.set(true);
-        mProvider = new BookmarkBarSettingProvider(mProfileSupplier, mCallback);
-        Robolectric.flushForegroundThreadScheduler();
-        verify(mCallback).onResult(true);
-        verifyNoMoreInteractions(mCallback);
-        clearInvocations(mCallback);
-
-        // Case: Profile changed to `null`.
-        mProfileSupplier.set(null);
-        verify(mCallback, times(2)).onResult(false);
-        clearInvocations(mCallback);
-
-        // Case: Profile changed from `null`.
-        mProfileSupplier.set(mProfile);
-        verify(mCallback, times(2)).onResult(true);
-    }
-
-    @Test
-    @SmallTest
-    public void testSettingChange() {
-        // Set up.
-        mSettingSupplier.set(false);
-        mProvider = new BookmarkBarSettingProvider(mProfileSupplier, mCallback);
-        Robolectric.flushForegroundThreadScheduler();
-        verify(mCallback).onResult(false);
-        verifyNoMoreInteractions(mCallback);
-        clearInvocations(mCallback);
-
-        // Case: Setting changed to enabled.
-        mSettingSupplier.set(true);
-        verify(mCallback).onResult(true);
-        verifyNoMoreInteractions(mCallback);
-        clearInvocations(mCallback);
-
-        // Case: Setting changed to disabled.
-        mSettingSupplier.set(false);
-        verify(mCallback).onResult(false);
-        verifyNoMoreInteractions(mCallback);
-    }
-
-    private @NonNull <T> Answer<Void> runCallbackWithValueAtIndex(
-            @NonNull Callback<T> callback, int index) {
-        return invocation -> {
-            final T value = invocation.getArgument(index);
-            callback.onResult(value);
-            return null;
-        };
-    }
-}
diff --git a/chrome/browser/bookmarks/android/junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarVisibilityProviderTest.java b/chrome/browser/bookmarks/android/junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarVisibilityProviderTest.java
index 22f9e8c7..5585cf9 100644
--- a/chrome/browser/bookmarks/android/junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarVisibilityProviderTest.java
+++ b/chrome/browser/bookmarks/android/junit/src/org/chromium/chrome/browser/bookmarks/bar/BookmarkBarVisibilityProviderTest.java
@@ -21,7 +21,7 @@
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
-import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -34,12 +34,15 @@
 
 import org.chromium.base.supplier.ObservableSupplierImpl;
 import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.browser.bookmarks.bar.BookmarkBarVisibilityProvider.BookmarkBarVisibilityObserver;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
 import org.chromium.chrome.browser.lifecycle.ConfigurationChangedObserver;
 import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.components.prefs.PrefChangeRegistrar;
 import org.chromium.components.prefs.PrefChangeRegistrar.PrefObserver;
 import org.chromium.components.prefs.PrefChangeRegistrarJni;
 import org.chromium.components.prefs.PrefService;
+import org.chromium.components.user_prefs.UserPrefs;
 import org.chromium.components.user_prefs.UserPrefsJni;
 
 import java.util.HashSet;
@@ -55,11 +58,11 @@
     @Mock private Resources mResources;
     @Mock private ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
     @Mock private Configuration mConfig;
-    @Mock private PrefChangeRegistrarJni mPrefChangeRegistrarJni;
+    @Mock private PrefChangeRegistrar.Natives mPrefChangeRegistrarJni;
     @Mock private PrefService mPrefService;
     @Mock private Profile mProfile;
-    @Mock private UserPrefsJni mUserPrefsJni;
-    @Mock private BookmarkBarVisibilityProvider.BookmarkBarVisibilityObserver mObserver;
+    @Mock private UserPrefs.Natives mUserPrefsJni;
+    @Mock private BookmarkBarVisibilityObserver mObserver;
 
     private final Set<ConfigurationChangedObserver> mConfigChangeObserverCache = new HashSet<>();
     private final ObservableSupplierImpl<Profile> mProfileSupplier = new ObservableSupplierImpl<>();
@@ -91,8 +94,8 @@
         BookmarkBarUtils.setSettingObserverCacheForTesting(mSettingObserverCache);
     }
 
-    @After
-    public void tearDown() {
+    @AfterClass
+    public static void tearDown() {
         PrefChangeRegistrarJni.setInstanceForTesting(null);
         UserPrefsJni.setInstanceForTesting(null);
     }
@@ -183,7 +186,7 @@
 
     @Test
     @SmallTest
-    public void testSettingChange() {
+    public void testPrefChange() {
         // Set up.
         BookmarkBarUtils.setFeatureAllowedForTesting(true);
         BookmarkBarUtils.setSettingEnabledForTesting(true);
@@ -207,6 +210,32 @@
         provider.destroy();
     }
 
+    @Test
+    @SmallTest
+    public void testProfileChange() {
+        // Set up.
+        BookmarkBarUtils.setFeatureAllowedForTesting(true);
+        BookmarkBarUtils.setSettingEnabledForTesting(true);
+        BookmarkBarVisibilityProvider provider = createProvider();
+        Robolectric.flushForegroundThreadScheduler();
+
+        // Case: Profile changed to `null`
+        BookmarkBarUtils.setSettingEnabledForTesting(false);
+        mProfileSupplier.set(null);
+        verify(mObserver, times(1)).onVisibilityChanged(false);
+        verify(mObserver, never()).onMaxWidthChanged(anyInt());
+        clearInvocations(mObserver);
+
+        // Case: Profile changed from `null`
+        BookmarkBarUtils.setSettingEnabledForTesting(true);
+        mProfileSupplier.set(mProfile);
+        verify(mObserver, times(1)).onVisibilityChanged(true);
+        verify(mObserver, never()).onMaxWidthChanged(anyInt());
+
+        // Clean up.
+        provider.destroy();
+    }
+
     private @NonNull <T> Answer<Void> addValueAtIndexToSet(@NonNull Set<T> set, int index) {
         return invocation -> {
             final T value = invocation.getArgument(index);
@@ -220,6 +249,7 @@
                 new BookmarkBarVisibilityProvider(
                         mActivity, mActivityLifecycleDispatcher, mProfileSupplier);
         provider.addObserver(mObserver);
+        mSettingObserverCache.add(provider.getPrefObserverForTesting());
         return provider;
     }
 
diff --git a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerUiConfig.java b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerUiConfig.java
index 964dc0f..a64e293 100644
--- a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerUiConfig.java
+++ b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerUiConfig.java
@@ -58,6 +58,9 @@
      */
     public final boolean showDangerousItems;
 
+    /** Whether need to focus on search box at first. */
+    public final boolean autoFocusSearchBox;
+
     /** Constructor. */
     private DownloadManagerUiConfig(Builder builder) {
         otrProfileId = builder.mOtrProfileId;
@@ -70,6 +73,7 @@
         showPaginationHeaders = builder.mShowPaginationHeaders;
         startWithPrefetchedContent = builder.mStartWithPrefetchedContent;
         showDangerousItems = builder.mShowDangerousItems;
+        autoFocusSearchBox = builder.mAutoFocusSearchBox;
     }
 
     /** Helper class for building a {@link DownloadManagerUiConfig}. */
@@ -88,6 +92,7 @@
         private boolean mShowPaginationHeaders;
         private boolean mStartWithPrefetchedContent;
         private boolean mShowDangerousItems;
+        private boolean mAutoFocusSearchBox;
 
         public Builder() {
             mSupportFullWidthImages =
@@ -146,6 +151,11 @@
             return this;
         }
 
+        public Builder setAutoFocusSearchBox(boolean autoFocusSearchBox) {
+            mAutoFocusSearchBox = autoFocusSearchBox;
+            return this;
+        }
+
         public DownloadManagerUiConfig build() {
             return new DownloadManagerUiConfig(this);
         }
diff --git a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerUiConfigHelper.java b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerUiConfigHelper.java
index ea1bb9c3..d3696a5 100644
--- a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerUiConfigHelper.java
+++ b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerUiConfigHelper.java
@@ -6,6 +6,7 @@
 
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
+import org.chromium.ui.base.DeviceFormFactor;
 
 /** Helper class to build default or base {@link DownloadManagerUiConfig.Builder} instances. */
 @NullMarked
@@ -19,6 +20,7 @@
                         && !ChromeFeatureList.sMaliciousApkDownloadCheckTelemetryOnly.getValue();
         return new DownloadManagerUiConfig.Builder()
                 .setShowDangerousItems(showDangerousItems)
-                .setSupportsGrouping(true);
+                .setSupportsGrouping(true)
+                .setAutoFocusSearchBox(DeviceFormFactor.isTablet());
     }
 }
diff --git a/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/DownloadActivityV2Test.java b/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/DownloadActivityV2Test.java
index f72a0af..cfc3126a 100644
--- a/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/DownloadActivityV2Test.java
+++ b/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/DownloadActivityV2Test.java
@@ -33,6 +33,7 @@
 import android.view.View;
 
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import androidx.test.espresso.Espresso;
 import androidx.test.espresso.action.ViewActions;
 import androidx.test.espresso.contrib.RecyclerViewActions;
 import androidx.test.espresso.matcher.BoundedMatcher;
@@ -195,15 +196,16 @@
     }
 
     private void setUpUi() {
-        setUpUi(false);
+        setUpUi(/* showDangerousItems= */ false, /* autoFocusSearchBox= */ false);
     }
 
-    private void setUpUi(boolean showDangerousItems) {
+    private void setUpUi(boolean showDangerousItems, boolean autoFocusSearchBox) {
         DownloadManagerUiConfig config =
                 DownloadManagerUiConfigHelper.fromFlags()
                         .setOtrProfileId(null)
                         .setIsSeparateActivity(true)
                         .setShowDangerousItems(showDangerousItems)
+                        .setAutoFocusSearchBox(autoFocusSearchBox)
                         .build();
 
         mAppModalPresenter = new AppModalPresenter(sActivity);
@@ -398,7 +400,7 @@
     public void testAddRemoveDangerousItem() throws Exception {
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
-                    setUpUi(/*showDangerousItems*/ true);
+                    setUpUi(/* showDangerousItems= */ true, /* autoFocusSearchBox= */ false);
                 });
 
         String storageHeaderText = "Using 1.10 KB of";
@@ -451,7 +453,7 @@
     public void testDangerousItemNotShownDueToConfig() throws Exception {
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
-                    setUpUi(/*showDangerousItems*/ false);
+                    setUpUi(/* showDangerousItems= */ false, /* autoFocusSearchBox= */ false);
                 });
 
         String storageHeaderText = "Using 1.10 KB of";
@@ -493,7 +495,7 @@
     public void testBypassDangerousWarning() throws Exception {
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
-                    setUpUi(/*showDangerousItems*/ true);
+                    setUpUi(/* showDangerousItems= */ true, /* autoFocusSearchBox= */ false);
                 });
 
         String storageHeaderText = "Using 1.10 KB of";
@@ -549,7 +551,7 @@
     public void testWarningBypassDialogLearnMore() throws Exception {
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
-                    setUpUi(/*showDangerousItems*/ true);
+                    setUpUi(/* showDangerousItems= */ true, /* autoFocusSearchBox= */ false);
                 });
 
         // Add a dangerous item.
@@ -849,6 +851,26 @@
                 });
     }
 
+    @Test
+    @MediumTest
+    public void testDownloadsFocus() throws Exception {
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    setUpUi(/* showDangerousItems= */ false, /* autoFocusSearchBox= */ true);
+                });
+
+        onView(withText("Download")).check(doesNotExist());
+        // Check the search field is displayed
+        onView(withId(R.id.search_text)).check(matches(isDisplayed()));
+        // Check we can type in search query
+        onView(withId(R.id.search_text)).perform(ViewActions.typeText("Google"));
+        // Close keyboard first then press back to Download Page.
+        Espresso.closeSoftKeyboard();
+        Espresso.pressBack();
+        // After back to download pagem user should see the search field
+        onView(withId(R.id.search_text)).check(matches(not(isDisplayed())));
+    }
+
     /**
      * @param items The list (unsorted) of OfflineItems that could be displayed.
      * @param expectations Whether or not each item (1:1 with {@code items}) is visible.
diff --git a/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerCoordinatorImpl.java b/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerCoordinatorImpl.java
index 2c20257..4088e74 100644
--- a/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerCoordinatorImpl.java
+++ b/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/DownloadManagerCoordinatorImpl.java
@@ -98,6 +98,7 @@
                         /* listContentView= */ mListCoordinator.getView(),
                         mSelectionDelegate,
                         config.isSeparateActivity,
+                        config.autoFocusSearchBox,
                         tracker);
 
         initializeView();
diff --git a/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/toolbar/ToolbarCoordinator.java b/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/toolbar/ToolbarCoordinator.java
index c4b9bb0..564821a 100644
--- a/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/toolbar/ToolbarCoordinator.java
+++ b/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/toolbar/ToolbarCoordinator.java
@@ -96,6 +96,7 @@
             View listContentView,
             SelectionDelegate<ListItem> selectionDelegate,
             boolean hasCloseButton,
+            boolean autoFocusSearchBox,
             Tracker tracker) {
         mDelegate = delegate;
         mListActionDelegate = listActionDelegate;
@@ -115,7 +116,10 @@
         mToolbar.setOnMenuItemClickListener(this::onMenuItemClick);
         mToolbar.setFocusable(true);
         mToolbar.setListContentView(listContentView);
-
+        if (autoFocusSearchBox) {
+            // Request focus for the toolbar by default attach state listener
+            mView.addOnAttachStateChangeListener(new FocusListener());
+        }
         // TODO(crbug.com/41412009): Pass the visible group to the toolbar during initialization.
         mToolbar.initializeSearchView(
                 mSearchDelegate, R.string.download_manager_search, R.id.search_menu_id);
@@ -212,6 +216,21 @@
         }
     }
 
+    /**
+     * A listener in download page that focuses the search view when the toolbar is first attached
+     * to the window, then removes itself.
+     */
+    private class FocusListener implements View.OnAttachStateChangeListener {
+        @Override
+        public void onViewAttachedToWindow(View v) {
+            mToolbar.showSearchView(true);
+            v.removeOnAttachStateChangeListener(this);
+        }
+
+        @Override
+        public void onViewDetachedFromWindow(View v) {}
+    }
+
     // TODO(shaktisahu): May be merge toolbar shadow logic into the toolbar itself.
     private void updateShadowVisibility() {
         boolean show = mShowToolbarShadow || mToolbar.isSearching();
diff --git a/chrome/browser/extensions/BUILD.gn b/chrome/browser/extensions/BUILD.gn
index 9bbfd65..d7b86895 100644
--- a/chrome/browser/extensions/BUILD.gn
+++ b/chrome/browser/extensions/BUILD.gn
@@ -112,6 +112,8 @@
     "api/identity/launch_web_auth_flow_delegate.h",
     "api/identity/web_auth_flow_info_bar_delegate.cc",
     "api/identity/web_auth_flow_info_bar_delegate.h",
+    "api/instance_id/instance_id_api.cc",
+    "api/instance_id/instance_id_api.h",
     "api/management/chrome_management_api_delegate.h",
     "api/messaging/chrome_messaging_delegate.cc",
     "api/messaging/chrome_messaging_delegate.h",
@@ -547,6 +549,7 @@
     "//ui/shell_dialogs",
   ]
   public_deps = [
+    "//chrome/browser/upgrade_detector",
     "//components/safe_browsing/core/browser/db:util",
     "//components/signin/core/browser",
     "//extensions/buildflags",
@@ -658,8 +661,6 @@
       "api/image_writer_private/xz_extractor.h",
       "api/image_writer_private/zip_extractor.cc",
       "api/image_writer_private/zip_extractor.h",
-      "api/instance_id/instance_id_api.cc",
-      "api/instance_id/instance_id_api.h",
       "api/language_settings_private/language_settings_private_api.cc",
       "api/language_settings_private/language_settings_private_api.h",
       "api/language_settings_private/language_settings_private_delegate.cc",
diff --git a/chrome/browser/extensions/api/instance_id/instance_id_api.cc b/chrome/browser/extensions/api/instance_id/instance_id_api.cc
index 061c30d..d5368d9 100644
--- a/chrome/browser/extensions/api/instance_id/instance_id_api.cc
+++ b/chrome/browser/extensions/api/instance_id/instance_id_api.cc
@@ -13,8 +13,11 @@
 #include "chrome/common/extensions/api/instance_id.h"
 #include "components/gcm_driver/instance_id/instance_id_driver.h"
 #include "components/gcm_driver/instance_id/instance_id_profile_service.h"
+#include "extensions/buildflags/buildflags.h"
 #include "extensions/common/extension.h"
 
+static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
+
 namespace extensions {
 
 namespace {
diff --git a/chrome/browser/extensions/api/instance_id/instance_id_api.h b/chrome/browser/extensions/api/instance_id/instance_id_api.h
index 4ef2069..155fc53 100644
--- a/chrome/browser/extensions/api/instance_id/instance_id_api.h
+++ b/chrome/browser/extensions/api/instance_id/instance_id_api.h
@@ -7,6 +7,9 @@
 
 #include "components/gcm_driver/instance_id/instance_id.h"
 #include "extensions/browser/extension_function.h"
+#include "extensions/buildflags/buildflags.h"
+
+static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
 
 namespace extensions {
 
diff --git a/chrome/browser/extensions/api/instance_id/instance_id_apitest.cc b/chrome/browser/extensions/api/instance_id/instance_id_apitest.cc
index f8ae2f7..29b1e809 100644
--- a/chrome/browser/extensions/api/instance_id/instance_id_apitest.cc
+++ b/chrome/browser/extensions/api/instance_id/instance_id_apitest.cc
@@ -13,13 +13,16 @@
 #include "chrome/browser/gcm/gcm_profile_service_factory.h"
 #include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h"
 #include "chrome/browser/profiles/profile.h"
-#include "chrome/test/base/ui_test_utils.h"
 #include "components/gcm_driver/fake_gcm_profile_service.h"
 #include "components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h"
 #include "components/version_info/version_info.h"
+#include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
+#include "extensions/buildflags/buildflags.h"
 #include "extensions/test/result_catcher.h"
 
+static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
+
 using extensions::ResultCatcher;
 
 namespace extensions {
@@ -67,9 +70,13 @@
 IN_PROC_BROWSER_TEST_F(InstanceIDApiTest, Incognito) {
   ResultCatcher catcher;
   catcher.RestrictToBrowserContext(profile());
+  // Android requires a tab to be created for the incognito profile.
+  auto* incognito_web_contents =
+      PlatformOpenURLOffTheRecord(profile(), GURL("about:blank"));
+  auto* incognito_context = incognito_web_contents->GetBrowserContext();
+  ASSERT_TRUE(incognito_context->IsOffTheRecord());
   ResultCatcher incognito_catcher;
-  incognito_catcher.RestrictToBrowserContext(
-      profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true));
+  incognito_catcher.RestrictToBrowserContext(incognito_context);
 
   ASSERT_TRUE(RunExtensionTest("instance_id/incognito", {},
                                {.allow_in_incognito = true}));
diff --git a/chrome/browser/extensions/chrome_app_icon_loader.cc b/chrome/browser/extensions/chrome_app_icon_loader.cc
index 2b0b612..ebb8c72 100644
--- a/chrome/browser/extensions/chrome_app_icon_loader.cc
+++ b/chrome/browser/extensions/chrome_app_icon_loader.cc
@@ -27,7 +27,8 @@
                                          int icon_size_in_dip,
                                          const ResizeFunction& resize_function,
                                          AppIconLoaderDelegate* delegate)
-    : AppIconLoader(profile, icon_size_in_dip, delegate),
+    : AppIconLoader(icon_size_in_dip, delegate),
+      profile_(profile),
       resize_function_(resize_function) {}
 
 ChromeAppIconLoader::ChromeAppIconLoader(Profile* profile,
diff --git a/chrome/browser/extensions/chrome_app_icon_loader.h b/chrome/browser/extensions/chrome_app_icon_loader.h
index 7313cad..1f8a875 100644
--- a/chrome/browser/extensions/chrome_app_icon_loader.h
+++ b/chrome/browser/extensions/chrome_app_icon_loader.h
@@ -10,6 +10,7 @@
 #include <string>
 
 #include "base/functional/callback.h"
+#include "base/memory/raw_ptr.h"
 #include "chrome/browser/extensions/chrome_app_icon_delegate.h"
 #include "chrome/browser/ui/app_icon_loader.h"
 
@@ -54,6 +55,8 @@
   // Sets `extensions_only_` as true to load icons for extensions only.
   void SetExtensionsOnly();
 
+  Profile* profile() { return profile_; }
+
  private:
   using ExtensionIDToChromeAppIconMap =
       std::map<std::string, std::unique_ptr<ChromeAppIcon>>;
@@ -61,6 +64,8 @@
   // ChromeAppIconDelegate:
   void OnIconUpdated(ChromeAppIcon* icon) override;
 
+  const raw_ptr<Profile, DanglingUntriaged> profile_ = nullptr;
+
   // Maps from extension id to ChromeAppIcon.
   ExtensionIDToChromeAppIconMap map_;
 
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index b575b14..66e9aa5 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -7469,6 +7469,11 @@
     "expiry_milestone": 130
   },
   {
+    "name": "omnibox-toolbelt",
+    "owners": ["orin@google.com", "manukh@google.com", "chrome-omnibox-team@google.com"],
+    "expiry_milestone": 150
+  },
+  {
     "name": "omnibox-ui-max-autocomplete-matches",
     "owners": [ "jdonnelly@chromium.org", "chrome-omnibox-team@google.com" ],
     "expiry_milestone": 120
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 09c4257..bee3758 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -2868,16 +2868,6 @@
     "credential mode disallows. Without this flag enabled, Chrome will always "
     "try sending client certificates regardless of the credential mode.";
 
-const char kOmniboxAdaptiveSuggestionsCountName[] =
-    "Adaptive Omnibox Suggestions count";
-const char kOmniboxAdaptiveSuggestionsCountDescription[] =
-    "Dynamically adjust number of presented Omnibox suggestions depending on "
-    "available space. When enabled, this feature will increase (or decrease) "
-    "amount of offered Omnibox suggestions to fill in the space between the "
-    "Omnibox and soft keyboard (if any). See also Max Autocomplete Matches "
-    "flag to adjust the limit of offered suggestions. The number of shown "
-    "suggestions will be no less than the platform default limit.";
-
 const char kOmniboxAdjustIndentationName[] =
     "Adjust Indentation for Omnibox Text and Suggestions";
 const char kOmniboxAdjustIndentationDescription[] =
@@ -2926,12 +2916,6 @@
     "When set, applies certain assets to match Desktop visuals and "
     "descriptions";
 
-const char kOmniboxMostVisitedTilesHorizontalRenderGroupName[] =
-    "Omnibox MV Tiles Horizontal Render Group";
-const char kOmniboxMostVisitedTilesHorizontalRenderGroupDescription[] =
-    "Updates the logic constructing MV tiles to use horizontal render group. "
-    "No user-facing changes expected.";
-
 const char kOmniboxNumNtpZpsRecentSearchesName[] =
     "Omnibox: Recent Searches on new tab page ZPS";
 const char kOmniboxNumNtpZpsRecentSearchesDescription[] =
@@ -2974,6 +2958,10 @@
     "Controls presence/volume of Most Visited URLs shown in zero-prefix "
     "context on the Web";
 
+const char kOmniboxToolbeltName[] = "Omnibox toolbelt";
+const char kOmniboxToolbeltDescription[] =
+    "Adds a row of buttons at the bottom of the omnibox.";
+
 const char kOmniboxZeroSuggestPrefetchDebouncingName[] =
     "Omnibox Zero Prefix Suggest Prefetch Request Debouncing";
 const char kOmniboxZeroSuggestPrefetchDebouncingDescription[] =
@@ -3073,10 +3061,6 @@
 const char kOmniboxSuggestionAnswerMigrationDescription[] =
     "Uses protos instead of SuggestionAnswer to hold answer data.";
 
-const char kOmniboxShortcutBoostName[] = "Omnibox shortcut boosting";
-const char kOmniboxShortcutBoostDescription[] =
-    "Promote shortcuts to be default when available.";
-
 const char kOmniboxMaxZeroSuggestMatchesName[] =
     "Omnibox Max Zero Suggest Matches";
 const char kOmniboxMaxZeroSuggestMatchesDescription[] =
@@ -3146,7 +3130,7 @@
 
 const char kOmniboxUrlSuggestionsOnFocus[] =
     "Omnibox on-focus URL suggestions on web and SRP";
-const char kOmniboxUrlSuggestionsOnFocusDecription[] =
+const char kOmniboxUrlSuggestionsOnFocusDescription[] =
     "Enables zero-prefix URL suggestions on web and SRP when the omnibox is "
     "focused.";
 
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 67d4d92..6824092 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1661,9 +1661,6 @@
 extern const char kOmitCorsClientCertName[];
 extern const char kOmitCorsClientCertDescription[];
 
-extern const char kOmniboxAdaptiveSuggestionsCountName[];
-extern const char kOmniboxAdaptiveSuggestionsCountDescription[];
-
 extern const char kOmniboxAdjustIndentationName[];
 extern const char kOmniboxAdjustIndentationDescription[];
 
@@ -1676,9 +1673,6 @@
 extern const char kOmniboxCalcProviderName[];
 extern const char kOmniboxCalcProviderDescription[];
 
-extern const char kOmniboxConsumesImeInsetsName[];
-extern const char kOmniboxConsumesImeInsetsDescription[];
-
 extern const char kOmniboxDiagnosticsName[];
 extern const char kOmniboxDiagnosticsDescription[];
 
@@ -1716,12 +1710,6 @@
 extern const char kOmniboxMobileParityUpdateName[];
 extern const char kOmniboxMobileParityUpdateDescription[];
 
-extern const char kOmniboxMostVisitedTilesHorizontalRenderGroupName[];
-extern const char kOmniboxMostVisitedTilesHorizontalRenderGroupDescription[];
-
-extern const char kOmniboxMostVisitedTilesTitleWrapAroundName[];
-extern const char kOmniboxMostVisitedTilesTitleWrapAroundDescription[];
-
 extern const char kOmniboxNumNtpZpsRecentSearchesName[];
 extern const char kOmniboxNumNtpZpsRecentSearchesDescription[];
 
@@ -1752,10 +1740,6 @@
 extern const char kOmniboxOnDeviceTailSuggestionsName[];
 extern const char kOmniboxOnDeviceTailSuggestionsDescription[];
 
-extern const char kOmniboxSuppressClipboardSuggestionAfterFirstUsedName[];
-extern const char
-    kOmniboxSuppressClipboardSuggestionAfterFirstUsedDescription[];
-
 extern const char kOmniboxRichAutocompletionPromisingName[];
 extern const char kOmniboxRichAutocompletionPromisingDescription[];
 
@@ -1765,12 +1749,6 @@
 extern const char kOmniboxSuggestionAnswerMigrationName[];
 extern const char kOmniboxSuggestionAnswerMigrationDescription[];
 
-extern const char kOmniboxShortcutBoostName[];
-extern const char kOmniboxShortcutBoostDescription[];
-
-extern const char kOmniboxShortcutExpandingName[];
-extern const char kOmniboxShortcutExpandingDescription[];
-
 extern const char kOmniboxStarterPackExpansionName[];
 extern const char kOmniboxStarterPackExpansionDescription[];
 
@@ -1804,8 +1782,11 @@
 extern const char kOmniboxHideSuggestionGroupHeadersName[];
 extern const char kOmniboxHideSuggestionGroupHeadersDescription[];
 
+extern const char kOmniboxToolbeltName[];
+extern const char kOmniboxToolbeltDescription[];
+
 extern const char kOmniboxUrlSuggestionsOnFocus[];
-extern const char kOmniboxUrlSuggestionsOnFocusDecription[];
+extern const char kOmniboxUrlSuggestionsOnFocusDescription[];
 
 extern const char kOmniboxZeroSuggestPrefetchDebouncingName[];
 extern const char kOmniboxZeroSuggestPrefetchDebouncingDescription[];
diff --git a/chrome/browser/page_load_metrics/integration_tests/soft_navigation_metrics_browsertest.cc b/chrome/browser/page_load_metrics/integration_tests/soft_navigation_metrics_browsertest.cc
index 883776d..79958b6 100644
--- a/chrome/browser/page_load_metrics/integration_tests/soft_navigation_metrics_browsertest.cc
+++ b/chrome/browser/page_load_metrics/integration_tests/soft_navigation_metrics_browsertest.cc
@@ -341,7 +341,7 @@
 #define MAYBE_LargestContentfulPaint LargestContentfulPaint
 #endif
 
-IN_PROC_BROWSER_TEST_P(SoftNavigationTest, MAYBE_LargestContentfulPaint) {
+IN_PROC_BROWSER_TEST_P(SoftNavigationTest, DISABLED_LargestContentfulPaint) {
   auto waiter = std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
       web_contents());
 
@@ -558,7 +558,8 @@
 #else
 #define MAYBE_INP_ClickWithPresentation INP_ClickWithPresentation
 #endif  //  BUILDFLAG(IS_WIN)
-IN_PROC_BROWSER_TEST_P(SoftNavigationTest, MAYBE_INP_ClickWithPresentation) {
+
+IN_PROC_BROWSER_TEST_P(SoftNavigationTest, DISABLED_INP_ClickWithPresentation) {
   // Add waiter to wait for the interaction is arrived in browser.
   auto waiter = std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
       web_contents());
@@ -607,7 +608,8 @@
 #else
 #define MAYBE_LayoutShift LayoutShift
 #endif  //  BUILDFLAG(IS_WIN) && defined(ADDRESS_SANITIZER)
-IN_PROC_BROWSER_TEST_P(SoftNavigationTest, MAYBE_LayoutShift) {
+
+IN_PROC_BROWSER_TEST_P(SoftNavigationTest, DISABLED_LayoutShift) {
   auto waiter = std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
       web_contents());
 
diff --git a/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerSheetContent.java b/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerSheetContent.java
index 062e6838..1ec80bf 100644
--- a/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerSheetContent.java
+++ b/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerSheetContent.java
@@ -278,6 +278,16 @@
         }
     }
 
+    void setRequestedPlaybackMode(PlaybackMode playbackMode) {
+      if  (playbackMode == PlaybackMode.OVERVIEW) {
+        mLoadingTextView.setText(mContext.getString(R.string.readaloud_mini_player_loading_ai_playback));
+        mLoadingTextView.setContentDescription(mContext.getString(R.string.readaloud_mini_player_loading_ai_playback));
+      } else {
+        mLoadingTextView.setText(mContext.getString(R.string.readaloud_playback_loading));
+        mLoadingTextView.setContentDescription(mContext.getString(R.string.readaloud_playback_loading));
+      }
+    }
+
     void showFeedbackButtons() {
         mThumbUpButton.setVisibility(View.VISIBLE);
         mThumbDownButton.setVisibility(View.VISIBLE);
diff --git a/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerSheetContentUnitTest.java b/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerSheetContentUnitTest.java
index e96c245..22b5985 100644
--- a/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerSheetContentUnitTest.java
+++ b/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerSheetContentUnitTest.java
@@ -321,4 +321,15 @@
         scrollView.scrollTo(0, 100);
         assertEquals(100, mContent.getVerticalScrollOffset());
     }
+
+    @Test
+    public void testLoadingTextIsSetCorrectly() {
+      TextView loadingText = (TextView) mContentView.findViewById(R.id.readaloud_loading_text);
+
+      mContent.setRequestedPlaybackMode(PlaybackMode.OVERVIEW);
+      assertEquals(loadingText.getText(), mContext.getString(R.string.readaloud_mini_player_loading_ai_playback));
+
+      mContent.setRequestedPlaybackMode(PlaybackMode.CLASSIC);
+      assertEquals(loadingText.getText(), mContext.getString(R.string.readaloud_playback_loading));
+    }
 }
diff --git a/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerViewBinder.java b/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerViewBinder.java
index 67e583a..117611b 100644
--- a/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerViewBinder.java
+++ b/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/ExpandedPlayerViewBinder.java
@@ -77,6 +77,9 @@
                             model.get(PlayerProperties.PLAYBACK_MODE_SELECTION_ENABLED)));
         } else if (key == PlayerProperties.FEEDBACK_TYPE) {
           content.setSentFeedback(FeedbackType.fromValue(model.get(PlayerProperties.FEEDBACK_TYPE)));
+        } else if (key == PlayerProperties.REQUESTED_PLAYBACK_MODE) {
+            content.setRequestedPlaybackMode(
+                    PlaybackMode.fromValue(model.get(PlayerProperties.REQUESTED_PLAYBACK_MODE)));
         }
     }
 }
diff --git a/chrome/browser/tab_group_sync/android/java/src/org/chromium/chrome/browser/tab_group_sync/TestTabGroupSyncService.java b/chrome/browser/tab_group_sync/android/java/src/org/chromium/chrome/browser/tab_group_sync/TestTabGroupSyncService.java
index b41ec1a6..46490c9 100644
--- a/chrome/browser/tab_group_sync/android/java/src/org/chromium/chrome/browser/tab_group_sync/TestTabGroupSyncService.java
+++ b/chrome/browser/tab_group_sync/android/java/src/org/chromium/chrome/browser/tab_group_sync/TestTabGroupSyncService.java
@@ -15,6 +15,7 @@
 import org.chromium.components.tab_group_sync.OpeningSource;
 import org.chromium.components.tab_group_sync.SavedTabGroup;
 import org.chromium.components.tab_group_sync.TabGroupSyncService;
+import org.chromium.components.tab_group_sync.VersioningMessageController;
 import org.chromium.url.GURL;
 
 import java.util.ArrayList;
@@ -26,6 +27,8 @@
     public static final String LOCAL_DEVICE_CACHE_GUID = "LocalDevice";
 
     private final List<SavedTabGroup> mTabGroups = new ArrayList<>();
+    private final VersioningMessageController mVersioningMessageController =
+            new TestVersioningMessageController();
 
     @Override
     public void addObserver(Observer observer) {}
@@ -142,5 +145,21 @@
     public void updateArchivalStatus(String syncTabGroupId, boolean archivalStatus) {}
 
     @Override
+    public VersioningMessageController getVersioningMessageController() {
+        return mVersioningMessageController;
+    }
+
+    @Override
     public void setCollaborationAvailableInFinderForTesting(String collaborationId) {}
+
+    private static class TestVersioningMessageController implements VersioningMessageController {
+        @Override
+        public void shouldShowMessageUiAsync(int messageType, Callback<Boolean> callback) {}
+
+        @Override
+        public void onMessageUiShown(int messageType) {}
+
+        @Override
+        public void onMessageUiDismissed(int messageType) {}
+    }
 }
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index c17d964..35bcb53 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -1763,6 +1763,7 @@
       "//components/media_router/common/mojom:media_router",
       "//components/network_session_configurator/common",
       "//components/omnibox/browser:mojo_bindings",
+      "//components/omnibox/composebox",
       "//components/page_load_metrics/browser",
       "//components/paint_preview/browser",
       "//components/paint_preview/common",
@@ -4919,6 +4920,7 @@
       # and chrome/browser/ui/views/sharing_hub/ get componentized.
       "//chrome/browser/prefs:util",
       "//chrome/browser/sharing_hub",
+      "//chrome/browser/upgrade_detector",
       "//ui/base/mojom:ui_base_types",
     ]
 
diff --git a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenu.java b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenu.java
index d7de06cc..92c811e 100644
--- a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenu.java
+++ b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenu.java
@@ -16,7 +16,6 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.SystemClock;
-import android.text.TextUtils;
 import android.util.SparseArray;
 import android.view.Gravity;
 import android.view.KeyEvent;
@@ -29,17 +28,13 @@
 import android.view.ViewParent;
 import android.view.ViewStub;
 import android.view.WindowManager;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ImageButton;
 import android.widget.ListView;
 import android.widget.PopupWindow;
 
-import androidx.annotation.ColorInt;
 import androidx.annotation.IdRes;
 import androidx.annotation.VisibleForTesting;
 import androidx.appcompat.content.res.AppCompatResources;
-import androidx.core.content.ContextCompat;
 
 import org.chromium.base.Callback;
 import org.chromium.base.SysUtils;
@@ -55,7 +50,6 @@
 import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider.ControlsPosition;
 import org.chromium.chrome.browser.ui.appmenu.internal.R;
 import org.chromium.components.browser_ui.styles.SemanticColorUtils;
-import org.chromium.components.browser_ui.util.motion.MotionEventInfo;
 import org.chromium.components.browser_ui.widget.chips.ChipView;
 import org.chromium.components.browser_ui.widget.highlight.ViewHighlighter;
 import org.chromium.components.browser_ui.widget.highlight.ViewHighlighter.HighlightParams;
@@ -63,7 +57,6 @@
 import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
 import org.chromium.ui.modelutil.ModelListAdapter;
 import org.chromium.ui.modelutil.PropertyModel;
-import org.chromium.ui.widget.Toast;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -81,7 +74,7 @@
  * </ul>
  */
 @NullMarked
-class AppMenu implements OnItemClickListener, OnKeyListener, AppMenuClickHandler {
+class AppMenu implements OnKeyListener {
     private static final float LAST_ITEM_SHOW_FRACTION = 0.5f;
 
     /** A means of reporting an exception/stack without crashing. */
@@ -335,7 +328,6 @@
         mSelectedItemBeforeDismiss = false;
         mMenuShownTimeMs = SystemClock.elapsedRealtime();
 
-        mListView.setOnItemClickListener(this);
         mListView.setItemsCanFocus(true);
         mListView.setOnKeyListener(this);
 
@@ -420,44 +412,9 @@
         return position;
     }
 
-    @Override
-    public void onItemClick(PropertyModel model, @Nullable MotionEventInfo triggeringMotion) {
-        if (!model.get(AppMenuItemProperties.ENABLED)) return;
-
-        int id = model.get(AppMenuItemProperties.MENU_ITEM_ID);
-        mSelectedItemBeforeDismiss = true;
-        dismiss();
-        mHandler.onOptionsItemSelected(id, triggeringMotion);
-    }
-
-    @Override
-    public boolean onItemLongClick(PropertyModel model, View view) {
-        if (!model.get(AppMenuItemProperties.ENABLED)) return false;
-
-        mSelectedItemBeforeDismiss = true;
-        CharSequence titleCondensed = model.get(AppMenuItemProperties.TITLE_CONDENSED);
-        CharSequence message =
-                TextUtils.isEmpty(titleCondensed)
-                        ? model.get(AppMenuItemProperties.TITLE)
-                        : titleCondensed;
-        return showToastForItem(message, view);
-    }
-
-    @VisibleForTesting
-    boolean showToastForItem(CharSequence message, View view) {
-        Context context = view.getContext();
-        final @ColorInt int backgroundColor = ContextCompat.getColor(context, R.color.toast_color);
-        return new Toast.Builder(context)
-                .withText(message)
-                .withAnchoredView(view)
-                .withBackgroundColor(backgroundColor)
-                .withTextAppearance(R.style.TextAppearance_TextSmall_Primary)
-                .buildAndShow();
-    }
-
-    @Override
-    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-        onItemClick(mModelList.get(position).model);
+    /** Marks whether an item was selected prior to dismissal. */
+    public void setSelectedItemBeforeDismiss(boolean selected) {
+        mSelectedItemBeforeDismiss = selected;
     }
 
     @Override
@@ -553,11 +510,6 @@
         return null;
     }
 
-    /** Invalidate the app menu data. See {@link AppMenuAdapter#notifyDataSetChanged}. */
-    void invalidate() {
-        if (mAdapter != null) mAdapter.notifyDataSetChanged();
-    }
-
     @RequiresNonNull("mPopup")
     private void setMenuHeight(
             List<Integer> menuItemIds,
diff --git a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandlerImpl.java b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandlerImpl.java
index 028bf00..b114e03 100644
--- a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandlerImpl.java
+++ b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandlerImpl.java
@@ -12,17 +12,19 @@
 import android.content.res.TypedArray;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.text.TextUtils;
 import android.util.SparseArray;
 import android.view.ContextThemeWrapper;
 import android.view.Display;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.widget.Adapter;
 
+import androidx.annotation.ColorInt;
 import androidx.annotation.LayoutRes;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.content.ContextCompat;
 
 import org.chromium.base.Callback;
 import org.chromium.base.metrics.RecordUserAction;
@@ -47,6 +49,7 @@
 import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
 import org.chromium.ui.modelutil.ModelListAdapter;
 import org.chromium.ui.modelutil.PropertyModel;
+import org.chromium.ui.widget.Toast;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -58,7 +61,10 @@
  */
 @NullMarked
 class AppMenuHandlerImpl
-        implements AppMenuHandler, StartStopWithNativeObserver, ConfigurationChangedObserver {
+        implements AppMenuHandler,
+                AppMenuClickHandler,
+                StartStopWithNativeObserver,
+                ConfigurationChangedObserver {
 
     /** An {@link Adapter} for the list of items in the app menu. */
     private static final class AppMenuAdapter extends ModelListAdapter {
@@ -161,7 +167,7 @@
                         updateModelForHighlightAndClick(
                                 mModelList,
                                 mHighlightMenuId,
-                                mAppMenu,
+                                AppMenuHandlerImpl.this,
                                 /* startIndex= */ index,
                                 /* withAssertions= */ false);
                     }
@@ -172,7 +178,7 @@
                         updateModelForHighlightAndClick(
                                 mModelList,
                                 mHighlightMenuId,
-                                mAppMenu,
+                                AppMenuHandlerImpl.this,
                                 /* startIndex= */ index,
                                 /* withAssertions= */ false);
                     }
@@ -260,7 +266,7 @@
 
         assert !(isByPermanentButton && startDragging);
 
-        mModelList = mDelegate.getMenuItems(this);
+        mModelList = mDelegate.getMenuItems();
         mModelList.addObserver(mListObserver);
         ContextThemeWrapper wrapper =
                 new ContextThemeWrapper(mContext, R.style.OverflowMenuThemeOverlay);
@@ -274,7 +280,7 @@
             mAppMenu = new AppMenu(itemRowHeight, this, mContext.getResources());
             mAppMenuDragHelper = new AppMenuDragHelper(mContext, mAppMenu, itemRowHeight);
         }
-        setupModelForHighlightAndClick(mModelList, mHighlightMenuId, mAppMenu);
+        setupModelForHighlightAndClick(mModelList, mHighlightMenuId, this);
         AppMenuAdapter adapter = new AppMenuAdapter(mModelList);
         mAppMenu.updateMenu(mModelList, adapter);
 
@@ -362,11 +368,6 @@
     }
 
     @Override
-    public void invalidateAppMenu() {
-        if (mAppMenu != null) mAppMenu.invalidate();
-    }
-
-    @Override
     public void addObserver(AppMenuObserver observer) {
         mObservers.add(observer);
     }
@@ -390,16 +391,15 @@
         hideAppMenu();
     }
 
-    /**
-     * Called when a menu item is selected.
-     *
-     * @param itemId The menu item ID.
-     * @param triggeringMotion The {@link MotionEventInfo} that triggered the click; it is {@code
-     *     null} if {@link MotionEvent} wasn't available when the click was detected, such as in
-     *     {@link android.view.View.OnClickListener}.
-     */
-    @VisibleForTesting
-    void onOptionsItemSelected(int itemId, @Nullable MotionEventInfo triggeringMotion) {
+    @Override
+    public void onItemClick(PropertyModel model, @Nullable MotionEventInfo triggeringMotion) {
+        if (mAppMenu == null) return;
+        if (!model.get(AppMenuItemProperties.ENABLED)) return;
+
+        int itemId = model.get(AppMenuItemProperties.MENU_ITEM_ID);
+        mAppMenu.setSelectedItemBeforeDismiss(true);
+        mAppMenu.dismiss();
+
         if (mTestOptionsItemSelectedListener != null) {
             mTestOptionsItemSelectedListener.onResult(itemId);
             return;
@@ -409,8 +409,35 @@
                 itemId, mDelegate.getBundleForMenuItem(itemId), triggeringMotion);
     }
 
+    @Override
+    public boolean onItemLongClick(PropertyModel model, View view) {
+        if (mAppMenu == null) return false;
+        if (!model.get(AppMenuItemProperties.ENABLED)) return false;
+
+        mAppMenu.setSelectedItemBeforeDismiss(true);
+
+        CharSequence titleCondensed = model.get(AppMenuItemProperties.TITLE_CONDENSED);
+        CharSequence message =
+                TextUtils.isEmpty(titleCondensed)
+                        ? model.get(AppMenuItemProperties.TITLE)
+                        : titleCondensed;
+        return showToastForItem(message, view);
+    }
+
+    private static boolean showToastForItem(CharSequence message, View view) {
+        Context context = view.getContext();
+        final @ColorInt int backgroundColor = ContextCompat.getColor(context, R.color.toast_color);
+        return new Toast.Builder(context)
+                .withText(message)
+                .withAnchoredView(view)
+                .withBackgroundColor(backgroundColor)
+                .withTextAppearance(R.style.TextAppearance_TextSmall_Primary)
+                .buildAndShow();
+    }
+
     /**
      * Called by AppMenu to report that the App Menu visibility has changed.
+     *
      * @param isVisible Whether the App Menu is showing.
      */
     void onMenuVisibilityChanged(boolean isVisible) {
@@ -462,7 +489,8 @@
         mTestOptionsItemSelectedListener = onOptionsItemSelectedListener;
     }
 
-    AppMenuPropertiesDelegate getDelegateForTests() {
+    @Override
+    public AppMenuPropertiesDelegate getMenuPropertiesDelegate() {
         return mDelegate;
     }
 
diff --git a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTest.java b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTest.java
index d414238..20beb11 100644
--- a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTest.java
+++ b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTest.java
@@ -67,6 +67,8 @@
 import org.chromium.ui.test.util.BlankUiTestActivity;
 import org.chromium.ui.test.util.NightModeTestUtils;
 import org.chromium.ui.test.util.RenderTestRule;
+import org.chromium.ui.widget.Toast;
+import org.chromium.ui.widget.ToastManager;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -333,35 +335,23 @@
 
     @Test
     @MediumTest
-    public void testClickMenuItem_UsingPosition() throws TimeoutException {
-        showMenuAndAssert();
-
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> mAppMenuHandler.getAppMenu().onItemClick(null, null, 0, 0));
-
-        mDelegate.itemSelectedCallbackHelper.waitForCallback(0);
-        Assert.assertEquals(
-                "Incorrect id for last selected item.",
-                R.id.menu_item_one,
-                mDelegate.lastSelectedItemId);
-    }
-
-    @Test
-    @MediumTest
     public void testLongClickMenuItem_Title() throws TimeoutException {
         mPropertiesDelegate.enableAppIconRow = true;
         showMenuAndAssert();
-        AppMenu spiedMenu = Mockito.spy(mAppMenuHandler.getAppMenu());
+
+        ToastManager toastManager = Mockito.mock(ToastManager.class);
+        ToastManager.setInstanceForTesting(toastManager);
 
         View testView = new View(sActivity);
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
-                    spiedMenu.onItemLongClick(
-                            mAppMenuHandler.getAppMenu().getMenuItemPropertyModel(R.id.icon_one),
-                            testView);
+                    AppMenuTestSupport.callOnItemLongClick(
+                            mAppMenuCoordinator, R.id.icon_one, testView);
                 });
 
-        Mockito.verify(spiedMenu, Mockito.times(1)).showToastForItem("Icon One", testView);
+        ArgumentCaptor<Toast> toastCaptor = ArgumentCaptor.forClass(Toast.class);
+        Mockito.verify(toastManager, Mockito.times(1)).requestShow(toastCaptor.capture());
+        Assert.assertEquals("Icon One", toastCaptor.getValue().getText());
     }
 
     @Test
@@ -369,17 +359,20 @@
     public void testLongClickMenuItem_TitleCondensed() throws TimeoutException {
         mPropertiesDelegate.enableAppIconRow = true;
         showMenuAndAssert();
-        AppMenu spiedMenu = Mockito.spy(mAppMenuHandler.getAppMenu());
+
+        ToastManager toastManager = Mockito.mock(ToastManager.class);
+        ToastManager.setInstanceForTesting(toastManager);
 
         View testView = new View(sActivity);
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
-                    spiedMenu.onItemLongClick(
-                            mAppMenuHandler.getAppMenu().getMenuItemPropertyModel(R.id.icon_two),
-                            testView);
+                    AppMenuTestSupport.callOnItemLongClick(
+                            mAppMenuCoordinator, R.id.icon_two, testView);
                 });
 
-        Mockito.verify(spiedMenu, Mockito.times(1)).showToastForItem("2", testView);
+        ArgumentCaptor<Toast> toastCaptor = ArgumentCaptor.forClass(Toast.class);
+        Mockito.verify(toastManager, Mockito.times(1)).requestShow(toastCaptor.capture());
+        Assert.assertEquals("2", toastCaptor.getValue().getText());
     }
 
     @Test
@@ -387,18 +380,18 @@
     public void testLongClickMenuItem_Disabled() throws TimeoutException {
         mPropertiesDelegate.enableAppIconRow = true;
         showMenuAndAssert();
-        AppMenu spiedMenu = Mockito.spy(mAppMenuHandler.getAppMenu());
+
+        ToastManager toastManager = Mockito.mock(ToastManager.class);
+        ToastManager.setInstanceForTesting(toastManager);
 
         View testView = new View(sActivity);
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
-                    spiedMenu.onItemLongClick(
-                            mAppMenuHandler.getAppMenu().getMenuItemPropertyModel(R.id.icon_three),
-                            testView);
+                    AppMenuTestSupport.callOnItemLongClick(
+                            mAppMenuCoordinator, R.id.icon_three, testView);
                 });
 
-        Mockito.verify(spiedMenu, Mockito.times(0))
-                .showToastForItem(Mockito.any(CharSequence.class), Mockito.any(View.class));
+        Mockito.verify(toastManager, Mockito.times(0)).requestShow(Mockito.any(Toast.class));
     }
 
     @Test
@@ -594,9 +587,6 @@
                             .getModelListForTesting()
                             .add(0, new MVCListAdapter.ListItem(AppMenuItemType.STANDARD, model));
                 });
-        // ensure clicking on the newly added item doesn't break anything
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> mAppMenuHandler.getAppMenu().onItemClick(null, null, 0, 0));
 
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
diff --git a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/TestAppMenuPropertiesDelegate.java b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/TestAppMenuPropertiesDelegate.java
index 80bbc5c..a321ceb 100644
--- a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/TestAppMenuPropertiesDelegate.java
+++ b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/TestAppMenuPropertiesDelegate.java
@@ -34,7 +34,7 @@
     public void destroy() {}
 
     @Override
-    public ModelList getMenuItems(AppMenuHandler handler) {
+    public ModelList getMenuItems() {
         ModelList modelList = new ModelList();
 
         modelList.add(
diff --git a/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandler.java b/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandler.java
index 4ca5c3f..392a2087 100644
--- a/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandler.java
+++ b/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandler.java
@@ -99,6 +99,8 @@
      */
     AppMenuButtonHelper createAppMenuButtonHelper();
 
-    /** Call to cause a redraw when an item in the app menu changes. */
-    void invalidateAppMenu();
+    /**
+     * @return {@link AppMenuPropertiesDelegate} that builds the menu list.
+     */
+    AppMenuPropertiesDelegate getMenuPropertiesDelegate();
 }
diff --git a/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuItemProperties.java b/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuItemProperties.java
index 407ab61c..6bd1d50 100644
--- a/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuItemProperties.java
+++ b/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuItemProperties.java
@@ -93,14 +93,6 @@
     public static final WritableObjectPropertyKey<ModelList> ADDITIONAL_ICONS =
             new WritableObjectPropertyKey<>("ADDITIONAL_ICONS");
 
-    /**
-     * A generic key for non-standard data types.
-     *
-     * <p>TODO(crbug.com/40145539): Remove this super hacky key.
-     */
-    public static final WritableObjectPropertyKey<Object> CUSTOM_ITEM_DATA =
-            new WritableObjectPropertyKey<>("CUSTOM_ITEM_DATA");
-
     public static final PropertyKey[] ALL_ICON_KEYS =
             new PropertyKey[] {
                 MENU_ITEM_ID,
@@ -131,7 +123,6 @@
                 SUPPORT_ENTER_ANIMATION,
                 CLICK_HANDLER,
                 MENU_ICON_AT_START,
-                ADDITIONAL_ICONS,
-                CUSTOM_ITEM_DATA
+                ADDITIONAL_ICONS
             };
 }
diff --git a/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuPropertiesDelegate.java b/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuPropertiesDelegate.java
index 1324d539..2433f725 100644
--- a/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuPropertiesDelegate.java
+++ b/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuPropertiesDelegate.java
@@ -36,10 +36,9 @@
     /**
      * Gets the menu items for app menu.
      *
-     * @param handler The {@link AppMenuHandler} associated with {@code menu}.
      * @return The {@link ModelList} which contains the menu items for app menu.
      */
-    ModelList getMenuItems(AppMenuHandler handler);
+    ModelList getMenuItems();
 
     /**
      * Gets a bundle of (optional) extra data associated with the provided MenuItem.
diff --git a/chrome/browser/ui/android/appmenu/test/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTestSupport.java b/chrome/browser/ui/android/appmenu/test/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTestSupport.java
index 14b76bc..f25819e0 100644
--- a/chrome/browser/ui/android/appmenu/test/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTestSupport.java
+++ b/chrome/browser/ui/android/appmenu/test/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTestSupport.java
@@ -40,7 +40,9 @@
     public static void onOptionsItemSelected(AppMenuCoordinator coordinator, int itemId) {
         ((AppMenuCoordinatorImpl) coordinator)
                 .getAppMenuHandlerImplForTesting()
-                .onOptionsItemSelected(itemId, /* triggeringMotion= */ null);
+                .onItemClick(
+                        getMenuItemPropertyModel(coordinator, itemId),
+                        /* triggeringMotion= */ null);
     }
 
     /**
@@ -72,23 +74,41 @@
 
         ((AppMenuCoordinatorImpl) coordinator)
                 .getAppMenuHandlerImplForTesting()
-                .getAppMenu()
                 .onItemClick(model, triggeringMotion);
     }
 
     /**
+     * Simulates a long click on the menu item matching the provided id.
+     *
+     * @param coordinator The {@link AppMenuCoordinator} associated with the app menu being tested.
+     * @param menuItemId The id of the menu item to click.
+     * @param view The view that was clicked.
+     */
+    public static void callOnItemLongClick(
+            AppMenuCoordinator coordinator, int menuItemId, View view) {
+        PropertyModel model =
+                ((AppMenuCoordinatorImpl) coordinator)
+                        .getAppMenuHandlerImplForTesting()
+                        .getAppMenu()
+                        .getMenuItemPropertyModel(menuItemId);
+
+        ((AppMenuCoordinatorImpl) coordinator)
+                .getAppMenuHandlerImplForTesting()
+                .onItemLongClick(model, view);
+    }
+
+    /**
      * Show the app menu.
+     *
      * @param coordinator The {@link AppMenuCoordinator} associated with the app menu being tested.
      * @param anchorView Anchor view (usually a menu button) to be used for the popup, if null is
-     *         passed then hardware menu button anchor will be used.
+     *     passed then hardware menu button anchor will be used.
      * @param startDragging Whether dragging is started. For example, if the app menu is showed by
-     *         tapping on a button, this should be false. If it is showed by start
-     *         dragging down on the menu button, this should be true. Note that if
-     *         anchorView is null, this must be false since we no longer support
-     *         hardware menu button dragging.
-     * @return True, if the menu is shown, false, if menu is not shown, example
-     *         reasons: the menu is not yet available to be shown, or the menu is
-     *         already showing.
+     *     tapping on a button, this should be false. If it is showed by start dragging down on the
+     *     menu button, this should be true. Note that if anchorView is null, this must be false
+     *     since we no longer support hardware menu button dragging.
+     * @return True, if the menu is shown, false, if menu is not shown, example reasons: the menu is
+     *     not yet available to be shown, or the menu is already showing.
      */
     public static boolean showAppMenu(
             AppMenuCoordinator coordinator, View anchorView, boolean startDragging) {
@@ -140,7 +160,7 @@
             AppMenuCoordinator coordinator) {
         return ((AppMenuCoordinatorImpl) coordinator)
                 .getAppMenuHandlerImplForTesting()
-                .getDelegateForTests();
+                .getMenuPropertiesDelegate();
     }
 
     /**
diff --git a/chrome/browser/ui/android/multiwindow/BUILD.gn b/chrome/browser/ui/android/multiwindow/BUILD.gn
index 1b4129a..09aa199 100644
--- a/chrome/browser/ui/android/multiwindow/BUILD.gn
+++ b/chrome/browser/ui/android/multiwindow/BUILD.gn
@@ -35,6 +35,7 @@
     "//chrome/browser/util:java",
     "//components/browser_ui/modaldialog/android:java",
     "//components/browser_ui/styles/android:java",
+    "//components/browser_ui/util/android:java",
     "//components/browser_ui/widget/android:java",
     "//components/favicon/android:java",
     "//components/feature_engagement/public:public_java",
@@ -65,6 +66,7 @@
     "java/res/layout/instance_switcher_dialog.xml",
     "java/res/layout/instance_switcher_dialog_v2.xml",
     "java/res/layout/instance_switcher_item.xml",
+    "java/res/layout/instance_switcher_item_v2.xml",
     "java/res/layout/instance_switcher_list.xml",
     "java/res/layout/target_selector_dialog.xml",
     "java/res/values/colors.xml",
@@ -94,6 +96,7 @@
     "//base:base_java",
     "//base:base_java_test_support",
     "//base:base_junit_test_support",
+    "//chrome/browser/flags:java",
     "//chrome/browser/profiles/android:java",
     "//chrome/browser/util:java",
     "//components/favicon/android:java",
diff --git a/chrome/browser/ui/android/multiwindow/java/res/layout/instance_switcher_item_v2.xml b/chrome/browser/ui/android/multiwindow/java/res/layout/instance_switcher_item_v2.xml
new file mode 100644
index 0000000..b09773e
--- /dev/null
+++ b/chrome/browser/ui/android/multiwindow/java/res/layout/instance_switcher_item_v2.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright 2021 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/content"
+    style="@style/ListItemContainer" >
+
+    <org.chromium.ui.widget.ChromeImageView
+        android:id="@+id/favicon"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_alignParentStart="true"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="16dp"
+        android:layout_marginVertical="22dp"
+        android:scaleType="fitCenter"
+        android:layout_gravity="center_vertical"
+        android:importantForAccessibility="no" />
+
+    <LinearLayout
+        android:id="@+id/text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_toEndOf="@id/favicon"
+        android:layout_toStartOf="@id/last_accessed"
+        android:layout_centerVertical="true"
+        android:orientation="vertical"
+        android:layout_marginEnd="16dp" >
+
+        <org.chromium.ui.widget.TextViewWithLeading
+            android:id="@+id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:maxLines="1"
+            android:ellipsize="end"
+            android:textAppearance="@style/TextAppearance.TextLarge.Primary"
+            android:layout_gravity="center_vertical"
+            app:leading="@dimen/text_size_large_leading" />
+        <TextView
+            android:id="@+id/desc"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="10dp"
+            android:maxLines="1"
+            android:textAppearance="@style/TextAppearance.TextMedium.Secondary"
+            android:layout_gravity="center_vertical"
+            app:leading="@dimen/text_size_medium_leading" />
+    </LinearLayout>
+
+   <TextView
+        android:id="@+id/last_accessed"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:maxLines="1"
+        android:textAppearance="@style/TextAppearance.TextMedium.Secondary"
+        android:layout_centerVertical="true"
+        android:layout_toStartOf="@id/close_button"
+        android:layout_marginEnd="16dp"
+        android:gravity="end"
+        app:leading="@dimen/text_size_medium_leading" />
+
+    <ImageView
+        android:id="@+id/close_button"
+        android:src="@drawable/material_ic_close_24dp"
+        android:contentDescription="@string/close"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_alignParentEnd="true"
+        android:layout_centerVertical="true"
+        android:layout_marginHorizontal="16dp"
+        android:scaleType="fitCenter"/>
+</RelativeLayout>
diff --git a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinator.java b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinator.java
index 6a50f8d7..d4ebae23 100644
--- a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinator.java
+++ b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinator.java
@@ -32,9 +32,9 @@
 import org.chromium.build.annotations.MonotonicNonNull;
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.build.annotations.Nullable;
-import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
+import org.chromium.components.browser_ui.util.TimeTextResolver;
 import org.chromium.components.browser_ui.widget.BrowserUiListMenuUtils;
 import org.chromium.components.favicon.LargeIconBridge;
 import org.chromium.ui.listmenu.BasicListMenu;
@@ -56,8 +56,8 @@
 import java.util.List;
 
 /**
- * Coordinator to construct the instance switcher dialog.
- * TODO: Resolve various inconsistencies that can be caused by Ui from multiple instances.
+ * Coordinator to construct the instance switcher dialog. TODO: Resolve various inconsistencies that
+ * can be caused by Ui from multiple instances.
  */
 @NullMarked
 public class InstanceSwitcherCoordinator {
@@ -143,9 +143,9 @@
         mNewWindowAction = newWindowAction;
         mMaxInstanceCount = maxInstanceCount;
 
-        if (isInstanceSwitcherV2Enabled()) {
-            var activeListAdapter = getInstanceListAdapter(/* active= */ true);
-            var inactiveListAdapter = getInstanceListAdapter(/* active= */ false);
+        if (UiUtils.isInstanceSwitcherV2Enabled()) {
+            var activeListAdapter = getInstanceListV2Adapter(/* active= */ true);
+            var inactiveListAdapter = getInstanceListV2Adapter(/* active= */ false);
 
             mDialogView =
                     LayoutInflater.from(context)
@@ -213,13 +213,13 @@
         }
     }
 
-    private SimpleRecyclerViewAdapter getInstanceListAdapter(boolean active) {
+    private SimpleRecyclerViewAdapter getInstanceListV2Adapter(boolean active) {
         var adapter = new SimpleRecyclerViewAdapter(active ? mActiveModelList : mInactiveModelList);
         adapter.registerType(
                 EntryType.INSTANCE,
                 parentView ->
                         LayoutInflater.from(mContext)
-                                .inflate(R.layout.instance_switcher_item, null),
+                                .inflate(R.layout.instance_switcher_item_v2, null),
                 InstanceSwitcherItemViewBinder::bind);
         adapter.registerType(
                 EntryType.COMMAND,
@@ -237,7 +237,7 @@
             // An active instance should have an associated live task.
             boolean isActiveInstance = items.get(i).taskId != INVALID_TASK_ID;
             PropertyModel itemModel = generateListItem(items.get(i));
-            if (isInstanceSwitcherV2Enabled()) {
+            if (UiUtils.isInstanceSwitcherV2Enabled()) {
                 if (isActiveInstance) {
                     mActiveModelList.add(
                             new ModelListAdapter.ListItem(EntryType.INSTANCE, itemModel));
@@ -296,17 +296,32 @@
     private PropertyModel generateListItem(InstanceInfo item) {
         String title = mUiUtils.getItemTitle(item);
         String desc = mUiUtils.getItemDesc(item);
-        boolean currentIndicator = item.type == InstanceInfo.Type.CURRENT;
+        boolean isCurrentWindow = item.type == InstanceInfo.Type.CURRENT;
         PropertyModel.Builder builder =
                 new PropertyModel.Builder(InstanceSwitcherItemProperties.ALL_KEYS)
                         .with(InstanceSwitcherItemProperties.TITLE, title)
                         .with(InstanceSwitcherItemProperties.DESC, desc)
-                        .with(InstanceSwitcherItemProperties.CURRENT, currentIndicator)
                         .with(InstanceSwitcherItemProperties.INSTANCE_ID, item.instanceId)
                         .with(
                                 InstanceSwitcherItemProperties.CLICK_LISTENER,
                                 (view) -> switchToInstance(item));
-        if (!currentIndicator) buildMoreMenu(builder, item);
+
+        if (!UiUtils.isInstanceSwitcherV2Enabled()) {
+            builder.with(InstanceSwitcherItemProperties.CURRENT, isCurrentWindow);
+            if (!isCurrentWindow) {
+                buildMoreMenu(builder, item);
+            }
+        } else {
+            String lastAccessedString =
+                    isCurrentWindow
+                            ? mContext.getString(R.string.instance_last_accessed_current)
+                            : TimeTextResolver.resolveTimeAgoText(
+                                    mContext.getResources(), item.lastAccessedTime);
+            builder.with(InstanceSwitcherItemProperties.LAST_ACCESSED, lastAccessedString)
+                    .with(
+                            InstanceSwitcherItemProperties.CLOSE_BUTTON_CLICK_LISTENER,
+                            v -> closeWindow(item));
+        }
         PropertyModel model = builder.build();
         mUiUtils.setFavicon(model, InstanceSwitcherItemProperties.FAVICON, item);
         return model;
@@ -335,11 +350,7 @@
                 (model) -> {
                     int textId = model.get(ListMenuItemProperties.TITLE_ID);
                     if (textId == R.string.instance_switcher_close_window) {
-                        if (canSkipConfirm(item)) {
-                            removeInstance(item);
-                        } else {
-                            showConfirmationMessage(item);
-                        }
+                        closeWindow(item);
                     }
                 };
         BasicListMenu listMenu =
@@ -351,6 +362,14 @@
         builder.with(InstanceSwitcherItemProperties.MORE_MENU, () -> listMenu);
     }
 
+    private void closeWindow(InstanceInfo item) {
+        if (canSkipConfirm(item)) {
+            removeInstance(item);
+        } else {
+            showConfirmationMessage(item);
+        }
+    }
+
     private void switchToInstance(InstanceInfo item) {
         if (item.type == InstanceInfo.Type.CURRENT) {
             Toast.makeText(
@@ -381,7 +400,7 @@
      * </ul>
      */
     private void updateCommandUiState() {
-        if (!isInstanceSwitcherV2Enabled()) return;
+        if (!UiUtils.isInstanceSwitcherV2Enabled()) return;
         int numActiveInstances = mActiveModelList.size();
         int numInactiveInstances = mInactiveModelList.size();
         if (mNewWindowEnabled) {
@@ -427,7 +446,7 @@
     }
 
     private int getTotalInstanceCount() {
-        if (!isInstanceSwitcherV2Enabled()) {
+        if (!UiUtils.isInstanceSwitcherV2Enabled()) {
             // Exclude COMMAND item from list size.
             return mModelList.size() - 1;
         }
@@ -443,7 +462,7 @@
     private void removeInstance(InstanceInfo item) {
         int instanceId = item.instanceId;
 
-        if (isInstanceSwitcherV2Enabled()) {
+        if (UiUtils.isInstanceSwitcherV2Enabled()) {
             removeItemFromModelList(
                     instanceId,
                     item.taskId == INVALID_TASK_ID ? mInactiveModelList : mActiveModelList);
@@ -529,8 +548,4 @@
         inactiveTab.setText(
                 mContext.getString(R.string.instance_switcher_tabs_inactive, numInactiveInstances));
     }
-
-    private static boolean isInstanceSwitcherV2Enabled() {
-        return ChromeFeatureList.isEnabled(ChromeFeatureList.INSTANCE_SWITCHER_V2);
-    }
 }
diff --git a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinatorTest.java b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinatorTest.java
index acc6fec..9001c3b 100644
--- a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinatorTest.java
+++ b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinatorTest.java
@@ -14,6 +14,7 @@
 import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
 import static androidx.test.espresso.matcher.ViewMatchers.Visibility.GONE;
 import static androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE;
+import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
 import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
@@ -28,10 +29,13 @@
 
 import android.view.View;
 
+import androidx.recyclerview.widget.RecyclerView;
 import androidx.test.espresso.UiController;
 import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.matcher.BoundedMatcher;
 import androidx.test.filters.SmallTest;
 
+import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.Matchers;
 import org.junit.After;
@@ -42,6 +46,7 @@
 
 import org.chromium.base.Callback;
 import org.chromium.base.ThreadUtils;
+import org.chromium.base.TimeUtils;
 import org.chromium.base.test.BaseActivityTestRule;
 import org.chromium.base.test.util.CallbackHelper;
 import org.chromium.base.test.util.CommandLineFlags;
@@ -373,7 +378,6 @@
 
         final CallbackHelper closeCallbackHelper = new CallbackHelper();
         Callback<InstanceInfo> closeCallback = (item) -> closeCallbackHelper.notifyCalled();
-        InstanceSwitcherCoordinator.setSkipCloseConfirmation();
 
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
@@ -507,6 +511,57 @@
                 });
     }
 
+    @Test
+    @SmallTest
+    @EnableFeatures(ChromeFeatureList.INSTANCE_SWITCHER_V2)
+    public void testLastAccessedStringsForInstances() {
+        final String expectedCurrentString = "Current window";
+        final String expectedOtherString = "2 days ago";
+
+        InstanceInfo[] instances =
+                createPersistedInstances(
+                        /* numActiveInstances= */ 3, /* numInactiveInstances= */ 0);
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    InstanceSwitcherCoordinator.showDialog(
+                            mActivityTestRule.getActivity(),
+                            mModalDialogManager,
+                            mIconBridge,
+                            null,
+                            null,
+                            null,
+                            MAX_INSTANCE_COUNT,
+                            Arrays.asList(instances));
+                });
+
+        // Verify that the "Current window" string is at position 0.
+        onView(withId(R.id.active_instance_list))
+                .inRoot(isDialog())
+                .check(
+                        matches(
+                                atPosition(
+                                        0,
+                                        hasDescendant(
+                                                allOf(
+                                                        withId(R.id.last_accessed),
+                                                        withText(expectedCurrentString),
+                                                        isDisplayed())))));
+
+        // Verify that the "2 days ago" string is at position 2.
+        onView(withId(R.id.active_instance_list))
+                .inRoot(isDialog())
+                .check(
+                        matches(
+                                atPosition(
+                                        2,
+                                        hasDescendant(
+                                                allOf(
+                                                        withId(R.id.last_accessed),
+                                                        withText(expectedOtherString),
+                                                        isDisplayed())))));
+    }
+
     private InstanceInfo[] createPersistedInstances(
             int numActiveInstances, int numInactiveInstances) {
         int totalInstances = numActiveInstances + numInactiveInstances;
@@ -530,7 +585,7 @@
                             1,
                             0,
                             false,
-                            0);
+                            getDaysAgoMillis(i));
         }
 
         // Create inactive instances.
@@ -543,6 +598,11 @@
         return instances;
     }
 
+    /* Returns the millisecond timestamp for a given number of days in the past. */
+    private long getDaysAgoMillis(int lastAccessedDaysAgo) {
+        return TimeUtils.currentTimeMillis() - lastAccessedDaysAgo * 24L * 60L * 60L * 1000L;
+    }
+
     /* For use in instance switcher v1. */
     private void closeInstanceAt(int position, CallbackHelper closeCallbackHelper)
             throws TimeoutException {
@@ -576,18 +636,40 @@
 
                                     @Override
                                     public String getDescription() {
-                                        return "Click on the more menu.";
+                                        return "Click on the close button.";
                                     }
 
                                     @Override
                                     public void perform(UiController uiController, View view) {
-                                        View v = view.findViewById(R.id.more);
+                                        View v = view.findViewById(R.id.close_button);
                                         v.performClick();
                                     }
                                 }));
-        onView(withText(R.string.instance_switcher_close_window))
-                .inRoot(withDecorView(withClassName(containsString("Popup"))))
+        onView(withText(R.string.close))
+                .inRoot(isDialog())
+                .check(matches(isDisplayed()))
                 .perform(click());
         closeCallbackHelper.waitForCallback(closeCallbackCount);
     }
+
+    private static Matcher<View> atPosition(final int position, final Matcher<View> itemMatcher) {
+        return new BoundedMatcher<>(RecyclerView.class) {
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("has item at position " + position + ": ");
+                itemMatcher.describeTo(description);
+            }
+
+            @Override
+            protected boolean matchesSafely(final RecyclerView view) {
+                RecyclerView.ViewHolder viewHolder =
+                        view.findViewHolderForAdapterPosition(position);
+                if (viewHolder == null) {
+                    // Has no item at this position.
+                    return false;
+                }
+                return itemMatcher.matches(viewHolder.itemView);
+            }
+        };
+    }
 }
diff --git a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherItemProperties.java b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherItemProperties.java
index a28dd24..0c99ddd0 100644
--- a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherItemProperties.java
+++ b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherItemProperties.java
@@ -39,6 +39,12 @@
     public static final PropertyModel.WritableObjectPropertyKey<ListMenuDelegate> MORE_MENU =
             new PropertyModel.WritableObjectPropertyKey<>();
 
+    public static final PropertyModel.WritableObjectPropertyKey<String> LAST_ACCESSED =
+            new PropertyModel.WritableObjectPropertyKey<>();
+
+    public static final PropertyModel.WritableObjectPropertyKey<View.OnClickListener>
+            CLOSE_BUTTON_CLICK_LISTENER = new PropertyModel.WritableObjectPropertyKey<>();
+
     public static final PropertyKey[] ALL_KEYS =
             new PropertyKey[] {
                 CURRENT,
@@ -48,6 +54,8 @@
                 DESC,
                 INSTANCE_ID,
                 CLICK_LISTENER,
-                MORE_MENU
+                MORE_MENU,
+                LAST_ACCESSED,
+                CLOSE_BUTTON_CLICK_LISTENER
             };
 }
diff --git a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherItemViewBinder.java b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherItemViewBinder.java
index cc2dcf4..7d71009 100644
--- a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherItemViewBinder.java
+++ b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherItemViewBinder.java
@@ -46,7 +46,6 @@
             view.findViewById(R.id.current).setVisibility(current ? View.VISIBLE : View.INVISIBLE);
             // Do not show 3-dot submenu for the current instance.
             view.findViewById(R.id.more).setVisibility(current ? View.INVISIBLE : View.VISIBLE);
-
         } else if (InstanceSwitcherItemProperties.CLICK_LISTENER == propertyKey) {
             view.setOnClickListener(model.get(InstanceSwitcherItemProperties.CLICK_LISTENER));
 
@@ -62,6 +61,15 @@
                 View maxInfo = view.findViewById(R.id.max_info);
                 maxInfo.setVisibility(enabled ? View.GONE : View.VISIBLE);
             }
+
+        } else if (InstanceSwitcherItemProperties.LAST_ACCESSED == propertyKey) {
+            TextView lastAccessedView = view.findViewById(R.id.last_accessed);
+            String text = model.get(InstanceSwitcherItemProperties.LAST_ACCESSED);
+            lastAccessedView.setText(text);
+        } else if (InstanceSwitcherItemProperties.CLOSE_BUTTON_CLICK_LISTENER == propertyKey) {
+            ImageView closeButton = view.findViewById(R.id.close_button);
+            closeButton.setOnClickListener(
+                    model.get(InstanceSwitcherItemProperties.CLOSE_BUTTON_CLICK_LISTENER));
         }
     }
 }
diff --git a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/TargetSelectorCoordinatorTest.java b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/TargetSelectorCoordinatorTest.java
index 08a1695..0b20dc0 100644
--- a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/TargetSelectorCoordinatorTest.java
+++ b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/TargetSelectorCoordinatorTest.java
@@ -24,6 +24,8 @@
 import org.chromium.base.test.BaseActivityTestRule;
 import org.chromium.base.test.util.CallbackHelper;
 import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.Features.DisableFeatures;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.components.browser_ui.modaldialog.AppModalPresenter;
@@ -72,6 +74,7 @@
 
     @Test
     @SmallTest
+    @DisableFeatures(ChromeFeatureList.INSTANCE_SWITCHER_V2)
     public void testTargetSelectorCoordinatorTest_moveWindow() throws Exception {
         InstanceInfo[] instances =
                 new InstanceInfo[] {
diff --git a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/UiUtils.java b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/UiUtils.java
index fb26066..8cf273a 100644
--- a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/UiUtils.java
+++ b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/UiUtils.java
@@ -16,6 +16,7 @@
 
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.build.annotations.Nullable;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.ui.favicon.FaviconUtils;
 import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
 import org.chromium.components.favicon.LargeIconBridge;
@@ -84,9 +85,9 @@
         int totalTabCount = totalTabCount(item);
         String desc;
         Resources res = mContext.getResources();
-        if (item.type == InstanceInfo.Type.CURRENT) {
+        if (item.type == InstanceInfo.Type.CURRENT && !isInstanceSwitcherV2Enabled()) {
             desc = res.getString(R.string.instance_switcher_current_window);
-        } else if (item.type == InstanceInfo.Type.ADJACENT) {
+        } else if (item.type == InstanceInfo.Type.ADJACENT && !isInstanceSwitcherV2Enabled()) {
             desc = res.getString(R.string.instance_switcher_adjacent_window);
         } else if (totalTabCount == 0) { // <ex>No tabs</ex>
             desc = res.getString(R.string.instance_switcher_tab_count_zero);
@@ -168,6 +169,7 @@
 
     /**
      * Set the favicon for the given instance.
+     *
      * @param model {@link PropertyModel} that represents the instance entry.
      * @param faviconKey Property key for favicon item in the model.
      * @param item {@link InstanceInfo} object for the given instance.
@@ -229,4 +231,13 @@
                     DialogDismissalCause.NAVIGATE_BACK_OR_TOUCH_OUTSIDE);
         }
     }
+
+    /**
+     * Checks whether the Instance Switcher V2 feature is enabled.
+     *
+     * @return {@code true} if the Instance Switcher V2 feature is enabled, {@code false} otherwise.
+     */
+    public static boolean isInstanceSwitcherV2Enabled() {
+        return ChromeFeatureList.isEnabled(ChromeFeatureList.INSTANCE_SWITCHER_V2);
+    }
 }
diff --git a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/UiUtilsUnitTest.java b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/UiUtilsUnitTest.java
index d8d328c..407f8b4 100644
--- a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/UiUtilsUnitTest.java
+++ b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/UiUtilsUnitTest.java
@@ -17,6 +17,9 @@
 
 import androidx.annotation.DrawableRes;
 
+import org.chromium.base.test.util.Features.DisableFeatures;
+import org.chromium.base.test.util.Features.EnableFeatures;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -38,6 +41,7 @@
 
     // Description
     private static final String NO_TABS = "No tabs";
+    private static final String TWO_TABS_ONE_INCOGNITO = "2 tabs, 1 incognito";
     private static final String CURRENT = "Current";
     private static final String OPEN = "Window is open";
 
@@ -58,7 +62,14 @@
                 .getString(R.string.instance_switcher_entry_empty_window);
         doReturn(INCOGNITO).when(mResources).getString(R.string.notification_incognito_tab);
         doReturn(NO_TABS).when(mResources).getString(R.string.instance_switcher_tab_count_zero);
-
+        doReturn(TWO_TABS_ONE_INCOGNITO)
+                .when(mResources)
+                .getQuantityString(
+                        R.plurals.instance_switcher_desc_mixed,
+                        2,
+                        1,
+                        2,
+                        1);
         doReturn(CURRENT).when(mResources).getString(R.string.instance_switcher_current_window);
         doReturn(OPEN).when(mResources).getString(R.string.instance_switcher_adjacent_window);
         mUiUtils =
@@ -107,7 +118,108 @@
     }
 
     @Test
-    public void testItemDescription() {
+    @EnableFeatures(ChromeFeatureList.INSTANCE_SWITCHER_V2)
+    public void testItemDescriptionWithInstanceSwitcherV2() {
+        // Empty window -> No tabs
+        assertEquals(
+                "Instance with no tabs has a wrong description",
+                NO_TABS,
+                mUiUtils.getItemDesc(mockInstance(57, 0, 0, false)));
+
+        // Current instance -> 2 tabs, 1 incognito
+        assertEquals(
+                "Current instance has a wrong description",
+                TWO_TABS_ONE_INCOGNITO,
+                mUiUtils.getItemDesc(mockInstance(InstanceInfo.Type.CURRENT)));
+
+        // Other visible instance -> 2 tabs, 1 incognito
+        assertEquals(
+                "Visible instance has a wrong description",
+                TWO_TABS_ONE_INCOGNITO,
+                mUiUtils.getItemDesc(mockInstance(InstanceInfo.Type.ADJACENT)));
+
+        // Normal tabs only -> # of tabs
+        int normalTabCount = 3;
+        int incognitoTabCount = 0;
+        int totalTabCount = 3;
+        InstanceInfo item = mockInstance(57, normalTabCount, incognitoTabCount, false);
+        mUiUtils.getItemDesc(item);
+        verify(mResources)
+                .getQuantityString(
+                        R.plurals.instance_switcher_tab_count_nonzero,
+                        item.tabCount,
+                        item.tabCount);
+        clearInvocations(mResources);
+
+        // Mixed tabs -> # tabs, # incognito
+        normalTabCount = 3;
+        incognitoTabCount = 2;
+        totalTabCount = 5;
+        item = mockInstance(57, normalTabCount, incognitoTabCount, false);
+        mUiUtils.getItemDesc(item);
+        verify(mResources)
+                .getQuantityString(
+                        R.plurals.instance_switcher_desc_mixed,
+                        totalTabCount,
+                        incognitoTabCount,
+                        totalTabCount,
+                        incognitoTabCount);
+        clearInvocations(mResources);
+
+        // Incognito-selected, incognito tab only -> # incognito tabs
+        incognitoTabCount = 4;
+        item = mockInstance(57, 0, incognitoTabCount, true);
+        mUiUtils.getItemDesc(item);
+        verify(mResources)
+                .getQuantityString(
+                        R.plurals.instance_switcher_desc_incognito,
+                        incognitoTabCount,
+                        incognitoTabCount);
+        clearInvocations(mResources);
+
+        // Incognito-selected, mixed tabs -> # tabs, # incognito
+        normalTabCount = 7;
+        incognitoTabCount = 13;
+        totalTabCount = 7 + 13;
+        item = mockInstance(57, normalTabCount, incognitoTabCount, true);
+        mUiUtils.getItemDesc(item);
+        verify(mResources)
+                .getQuantityString(
+                        R.plurals.instance_switcher_desc_mixed,
+                        totalTabCount,
+                        incognitoTabCount,
+                        totalTabCount,
+                        incognitoTabCount);
+        clearInvocations(mResources);
+
+        // Disregard incognito tab count for a killed task -> # tabs
+        normalTabCount = 3;
+        incognitoTabCount = 2;
+        item = mockInstance(UiUtils.INVALID_TASK_ID, normalTabCount, incognitoTabCount, false);
+        mUiUtils.getItemDesc(item);
+        verify(mResources)
+                .getQuantityString(
+                        R.plurals.instance_switcher_tab_count_nonzero,
+                        normalTabCount,
+                        normalTabCount);
+
+        clearInvocations(mResources);
+
+        // Incognito-selected, mixed tabs, killed task -> # tabs
+        normalTabCount = 2;
+        incognitoTabCount = 2;
+        item = mockInstance(UiUtils.INVALID_TASK_ID, normalTabCount, incognitoTabCount, true);
+        mUiUtils.getItemDesc(item);
+        verify(mResources)
+                .getQuantityString(
+                        R.plurals.instance_switcher_tab_count_nonzero,
+                        normalTabCount,
+                        normalTabCount);
+    }
+
+    @Test
+    @DisableFeatures(ChromeFeatureList.INSTANCE_SWITCHER_V2)
+    public void testItemDescriptionWithoutInstanceSwitcherV2() {
         // Empty window -> No tabs
         assertEquals(
                 "Instance with no tabs has a wrong description",
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index 89be366d..7aafe2f 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -5821,6 +5821,9 @@
           =1 {<ph name="INCOGNITO_TAB_COUNT">%1$s<ex>2</ex></ph> incognito and <ph name="TAB_COUNT_ONE">%2$s<ex>1</ex></ph> more tab will be closed}
           other {<ph name="INCOGNITO_TAB_COUNT">%1$s<ex>4</ex></ph> incognito and <ph name="TAB_COUNT_MANY">%2$s<ex>4</ex></ph> more tabs will be closed}}
       </message>
+      <message name="IDS_INSTANCE_LAST_ACCESSED_CURRENT" desc="The current window indicator text.">
+        Current window
+      </message>
 
       <!-- Multi-instance message strings -->
       <message name="IDS_MULTI_INSTANCE_RESTORATION_MESSAGE_TITLE" desc="Title of the message shown when the number of persisted instances exceeds the max instance count after an instance limit downgrade.">
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_INSTANCE_LAST_ACCESSED_CURRENT.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_INSTANCE_LAST_ACCESSED_CURRENT.png.sha1
new file mode 100644
index 0000000..ee5a7a4
--- /dev/null
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_INSTANCE_LAST_ACCESSED_CURRENT.png.sha1
@@ -0,0 +1 @@
+b3daa96cffc5182fbc74d859d12970f3874d09f3
\ No newline at end of file
diff --git a/chrome/browser/ui/app_icon_loader.cc b/chrome/browser/ui/app_icon_loader.cc
index b8b85ab5..965f4f9 100644
--- a/chrome/browser/ui/app_icon_loader.cc
+++ b/chrome/browser/ui/app_icon_loader.cc
@@ -6,11 +6,8 @@
 
 AppIconLoader::AppIconLoader() = default;
 
-AppIconLoader::AppIconLoader(Profile* profile,
-                             int icon_size_in_dip,
+AppIconLoader::AppIconLoader(int icon_size_in_dip,
                              AppIconLoaderDelegate* delegate)
-    : profile_(profile),
-      icon_size_in_dip_(icon_size_in_dip),
-      delegate_(delegate) {}
+    : icon_size_in_dip_(icon_size_in_dip), delegate_(delegate) {}
 
 AppIconLoader::~AppIconLoader() = default;
diff --git a/chrome/browser/ui/app_icon_loader.h b/chrome/browser/ui/app_icon_loader.h
index 4f4c727d..7d4fbef 100644
--- a/chrome/browser/ui/app_icon_loader.h
+++ b/chrome/browser/ui/app_icon_loader.h
@@ -10,8 +10,6 @@
 #include "base/memory/raw_ptr.h"
 #include "chrome/browser/ui/app_icon_loader_delegate.h"
 
-class Profile;
-
 // Base class that loads and updates Chrome app's icons.
 // TODO(khmel): Switch to using ChromeAppIconService instead ChromeAppIconLoader
 // and ArcAppIconLoader.
@@ -40,16 +38,12 @@
 
  protected:
   AppIconLoader();
-  AppIconLoader(Profile* profile,
-                int icon_size_in_dip,
-                AppIconLoaderDelegate* delegate);
+  AppIconLoader(int icon_size_in_dip, AppIconLoaderDelegate* delegate);
 
-  Profile* profile() { return profile_; }
   int icon_size_in_dip() const { return icon_size_in_dip_; }
   AppIconLoaderDelegate* delegate() { return delegate_; }
 
  private:
-  const raw_ptr<Profile, DanglingUntriaged> profile_ = nullptr;
   const int icon_size_in_dip_ = 0;
 
   // The delegate object which receives the icon images. No ownership.
diff --git a/chrome/browser/ui/ash/quick_answers/BUILD.gn b/chrome/browser/ui/ash/quick_answers/BUILD.gn
index 520bf74..606acc62 100644
--- a/chrome/browser/ui/ash/quick_answers/BUILD.gn
+++ b/chrome/browser/ui/ash/quick_answers/BUILD.gn
@@ -12,6 +12,10 @@
     "quick_answers_state_ash.h",
     "quick_answers_ui_controller.cc",
     "quick_answers_ui_controller.h",
+    "ui/magic_boost_header.cc",
+    "ui/magic_boost_header.h",
+    "ui/magic_boost_user_consent_view.cc",
+    "ui/magic_boost_user_consent_view.h",
     "ui/quick_answers_text_label.cc",
     "ui/quick_answers_text_label.h",
     "ui/quick_answers_util.cc",
diff --git a/chrome/browser/ui/ash/quick_answers/quick_answers_controller_impl.cc b/chrome/browser/ui/ash/quick_answers/quick_answers_controller_impl.cc
index 733e6fb7..aa84946 100644
--- a/chrome/browser/ui/ash/quick_answers/quick_answers_controller_impl.cc
+++ b/chrome/browser/ui/ash/quick_answers/quick_answers_controller_impl.cc
@@ -20,6 +20,7 @@
 #include "chromeos/components/quick_answers/public/cpp/quick_answers_state.h"
 #include "chromeos/components/quick_answers/quick_answers_client.h"
 #include "chromeos/components/quick_answers/quick_answers_model.h"
+#include "chromeos/constants/chromeos_features.h"
 #include "chromeos/strings/grit/chromeos_strings.h"
 #include "components/prefs/pref_service.h"
 #include "components/user_manager/user_manager.h"
@@ -87,23 +88,22 @@
     return true;
   }
 
-  // If feature type is `kQuickAnswers`, return `true` for the case `kUnknown`
-  // to show a consent UI.
   if (QuickAnswersState::GetFeatureType() ==
-      QuickAnswersState::FeatureType::kQuickAnswers) {
-    base::expected<quick_answers::prefs::ConsentStatus,
-                   QuickAnswersState::Error>
-        maybe_consent_status = QuickAnswersState::GetConsentStatus();
-    if (!maybe_consent_status.has_value()) {
-      return false;
-    }
-
-    if (maybe_consent_status.value() ==
-        quick_answers::prefs::ConsentStatus::kUnknown) {
-      return true;
-    }
+          QuickAnswersState::FeatureType::kHmr &&
+      !chromeos::features::IsMagicBoostRevampForQuickAnswersEnabled()) {
+    return false;
   }
 
+  base::expected<quick_answers::prefs::ConsentStatus, QuickAnswersState::Error>
+      maybe_consent_status = QuickAnswersState::GetConsentStatus();
+  if (!maybe_consent_status.has_value()) {
+    return false;
+  }
+
+  if (maybe_consent_status.value() ==
+      quick_answers::prefs::ConsentStatus::kUnknown) {
+    return true;
+  }
   return false;
 }
 
@@ -575,10 +575,9 @@
 bool QuickAnswersControllerImpl::MaybeShowUserConsent(
     IntentType intent_type,
     const std::u16string& intent_text) {
-  // For non-QuickAnswers case (i.e., HMR), user consent is handled outside of
-  // QuickAnswers code.
-  if (QuickAnswersState::GetFeatureType() !=
-      QuickAnswersState::FeatureType::kQuickAnswers) {
+  if (QuickAnswersState::GetFeatureType() ==
+          QuickAnswersState::FeatureType::kHmr &&
+      !chromeos::features::IsMagicBoostRevampForQuickAnswersEnabled()) {
     return false;
   }
 
diff --git a/chrome/browser/ui/ash/quick_answers/quick_answers_ui_controller.cc b/chrome/browser/ui/ash/quick_answers/quick_answers_ui_controller.cc
index cefe4f5..c0bba2c 100644
--- a/chrome/browser/ui/ash/quick_answers/quick_answers_ui_controller.cc
+++ b/chrome/browser/ui/ash/quick_answers/quick_answers_ui_controller.cc
@@ -18,6 +18,7 @@
 #include "base/strings/stringprintf.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/ash/quick_answers/quick_answers_controller_impl.h"
+#include "chrome/browser/ui/ash/quick_answers/ui/magic_boost_user_consent_view.h"
 #include "chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.h"
 #include "chrome/browser/ui/ash/quick_answers/ui/quick_answers_view.h"
 #include "chrome/browser/ui/ash/quick_answers/ui/rich_answers_definition_view.h"
@@ -280,23 +281,36 @@
   CHECK_EQ(controller_->GetQuickAnswersVisibility(),
            QuickAnswersVisibility::kPending);
 
-  auto* view = GetReadWriteCardsUiController().SetQuickAnswersUi(
-      views::Builder<quick_answers::UserConsentView>(
-          std::make_unique<quick_answers::UserConsentView>(
-              use_refreshed_design, GetReadWriteCardsUiController()))
-          .SetIntentType(intent_type)
-          .SetIntentText(intent_text)
-          // It is safe to do `base::Unretained(this)`. UIs are destructed
-          // before a UI controller gets destructed. See
-          // `~QuickAnswersUiController`.
-          .SetNoThanksButtonPressed(base::BindRepeating(
-              &QuickAnswersUiController::OnUserConsentNoThanksPressed,
-              base::Unretained(this)))
-          .SetAllowButtonPressed(base::BindRepeating(
-              &QuickAnswersUiController::OnUserConsentAllowPressed,
-              base::Unretained(this)))
-          .Build());
-  user_consent_view_.SetView(view);
+  if (chromeos::features::IsMagicBoostRevampForQuickAnswersEnabled() &&
+      QuickAnswersState::GetFeatureType() ==
+          QuickAnswersState::FeatureType::kHmr) {
+    user_consent_view_.SetView(
+        GetReadWriteCardsUiController().SetQuickAnswersUi(
+            views::Builder<quick_answers::MagicBoostUserConsentView>(
+                std::make_unique<quick_answers::MagicBoostUserConsentView>(
+                    // TODO: crbug.com/414391121 - Populate the button label
+                    // with the correct text.
+                    intent_text, GetReadWriteCardsUiController()))
+                .Build()));
+  } else {
+    user_consent_view_.SetView(
+        GetReadWriteCardsUiController().SetQuickAnswersUi(
+            views::Builder<quick_answers::UserConsentView>(
+                std::make_unique<quick_answers::UserConsentView>(
+                    use_refreshed_design, GetReadWriteCardsUiController()))
+                .SetIntentType(intent_type)
+                .SetIntentText(intent_text)
+                // It is safe to do `base::Unretained(this)`. UIs are destructed
+                // before a UI controller gets destructed. See
+                // `~QuickAnswersUiController`.
+                .SetNoThanksButtonPressed(base::BindRepeating(
+                    &QuickAnswersUiController::OnUserConsentNoThanksPressed,
+                    base::Unretained(this)))
+                .SetAllowButtonPressed(base::BindRepeating(
+                    &QuickAnswersUiController::OnUserConsentAllowPressed,
+                    base::Unretained(this)))
+                .Build()));
+  }
 
   // `ViewAccessibility::AnnounceText` requires a root view. Announce text after
   // a view gets attached to a widget.
diff --git a/chrome/browser/ui/ash/quick_answers/ui/magic_boost_header.cc b/chrome/browser/ui/ash/quick_answers/ui/magic_boost_header.cc
new file mode 100644
index 0000000..dbafd82
--- /dev/null
+++ b/chrome/browser/ui/ash/quick_answers/ui/magic_boost_header.cc
@@ -0,0 +1,60 @@
+// Copyright 2025 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/ui/ash/quick_answers/ui/magic_boost_header.h"
+
+#include "ash/style/typography.h"
+#include "chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.h"
+#include "chromeos/components/magic_boost/public/cpp/views/experiment_badge.h"
+#include "chromeos/components/quick_answers/public/cpp/constants.h"
+#include "chromeos/strings/grit/chromeos_strings.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/gfx/geometry/insets.h"
+#include "ui/views/controls/label.h"
+#include "ui/views/layout/flex_layout.h"
+#include "ui/views/layout/layout_provider.h"
+#include "ui/views/layout/layout_types.h"
+#include "ui/views/style/typography_provider.h"
+#include "ui/views/view.h"
+#include "ui/views/view_class_properties.h"
+#include "ui/views/view_utils.h"
+
+namespace quick_answers {
+
+views::Builder<views::BoxLayoutView> GetMagicBoostHeader() {
+  int line_height = ash::TypographyProvider::Get()->ResolveLineHeight(
+      ash::TypographyToken::kCrosAnnotation1);
+  int vertical_padding = std::max(0, (20 - line_height) / 2);
+
+  return views::Builder<views::BoxLayoutView>()
+      .SetProperty(
+          views::kMarginsKey,
+          gfx::Insets::TLBR(
+              0, 0,
+              views::LayoutProvider::Get()->GetDistanceMetric(
+                  views::DistanceMetric::DISTANCE_RELATED_CONTROL_VERTICAL),
+              GetButtonsViewOcclusion(Design::kMagicBoost)))
+      .SetOrientation(views::LayoutOrientation::kHorizontal)
+      .SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
+      .SetProperty(
+          views::kFlexBehaviorKey,
+          views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
+                                   views::MaximumFlexSizeRule::kPreferred))
+      .SetBetweenChildSpacing(views::LayoutProvider::Get()->GetDistanceMetric(
+          views::DistanceMetric::DISTANCE_RELATED_BUTTON_HORIZONTAL))
+      .AddChild(
+          views::Builder<views::Label>()
+              .SetText(l10n_util::GetStringUTF16(IDS_ASH_MAHI_MENU_TITLE))
+              .SetLineHeight(line_height)
+              .SetProperty(views::kMarginsKey,
+                           gfx::Insets::VH(vertical_padding, 0))
+              .SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT)
+              .SetFontList(ash::TypographyProvider::Get()
+                               ->ResolveTypographyToken(
+                                   ash::TypographyToken::kCrosAnnotation1)
+                               .DeriveWithWeight(gfx::Font::Weight::MEDIUM)))
+      .AddChild(views::Builder<chromeos::ExperimentBadge>());
+}
+
+}  // namespace quick_answers
diff --git a/chrome/browser/ui/ash/quick_answers/ui/magic_boost_header.h b/chrome/browser/ui/ash/quick_answers/ui/magic_boost_header.h
new file mode 100644
index 0000000..92cab710
--- /dev/null
+++ b/chrome/browser/ui/ash/quick_answers/ui/magic_boost_header.h
@@ -0,0 +1,16 @@
+// Copyright 2025 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_UI_ASH_QUICK_ANSWERS_UI_MAGIC_BOOST_HEADER_H_
+#define CHROME_BROWSER_UI_ASH_QUICK_ANSWERS_UI_MAGIC_BOOST_HEADER_H_
+
+#include "ui/views/layout/box_layout_view.h"
+
+namespace quick_answers {
+
+views::Builder<views::BoxLayoutView> GetMagicBoostHeader();
+
+}
+
+#endif  // CHROME_BROWSER_UI_ASH_QUICK_ANSWERS_UI_MAGIC_BOOST_HEADER_H_
diff --git a/chrome/browser/ui/ash/quick_answers/ui/magic_boost_user_consent_view.cc b/chrome/browser/ui/ash/quick_answers/ui/magic_boost_user_consent_view.cc
new file mode 100644
index 0000000..c58db4ec
--- /dev/null
+++ b/chrome/browser/ui/ash/quick_answers/ui/magic_boost_user_consent_view.cc
@@ -0,0 +1,194 @@
+// Copyright 2025 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/ui/ash/quick_answers/ui/magic_boost_user_consent_view.h"
+
+#include <memory>
+#include <string>
+
+#include "base/functional/bind.h"
+#include "base/notreached.h"
+#include "chrome/browser/ui/ash/editor_menu/utils/pre_target_handler.h"
+#include "chrome/browser/ui/ash/quick_answers/quick_answers_ui_controller.h"
+#include "chrome/browser/ui/ash/quick_answers/ui/magic_boost_header.h"
+#include "chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.h"
+#include "chrome/browser/ui/ash/quick_answers/ui/typography.h"
+#include "chrome/browser/ui/ash/read_write_cards/read_write_cards_ui_controller.h"
+#include "chrome/browser/ui/ash/read_write_cards/read_write_cards_view.h"
+#include "chromeos/components/quick_answers/public/cpp/quick_answers_state.h"
+#include "chromeos/strings/grit/chromeos_strings.h"
+#include "chromeos/ui/vector_icons/vector_icons.h"
+#include "components/vector_icons/vector_icons.h"
+#include "ui/aura/window.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/base/metadata/metadata_impl_macros.h"
+#include "ui/base/models/image_model.h"
+#include "ui/base/ui_base_types.h"
+#include "ui/color/color_id.h"
+#include "ui/events/event_handler.h"
+#include "ui/gfx/geometry/insets.h"
+#include "ui/gfx/paint_vector_icon.h"
+#include "ui/gfx/text_constants.h"
+#include "ui/views/accessibility/view_accessibility.h"
+#include "ui/views/background.h"
+#include "ui/views/border.h"
+#include "ui/views/controls/button/image_button.h"
+#include "ui/views/controls/image_view.h"
+#include "ui/views/controls/label.h"
+#include "ui/views/controls/menu/menu_config.h"
+#include "ui/views/controls/menu/menu_controller.h"
+#include "ui/views/layout/fill_layout.h"
+#include "ui/views/layout/flex_layout.h"
+#include "ui/views/layout/flex_layout_types.h"
+#include "ui/views/layout/flex_layout_view.h"
+#include "ui/views/layout/layout_provider.h"
+#include "ui/views/layout/layout_types.h"
+#include "ui/views/metadata/view_factory.h"
+#include "ui/views/metadata/view_factory_internal.h"
+#include "ui/views/style/typography.h"
+#include "ui/views/view_class_properties.h"
+#include "ui/views/widget/tooltip_manager.h"
+#include "ui/views/widget/widget.h"
+#include "ui/wm/core/coordinate_conversion.h"
+
+namespace quick_answers {
+
+namespace {
+
+// Main view (or common) specs.
+constexpr auto kContentViewInsets = gfx::Insets::TLBR(0, 8, 0, 0);
+constexpr auto kButtonPadding = gfx::Insets::VH(6, 8);
+constexpr auto kSettingsViewInsets = gfx::Insets::TLBR(12, 16, 16, 16);
+constexpr auto kChipMargin = gfx::Insets::TLBR(0, 0, 0, 10);
+
+constexpr int kButtonBorderThickness = 1;
+constexpr int kButtonCornerRadius = 8;
+constexpr int kSettingsButtonSizeDip = 14;
+constexpr int kSettingsButtonBorderDip = 3;
+
+// TODO: crbug.com/414391121 - Updates the accessibility name and description.
+constexpr char kConsentViewAccessibilityName[] = "Magic Boost";
+constexpr char kConsentViewAccessibilityDescription[] = "Magic Boost";
+
+// Icon.
+constexpr gfx::Insets kIntentIconInsets = gfx::Insets(8);
+
+}  // namespace
+
+// MagicBoostUserConsentView
+// -------------------------------------------------------------
+
+MagicBoostUserConsentView::MagicBoostUserConsentView(
+    const std::u16string& chip_label,
+    chromeos::ReadWriteCardsUiController& read_write_cards_ui_controller)
+    : chromeos::ReadWriteCardsView(read_write_cards_ui_controller),
+      focus_search_(
+          this,
+          base::BindRepeating(&MagicBoostUserConsentView::GetFocusableViews,
+                              base::Unretained(this))) {
+  SetUseDefaultFillLayout(true);
+  SetBackground(views::CreateSolidBackground(ui::kColorPrimaryBackground));
+
+  AddChildView(
+      views::Builder<views::FlexLayoutView>()
+          .SetOrientation(views::LayoutOrientation::kHorizontal)
+          .SetInteriorMargin(kMainViewInsets)
+          .SetCrossAxisAlignment(views::LayoutAlignment::kStart)
+          .AddChild(views::Builder<views::ImageView>().SetImage(
+              ui::ImageModel::FromVectorIcon(chromeos::kInfoSparkIcon,
+                                             ui::ColorIds::kColorSysOnSurface,
+                                             kGoogleIconSizeDip)))
+          .AddChild(
+              views::Builder<views::FlexLayoutView>()
+                  .SetInteriorMargin(kContentViewInsets)
+                  .SetProperty(views::kFlexBehaviorKey,
+                               views::FlexSpecification(
+                                   views::MinimumFlexSizeRule::kScaleToZero,
+                                   views::MaximumFlexSizeRule::kPreferred,
+                                   /*adjust_height_for_width=*/true))
+                  .SetOrientation(views::LayoutOrientation::kVertical)
+                  .AddChild(GetMagicBoostHeader())
+                  .AddChild(
+                      views::Builder<views::FlexLayoutView>()
+                          .SetInteriorMargin(kIntentIconInsets)
+                          .AddChild(
+                              views::Builder<views::LabelButton>()
+                                  .CopyAddressTo(&intent_chip_)
+                                  .SetText(chip_label)
+                                  .SetProperty(views::kMarginsKey, kChipMargin)
+                                  .SetLabelStyle(
+                                      views::style::STYLE_BODY_4_EMPHASIS)
+                                  .SetTextColor(views::LabelButton::
+                                                    ButtonState::STATE_NORMAL,
+                                                ui::kColorSysOnSurface)
+                                  .SetTextColor(views::LabelButton::
+                                                    ButtonState::STATE_DISABLED,
+                                                ui::kColorSysStateDisabled)
+                                  .SetImageLabelSpacing(4)
+                                  .SetBorder(views::CreatePaddedBorder(
+                                      views::CreateRoundedRectBorder(
+                                          kButtonBorderThickness,
+                                          kButtonCornerRadius,
+                                          ui::kColorSysTonalOutline),
+                                      kButtonPadding))
+                                  .SetEnabled(true))))
+          .Build());
+
+  AddChildView(
+      views::Builder<views::BoxLayoutView>()
+          .SetOrientation(views::LayoutOrientation::kHorizontal)
+          .SetMainAxisAlignment(views::LayoutAlignment::kEnd)
+          .SetCrossAxisAlignment(views::LayoutAlignment::kStart)
+          .SetInsideBorderInsets(kSettingsViewInsets)
+          .AddChild(
+              views::Builder<views::ImageButton>()
+                  .SetTooltipText(l10n_util::GetStringUTF16(
+                      IDS_RICH_ANSWERS_VIEW_SETTINGS_BUTTON_A11Y_NAME_TEXT))
+                  .SetImageModel(
+                      views::Button::ButtonState::STATE_NORMAL,
+                      ui::ImageModel::FromVectorIcon(
+                          vector_icons::kSettingsOutlineIcon,
+                          ui::kColorSysSecondary, kSettingsButtonSizeDip))
+                  .SetProperty(views::kMarginsKey,
+                               gfx::Insets(kSettingsButtonBorderDip)))
+          .Build());
+
+  GetViewAccessibility().SetRole(ax::mojom::Role::kDialog);
+  GetViewAccessibility().SetDescription(kConsentViewAccessibilityName);
+  GetViewAccessibility().SetName(kConsentViewAccessibilityDescription);
+
+  // Focus should cycle to each of the buttons the view contains and back to it.
+  SetFocusBehavior(FocusBehavior::ALWAYS);
+  set_suppress_default_focus_handling();
+  views::FocusRing::Install(this);
+}
+
+MagicBoostUserConsentView::~MagicBoostUserConsentView() = default;
+
+void MagicBoostUserConsentView::OnFocus() {}
+
+views::FocusTraversable* MagicBoostUserConsentView::GetPaneFocusTraversable() {
+  return &focus_search_;
+}
+
+void MagicBoostUserConsentView::UpdateBoundsForQuickAnswers() {}
+
+std::u16string MagicBoostUserConsentView::chip_label_for_testing() {
+  return intent_chip_ == nullptr ? u"" : intent_chip_->GetText().data();
+}
+
+std::vector<views::View*> MagicBoostUserConsentView::GetFocusableViews() {
+  std::vector<views::View*> focusable_views;
+  // The view itself is not included in focus loop, unless screen-reader is on.
+  if (QuickAnswersState::Get()->spoken_feedback_enabled()) {
+    focusable_views.push_back(this);
+  }
+  focusable_views.push_back(intent_chip_);
+  return focusable_views;
+}
+
+BEGIN_METADATA(MagicBoostUserConsentView)
+END_METADATA
+
+}  // namespace quick_answers
diff --git a/chrome/browser/ui/ash/quick_answers/ui/magic_boost_user_consent_view.h b/chrome/browser/ui/ash/quick_answers/ui/magic_boost_user_consent_view.h
new file mode 100644
index 0000000..9cb09fb
--- /dev/null
+++ b/chrome/browser/ui/ash/quick_answers/ui/magic_boost_user_consent_view.h
@@ -0,0 +1,76 @@
+// Copyright 2025 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_UI_ASH_QUICK_ANSWERS_UI_MAGIC_BOOST_USER_CONSENT_VIEW_H_
+#define CHROME_BROWSER_UI_ASH_QUICK_ANSWERS_UI_MAGIC_BOOST_USER_CONSENT_VIEW_H_
+
+#include <memory>
+#include <string>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "chrome/browser/ui/ash/editor_menu/utils/focus_search.h"
+#include "chrome/browser/ui/ash/read_write_cards/read_write_cards_ui_controller.h"
+#include "chrome/browser/ui/ash/read_write_cards/read_write_cards_view.h"
+#include "ui/base/metadata/metadata_header_macros.h"
+#include "ui/views/controls/button/button.h"
+#include "ui/views/controls/image_view.h"
+#include "ui/views/layout/flex_layout_view.h"
+#include "ui/views/metadata/view_factory.h"
+#include "ui/views/view.h"
+#include "ui/views/widget/unique_widget_ptr.h"
+
+namespace views {
+class LabelButton;
+}  // namespace views
+
+class QuickAnswersUiController;
+
+namespace quick_answers {
+
+class MagicBoostUserConsentView : public chromeos::ReadWriteCardsView {
+  METADATA_HEADER(MagicBoostUserConsentView, chromeos::ReadWriteCardsView)
+
+ public:
+  static constexpr char kWidgetName[] = "MagicBoostUserConsentViewWidget";
+
+  // TODO: crbug.com/340628664 - remove `read_write_cards_ui_controller` arg
+  // once we stop extending `ReadWriteCardsView`.
+  explicit MagicBoostUserConsentView(
+      const std::u16string& chip_label,
+      chromeos::ReadWriteCardsUiController& read_write_cards_ui_controller);
+
+  // Disallow copy and assign.
+  MagicBoostUserConsentView(const MagicBoostUserConsentView&) = delete;
+  MagicBoostUserConsentView& operator=(const MagicBoostUserConsentView&) =
+      delete;
+
+  ~MagicBoostUserConsentView() override;
+
+  // chromeos::ReadWriteCardsView:
+  void OnFocus() override;
+  views::FocusTraversable* GetPaneFocusTraversable() override;
+  void UpdateBoundsForQuickAnswers() override;
+
+  std::u16string chip_label_for_testing();
+
+ private:
+  // FocusSearch::GetFocusableViewsCallback to poll currently focusable views.
+  std::vector<views::View*> GetFocusableViews();
+
+  base::WeakPtr<QuickAnswersUiController> controller_;
+  chromeos::editor_menu::FocusSearch focus_search_;
+  raw_ptr<views::LabelButton> intent_chip_ = nullptr;
+};
+
+BEGIN_VIEW_BUILDER(/* no export */,
+                   MagicBoostUserConsentView,
+                   chromeos::ReadWriteCardsView)
+END_VIEW_BUILDER
+
+}  // namespace quick_answers
+
+DEFINE_VIEW_BUILDER(/* no export */, quick_answers::MagicBoostUserConsentView)
+
+#endif  // CHROME_BROWSER_UI_ASH_QUICK_ANSWERS_UI_MAGIC_BOOST_USER_CONSENT_VIEW_H_
diff --git a/chrome/browser/ui/ash/quick_answers/ui/magic_boost_user_consent_view_unittest.cc b/chrome/browser/ui/ash/quick_answers/ui/magic_boost_user_consent_view_unittest.cc
new file mode 100644
index 0000000..6d1cbd00c
--- /dev/null
+++ b/chrome/browser/ui/ash/quick_answers/ui/magic_boost_user_consent_view_unittest.cc
@@ -0,0 +1,23 @@
+// Copyright 2025 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/ui/ash/quick_answers/ui/magic_boost_user_consent_view.h"
+
+#include "chrome/browser/ui/ash/read_write_cards/read_write_cards_ui_controller.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/views/test/test_layout_provider.h"
+
+namespace quick_answers {
+
+TEST(MagicBoostUserConsentViewTest, ButtonTextLabel) {
+  views::test::TestLayoutProvider test_layout_provider;
+  chromeos::ReadWriteCardsUiController read_write_cards_ui_controller;
+  MagicBoostUserConsentView magic_boost_user_consent_view(
+      /*chip_label=*/u"testing label", read_write_cards_ui_controller);
+
+  EXPECT_EQ(u"testing label",
+            magic_boost_user_consent_view.chip_label_for_testing());
+}
+
+}  // namespace quick_answers
diff --git a/chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.cc b/chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.cc
index 5cb7f45..8f21d61 100644
--- a/chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.cc
+++ b/chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.cc
@@ -22,6 +22,7 @@
 #include "ui/views/layout/box_layout_view.h"
 #include "ui/views/layout/flex_layout.h"
 #include "ui/views/layout/flex_layout_view.h"
+#include "ui/views/layout/layout_provider.h"
 
 namespace {
 
@@ -236,4 +237,41 @@
   tts_controller->SpeakOrEnqueue(std::move(tts_utterance));
 }
 
+const gfx::Insets GetMainViewInsets(Design design) {
+  switch (design) {
+    case Design::kCurrent:
+      return kMainViewInsets;
+    case Design::kRefresh:
+    case Design::kMagicBoost:
+      return gfx::Insets::TLBR(12, 16, 16, 16);
+  }
+
+  NOTREACHED() << "Invalid design enum value provided";
+}
+
+const gfx::Insets GetButtonsViewInsets(Design design) {
+  switch (design) {
+    case Design::kCurrent:
+      return gfx::Insets(kButtonsViewMarginDip);
+    case Design::kRefresh:
+    case Design::kMagicBoost:
+      // Buttons view is rendered as a layer on top of main view. For `kRefresh`
+      // and `kMagicBoost`, they share the same insets.
+      return GetMainViewInsets(design);
+  }
+
+  NOTREACHED() << "Invalid design enum value provided";
+}
+
+// TODO: crbug.com/340629098 - A temporary solution until buttons view is merged
+// into headers. See another comment for buttons view in
+// `QuickAnswersView::QuickAnswersView` about details.
+int GetButtonsViewOcclusion(Design design) {
+  gfx::Insets insets_icon_button =
+      views::LayoutProvider::Get()->GetInsetsMetric(
+          views::InsetsMetric::INSETS_ICON_BUTTON);
+  return insets_icon_button.left() + kGoogleIconSizeDip +
+         insets_icon_button.right() + GetButtonsViewInsets(design).right();
+}
+
 }  // namespace quick_answers
diff --git a/chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.h b/chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.h
index 1917a26..109e783b 100644
--- a/chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.h
+++ b/chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.h
@@ -25,6 +25,14 @@
 inline constexpr int kSubContentTextWidth =
     kContentTextWidth - kSubContentViewIndent;
 
+inline constexpr int kButtonsViewMarginDip = 4;
+
+// Google icon.
+inline constexpr int kGoogleIconSizeDip = 16;
+
+// Other icons
+inline constexpr int kIconSizeDip = 16;
+
 // Spacing constants.
 inline constexpr int kContentSingleSpacing = 8;
 inline constexpr int kContentDoubleSpacing = 16;
@@ -37,6 +45,7 @@
 inline constexpr int kRichAnswersIconBorderDip = 4;
 inline constexpr gfx::Insets kSubContentViewInsets =
     gfx::Insets::TLBR(0, kSubContentViewIndent, 0, 0);
+inline constexpr gfx::Insets kMainViewInsets = gfx::Insets::TLBR(12, 8, 12, 16);
 
 // Font constants.
 inline constexpr char kGoogleSansFont[] = "Google Sans";
@@ -115,6 +124,12 @@
                       const std::string& text,
                       const std::string& locale);
 
+const gfx::Insets GetMainViewInsets(Design design);
+
+const gfx::Insets GetButtonsViewInsets(Design design);
+
+int GetButtonsViewOcclusion(Design design);
+
 }  // namespace quick_answers
 
 #endif  // CHROME_BROWSER_UI_ASH_QUICK_ANSWERS_UI_QUICK_ANSWERS_UTIL_H_
diff --git a/chrome/browser/ui/ash/quick_answers/ui/quick_answers_view.cc b/chrome/browser/ui/ash/quick_answers/ui/quick_answers_view.cc
index c8c8ce00..2f811fca 100644
--- a/chrome/browser/ui/ash/quick_answers/ui/quick_answers_view.cc
+++ b/chrome/browser/ui/ash/quick_answers/ui/quick_answers_view.cc
@@ -19,6 +19,7 @@
 #include "chrome/browser/ui/ash/editor_menu/utils/pre_target_handler.h"
 #include "chrome/browser/ui/ash/quick_answers/quick_answers_ui_controller.h"
 #include "chrome/browser/ui/ash/quick_answers/ui/loading_view.h"
+#include "chrome/browser/ui/ash/quick_answers/ui/magic_boost_header.h"
 #include "chrome/browser/ui/ash/quick_answers/ui/quick_answers_stage_button.h"
 #include "chrome/browser/ui/ash/quick_answers/ui/quick_answers_text_label.h"
 #include "chrome/browser/ui/ash/quick_answers/ui/quick_answers_util.h"
@@ -99,35 +100,18 @@
 using views::Label;
 using views::View;
 
-constexpr auto kMainViewInsets = gfx::Insets::TLBR(12, 8, 12, 16);
 constexpr auto kContentViewInsets = gfx::Insets::TLBR(0, 8, 0, 0);
 
-// Google icon.
-constexpr int kIconSizeDip = 16;
-
 // Spacing between lines in the main view.
 constexpr int kLineSpacingDip = 4;
 constexpr int kDefaultLineHeightDip = 20;
 
 // Buttons view.
-constexpr int kButtonsViewMarginDip = 4;
 constexpr int kButtonsSpacingDip = 4;
 constexpr int kDogfoodButtonSizeDip = 20;
 constexpr int kSettingsButtonSizeDip = 14;
 constexpr int kSettingsButtonBorderDip = 3;
 
-const gfx::Insets GetMainViewInsets(Design design) {
-  switch (design) {
-    case Design::kCurrent:
-      return kMainViewInsets;
-    case Design::kRefresh:
-    case Design::kMagicBoost:
-      return gfx::Insets::TLBR(12, 16, 16, 16);
-  }
-
-  NOTREACHED() << "Invalid design enum value provided";
-}
-
 const gfx::Insets GetIconInsets(Design design) {
   switch (design) {
     case Design::kCurrent:
@@ -141,20 +125,6 @@
   NOTREACHED() << "Invalid design enum value provided";
 }
 
-const gfx::Insets GetButtonsViewInsets(Design design) {
-  switch (design) {
-    case Design::kCurrent:
-      return gfx::Insets(kButtonsViewMarginDip);
-    case Design::kRefresh:
-    case Design::kMagicBoost:
-      // Buttons view is rendered as a layer on top of main view. For `kRefresh`
-      // and `kMagicBoost`, they share the same insets.
-      return GetMainViewInsets(design);
-  }
-
-  NOTREACHED() << "Invalid design enum value provided";
-}
-
 const gfx::VectorIcon& GetVectorIcon(std::optional<Intent> intent) {
   if (!intent) {
     return omnibox::kAnswerDefaultIcon;
@@ -175,8 +145,9 @@
 ui::ImageModel GetIcon(Design design, std::optional<Intent> intent) {
   switch (design) {
     case Design::kCurrent:
-      return ui::ImageModel::FromVectorIcon(
-          vector_icons::kGoogleColorIcon, gfx::kPlaceholderColor, kIconSizeDip);
+      return ui::ImageModel::FromVectorIcon(vector_icons::kGoogleColorIcon,
+                                            gfx::kPlaceholderColor,
+                                            kGoogleIconSizeDip);
     case Design::kRefresh:
       return ui::ImageModel::FromVectorIcon(
           GetVectorIcon(intent), ui::kColorSysOnSurface, kIconSizeDip);
@@ -263,17 +234,6 @@
   NOTREACHED() << "Invalid intent enum value specified";
 }
 
-// TODO(b/340629098): A temporary solution until buttons view is merged into
-// headers. See another comment for buttons view in
-// `QuickAnswersView::QuickAnswersView` about details.
-int GetButtonsViewOcclusion(Design design) {
-  gfx::Insets insets_icon_button =
-      views::LayoutProvider::Get()->GetInsetsMetric(
-          views::InsetsMetric::INSETS_ICON_BUTTON);
-  return insets_icon_button.left() + kIconSizeDip + insets_icon_button.right() +
-         GetButtonsViewInsets(design).right();
-}
-
 views::Builder<views::Label> GetRefreshUiHeader() {
   int line_height = ash::TypographyProvider::Get()->ResolveLineHeight(
       ash::TypographyToken::kCrosAnnotation1);
@@ -300,41 +260,6 @@
                                    views::MaximumFlexSizeRule::kPreferred));
 }
 
-views::Builder<views::BoxLayoutView> GetMagicBoostHeader() {
-  int line_height = ash::TypographyProvider::Get()->ResolveLineHeight(
-      ash::TypographyToken::kCrosAnnotation1);
-  int vertical_padding = std::max(0, (20 - line_height) / 2);
-
-  return views::Builder<views::BoxLayoutView>()
-      .SetProperty(
-          views::kMarginsKey,
-          gfx::Insets::TLBR(
-              0, 0,
-              views::LayoutProvider::Get()->GetDistanceMetric(
-                  views::DistanceMetric::DISTANCE_RELATED_CONTROL_VERTICAL),
-              GetButtonsViewOcclusion(Design::kMagicBoost)))
-      .SetOrientation(views::LayoutOrientation::kHorizontal)
-      .SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
-      .SetProperty(
-          views::kFlexBehaviorKey,
-          views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
-                                   views::MaximumFlexSizeRule::kPreferred))
-      .SetBetweenChildSpacing(views::LayoutProvider::Get()->GetDistanceMetric(
-          views::DistanceMetric::DISTANCE_RELATED_BUTTON_HORIZONTAL))
-      .AddChild(
-          views::Builder<views::Label>()
-              .SetText(l10n_util::GetStringUTF16(IDS_ASH_MAHI_MENU_TITLE))
-              .SetLineHeight(line_height)
-              .SetProperty(views::kMarginsKey,
-                           gfx::Insets::VH(vertical_padding, 0))
-              .SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT)
-              .SetFontList(ash::TypographyProvider::Get()
-                               ->ResolveTypographyToken(
-                                   ash::TypographyToken::kCrosAnnotation1)
-                               .DeriveWithWeight(gfx::Font::Weight::MEDIUM)))
-      .AddChild(views::Builder<chromeos::ExperimentBadge>());
-}
-
 std::string GetResultA11yDescription(ResultView* result_view,
                                      std::optional<Intent> intent,
                                      Design design) {
diff --git a/chrome/browser/ui/ash/quick_answers/ui/rich_answers_view.cc b/chrome/browser/ui/ash/quick_answers/ui/rich_answers_view.cc
index 5bb5e3d7..e187c95 100644
--- a/chrome/browser/ui/ash/quick_answers/ui/rich_answers_view.cc
+++ b/chrome/browser/ui/ash/quick_answers/ui/rich_answers_view.cc
@@ -56,9 +56,6 @@
 constexpr int kMinimumRichCardHeight = 120;
 constexpr int kMaximumRichCardHeight = 464;
 
-// View dimensions.
-constexpr auto kMainViewInsets = gfx::Insets::TLBR(20, 20, 16, 20);
-
 // Buttons view.
 constexpr int kSettingsButtonSizeDip = 20;
 
diff --git a/chrome/browser/ui/ash/quick_answers/ui/user_consent_view.cc b/chrome/browser/ui/ash/quick_answers/ui/user_consent_view.cc
index f006cf2..bf77b3b05 100644
--- a/chrome/browser/ui/ash/quick_answers/ui/user_consent_view.cc
+++ b/chrome/browser/ui/ash/quick_answers/ui/user_consent_view.cc
@@ -62,12 +62,10 @@
 // Main view (or common) specs.
 constexpr int kLineHeightDip = 20;
 constexpr int kContentSpacingDip = 8;
-constexpr auto kMainViewInsets = gfx::Insets::TLBR(16, 12, 16, 16);
 constexpr auto kContentInsets = gfx::Insets::TLBR(0, 12, 0, 0);
 constexpr auto kContentInsetsRefresh = gfx::Insets::TLBR(0, 16, 0, 0);
 
 // Icon.
-constexpr int kGoogleIconSizeDip = 16;
 constexpr int kIntentIconSizeDip = 20;
 constexpr int kIconBackgroundCornerRadiusDip = 12;
 constexpr gfx::Insets kIntentIconInsets = gfx::Insets(8);
diff --git a/chrome/browser/ui/ash/sharesheet/sharesheet_bubble_view_unittest.cc b/chrome/browser/ui/ash/sharesheet/sharesheet_bubble_view_unittest.cc
index 5e95425..3cc193c 100644
--- a/chrome/browser/ui/ash/sharesheet/sharesheet_bubble_view_unittest.cc
+++ b/chrome/browser/ui/ash/sharesheet/sharesheet_bubble_view_unittest.cc
@@ -97,6 +97,14 @@
     parent_window_ = widget->GetNativeWindow();
   }
 
+  void TearDown() override {
+    if (bubble_delegate_) {
+      bubble_delegate_->CloseBubble(::sharesheet::SharesheetResult::kCancel);
+    }
+    ASSERT_FALSE(sharesheet_widget_);
+    ChromeAshTestBase::TearDown();
+  }
+
   void ShowAndVerifyBubble(apps::IntentPtr intent,
                            ::sharesheet::LaunchSource source,
                            int num_actions_to_add = 0) {
@@ -105,7 +113,10 @@
     sharesheet_service->ShowBubbleForTesting(
         parent_window_, std::move(intent), source,
         /*delivered_callback=*/base::DoNothing(),
-        /*close_callback=*/base::DoNothing(), num_actions_to_add);
+        /*close_callback=*/
+        base::BindOnce(&SharesheetBubbleViewTest::OnClose,
+                       base::Unretained(this)),
+        num_actions_to_add);
     bubble_delegate_ = static_cast<SharesheetBubbleViewDelegate*>(
         sharesheet_service->GetUiDelegateForTesting(parent_window_));
     EXPECT_NE(bubble_delegate_, nullptr);
@@ -121,26 +132,26 @@
 
   void CloseBubble() {
     bubble_delegate_->CloseBubble(::sharesheet::SharesheetResult::kCancel);
-    // |bubble_delegate_| and |sharesheet_bubble_view_| destruct on close.
-    bubble_delegate_ = nullptr;
-    sharesheet_bubble_view_ = nullptr;
-
-    ASSERT_FALSE(IsSharesheetVisible());
+    ASSERT_FALSE(sharesheet_widget_);
   }
 
   void CloseBubbleWithEscKey() {
     GetEventGenerator()->PressAndReleaseKey(ui::VKEY_ESCAPE);
-    // |bubble_delegate_| and |sharesheet_bubble_view_| destruct on close.
-    bubble_delegate_ = nullptr;
-    sharesheet_bubble_view_ = nullptr;
-
-    ASSERT_FALSE(IsSharesheetVisible());
+    ASSERT_FALSE(sharesheet_widget_);
   }
 
-  bool IsSharesheetVisible() { return sharesheet_widget_->IsVisible(); }
+  bool IsSharesheetVisible() {
+    return sharesheet_widget_ && sharesheet_widget_->IsVisible();
+  }
 
   views::Widget* sharesheet_widget() { return sharesheet_widget_; }
 
+  void OnClose(views::Widget::ClosedReason reason) {
+    sharesheet_bubble_view_ = nullptr;
+    sharesheet_widget_ = nullptr;
+    bubble_delegate_ = nullptr;
+  }
+
   SharesheetBubbleView* sharesheet_bubble_view() {
     return sharesheet_bubble_view_;
   }
@@ -162,9 +173,9 @@
  private:
   gfx::NativeWindow parent_window_;
   std::unique_ptr<TestingProfile> profile_;
-  raw_ptr<SharesheetBubbleViewDelegate, DanglingUntriaged> bubble_delegate_;
-  raw_ptr<SharesheetBubbleView, DanglingUntriaged> sharesheet_bubble_view_;
-  raw_ptr<views::Widget, DanglingUntriaged> sharesheet_widget_;
+  raw_ptr<SharesheetBubbleViewDelegate> bubble_delegate_;
+  raw_ptr<SharesheetBubbleView> sharesheet_bubble_view_;
+  raw_ptr<views::Widget> sharesheet_widget_;
 };
 
 TEST_F(SharesheetBubbleViewTest, BubbleDoesOpenAndClose) {
diff --git a/chrome/browser/ui/ash/system/BUILD.gn b/chrome/browser/ui/ash/system/BUILD.gn
index f94ff0b..3674457 100644
--- a/chrome/browser/ui/ash/system/BUILD.gn
+++ b/chrome/browser/ui/ash/system/BUILD.gn
@@ -10,7 +10,10 @@
     "system_tray_client_impl.h",
   ]
 
-  public_deps = [ "//chrome/browser:browser_public_dependencies" ]
+  public_deps = [
+    "//chrome/browser:browser_public_dependencies",
+    "//chrome/browser/upgrade_detector",
+  ]
 
   deps = [
     "//ash/constants",
diff --git a/chrome/browser/ui/dialogs/BUILD.gn b/chrome/browser/ui/dialogs/BUILD.gn
index 7b20f77..70a44468 100644
--- a/chrome/browser/ui/dialogs/BUILD.gn
+++ b/chrome/browser/ui/dialogs/BUILD.gn
@@ -23,6 +23,7 @@
     "//chrome/app:generated_resources",
     "//chrome/browser:browser_process",
     "//chrome/browser/ui:browser_element_identifiers",
+    "//chrome/browser/upgrade_detector",
     "//chrome/common:channel_info",
     "//chrome/common:constants",
     "//content/public/browser",
diff --git a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
index 5eb06c4..70a7ee7 100644
--- a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
+++ b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
@@ -2613,6 +2613,7 @@
 // panel browsertest file.
 IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
                        SidePanel_SearchURLClickWithTextDirective) {
+  base::HistogramTester histogram_tester;
   WaitForPaint();
 
   // State should start in off.
@@ -2678,6 +2679,12 @@
   // Verify the loading state was never set.
   EXPECT_EQ(test_side_panel_coordinator->side_panel_loading_set_to_true_, 0);
   EXPECT_EQ(test_side_panel_coordinator->side_panel_loading_set_to_false_, 0);
+
+  // Record the text directive result.
+  histogram_tester.ExpectTotalCount("Lens.Overlay.TextDirectiveResult", 1);
+  histogram_tester.ExpectUniqueSample(
+      "Lens.Overlay.TextDirectiveResult",
+      lens::LensOverlayTextDirectiveResult::kOpenedInNewTab, 1);
 }
 
 // TODO(crbug.com/413042395): This test is not testing overlay logic, but
@@ -2685,6 +2692,7 @@
 // panel browsertest file.
 IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
                        SidePanel_LinkClickWithTextDirective_TextIsPresent) {
+  base::HistogramTester histogram_tester;
   WaitForPaint();
 
   // State should start in off.
@@ -2774,6 +2782,15 @@
     EXPECT_TRUE(highlighter->GetTextDirective() == "select" ||
                 highlighter->GetTextDirective() == "element");
   }
+  EXPECT_TRUE(base::test::RunUntil([&]() {
+    return histogram_tester.GetBucketCount(
+               "Lens.Overlay.TextDirectiveResult",
+               lens::LensOverlayTextDirectiveResult::kFoundOnPage) == 1;
+  }));
+  histogram_tester.ExpectTotalCount("Lens.Overlay.TextDirectiveResult", 1);
+  histogram_tester.ExpectUniqueSample(
+      "Lens.Overlay.TextDirectiveResult",
+      lens::LensOverlayTextDirectiveResult::kFoundOnPage, 1);
 }
 
 // TODO(crbug.com/413042395): This test is not testing overlay logic, but
@@ -2790,6 +2807,7 @@
 IN_PROC_BROWSER_TEST_F(
     LensOverlayControllerBrowserTest,
     MAYBE_SidePanel_LinkClickWithTextDirective_TextIsMissing) {
+  base::HistogramTester histogram_tester;
   WaitForPaint();
 
   // State should start in off.
@@ -2869,6 +2887,10 @@
                            kCheckSidePanelToastShownScript)
         .ExtractBool();
   }));
+  histogram_tester.ExpectTotalCount("Lens.Overlay.TextDirectiveResult", 1);
+  histogram_tester.ExpectUniqueSample(
+      "Lens.Overlay.TextDirectiveResult",
+      lens::LensOverlayTextDirectiveResult::kNotFoundOnPage, 1);
 }
 
 // TODO(crbug.com/413042395): This test is not testing overlay logic, but
@@ -2885,6 +2907,7 @@
 IN_PROC_BROWSER_TEST_F(
     LensOverlayControllerBrowserTest,
     MAYBE_SidePanel_LinkClickWithTextDirective_TextIsIncomplete) {
+  base::HistogramTester histogram_tester;
   WaitForPaint();
 
   // State should start in off.
@@ -2964,6 +2987,10 @@
                            kCheckSidePanelToastShownScript)
         .ExtractBool();
   }));
+  histogram_tester.ExpectTotalCount("Lens.Overlay.TextDirectiveResult", 1);
+  histogram_tester.ExpectUniqueSample(
+      "Lens.Overlay.TextDirectiveResult",
+      lens::LensOverlayTextDirectiveResult::kNotFoundOnPage, 1);
 }
 
 // TODO(crbug.com/413042395): This test is not testing overlay logic, but
diff --git a/chrome/browser/ui/lens/lens_overlay_side_panel_coordinator.cc b/chrome/browser/ui/lens/lens_overlay_side_panel_coordinator.cc
index 1e8fe69..4452104 100644
--- a/chrome/browser/ui/lens/lens_overlay_side_panel_coordinator.cc
+++ b/chrome/browser/ui/lens/lens_overlay_side_panel_coordinator.cc
@@ -226,6 +226,8 @@
       if (page_url.host() != nav_url.host() ||
           page_url.path() != nav_url.path() ||
           page_url_text_query != nav_url_text_query) {
+        lens::RecordHandleTextDirectiveResult(
+            lens::LensOverlayTextDirectiveResult::kOpenedInNewTab);
         lens_search_controller_->GetTabInterface()
             ->GetBrowserWindowInterface()
             ->OpenGURL(nav_url, WindowOpenDisposition::NEW_FOREGROUND_TAB);
@@ -883,11 +885,15 @@
                              ->GetLastCommittedURL();
   if (lookup_results.empty()) {
     if (URLsMatchWithoutTextFragment(page_url, nav_url)) {
+      lens::RecordHandleTextDirectiveResult(
+          lens::LensOverlayTextDirectiveResult::kNotFoundOnPage);
       ShowToast(l10n_util::GetStringUTF8(
           IDS_LENS_OVERLAY_TOAST_PAGE_CONTENT_NOT_FOUND_MESSAGE));
       return;
     }
 
+    lens::RecordHandleTextDirectiveResult(
+        lens::LensOverlayTextDirectiveResult::kOpenedInNewTab);
     lens_search_controller_->GetTabInterface()
         ->GetBrowserWindowInterface()
         ->OpenGURL(nav_url, WindowOpenDisposition::NEW_FOREGROUND_TAB);
@@ -899,11 +905,15 @@
     // If any of the text fragments are not found, then open in a new tab.
     if (!pair.second) {
       if (URLsMatchWithoutTextFragment(page_url, nav_url)) {
+        lens::RecordHandleTextDirectiveResult(
+            lens::LensOverlayTextDirectiveResult::kNotFoundOnPage);
         ShowToast(l10n_util::GetStringUTF8(
             IDS_LENS_OVERLAY_TOAST_PAGE_CONTENT_NOT_FOUND_MESSAGE));
         return;
       }
 
+      lens::RecordHandleTextDirectiveResult(
+          lens::LensOverlayTextDirectiveResult::kOpenedInNewTab);
       lens_search_controller_->GetTabInterface()
           ->GetBrowserWindowInterface()
           ->OpenGURL(nav_url, WindowOpenDisposition::NEW_FOREGROUND_TAB);
@@ -923,6 +933,8 @@
 
   // If every text fragment was found, then create a text highlighter manager to
   // render the text highlights. Focus the main tab first.
+  lens::RecordHandleTextDirectiveResult(
+      lens::LensOverlayTextDirectiveResult::kFoundOnPage);
   lens_search_controller_->GetTabInterface()->GetContents()->Focus();
   companion::TextHighlighterManager* text_highlighter_manager =
       companion::TextHighlighterManager::GetOrCreateForPage(page);
diff --git a/chrome/browser/ui/tabs/saved_tab_groups/tab_group_sync_service_proxy.cc b/chrome/browser/ui/tabs/saved_tab_groups/tab_group_sync_service_proxy.cc
index 21861d5..9f4ad6f 100644
--- a/chrome/browser/ui/tabs/saved_tab_groups/tab_group_sync_service_proxy.cc
+++ b/chrome/browser/ui/tabs/saved_tab_groups/tab_group_sync_service_proxy.cc
@@ -26,6 +26,7 @@
 #include "components/saved_tab_groups/public/saved_tab_group_tab.h"
 #include "components/saved_tab_groups/public/tab_group_sync_service.h"
 #include "components/saved_tab_groups/public/types.h"
+#include "components/saved_tab_groups/public/versioning_message_controller.h"
 #include "components/sync/base/user_selectable_type.h"
 #include "components/sync/service/sync_service.h"
 #include "components/sync/service/sync_user_settings.h"
@@ -382,6 +383,11 @@
   return false;
 }
 
+VersioningMessageController*
+TabGroupSyncServiceProxy::GetVersioningMessageController() {
+  return nullptr;
+}
+
 void TabGroupSyncServiceProxy::OnLastTabClosed(
     const SavedTabGroup& saved_tab_group) {}
 
diff --git a/chrome/browser/ui/tabs/saved_tab_groups/tab_group_sync_service_proxy.h b/chrome/browser/ui/tabs/saved_tab_groups/tab_group_sync_service_proxy.h
index ceb238a..096b77a 100644
--- a/chrome/browser/ui/tabs/saved_tab_groups/tab_group_sync_service_proxy.h
+++ b/chrome/browser/ui/tabs/saved_tab_groups/tab_group_sync_service_proxy.h
@@ -139,6 +139,7 @@
   std::unique_ptr<std::vector<SavedTabGroup>>
   TakeSharedTabGroupsAvailableAtStartupForMessaging() override;
   bool HadSharedTabGroupsLastSession(bool open_shared_tab_groups) override;
+  VersioningMessageController* GetVersioningMessageController() override;
   void OnLastTabClosed(const SavedTabGroup& saved_tab_group) override;
 
   void AddObserver(Observer* observer) override;
diff --git a/chrome/browser/ui/views/location_bar/lens_overlay_page_action_icon_view_interactive_uitest.cc b/chrome/browser/ui/views/location_bar/lens_overlay_page_action_icon_view_interactive_uitest.cc
index cd6ccfbe..8828de78 100644
--- a/chrome/browser/ui/views/location_bar/lens_overlay_page_action_icon_view_interactive_uitest.cc
+++ b/chrome/browser/ui/views/location_bar/lens_overlay_page_action_icon_view_interactive_uitest.cc
@@ -110,12 +110,17 @@
   LensOverlayPageActionIconViewTest() {
     if (IsMigrationEnabled()) {
       scoped_feature_list_.InitWithFeaturesAndParameters(
-          {{lens::features::kLensOverlay, {}},
-           {lens::features::kLensOverlayOmniboxEntryPoint, {}},
-           {::features::kPageActionsMigration,
-            {
-                {::features::kPageActionsMigrationLensOverlay.name, "true"},
-            }}},
+          {
+              {lens::features::kLensOverlay, {}},
+              {lens::features::kLensOverlayOmniboxEntryPoint, {}},
+              {
+                  ::features::kPageActionsMigration,
+                  {
+                      {::features::kPageActionsMigrationLensOverlay.name,
+                       "true"},
+                  },
+              },
+          },
           {});
     } else {
       scoped_feature_list_.InitWithFeatures(
@@ -169,8 +174,9 @@
       scoped_feature_list_.InitWithFeaturesAndParameters(
           {base::test::FeatureRefAndParams(lens::features::kLensOverlay,
                                            {{"omnibox-entry-point", "false"}}),
-           base::test::FeatureRefAndParams(::features::kPageActionsMigration,
-                                           {})},
+           base::test::FeatureRefAndParams(
+               ::features::kPageActionsMigration,
+               {{::features::kPageActionsMigrationLensOverlay.name, "true"}})},
           {});
     } else {
       scoped_feature_list_.InitWithFeaturesAndParameters(
diff --git a/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc b/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc
index 88bc5779..592164a5 100644
--- a/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc
+++ b/chrome/browser/ui/views/location_bar/location_bar_view_browsertest.cc
@@ -502,8 +502,11 @@
     : public InProcessBrowserTest {
  public:
   LocationBarViewPageActionHideWhileEditingTests() {
-    scoped_feature_list_.InitWithFeaturesAndParameters(
-        {{::features::kPageActionsMigration, {}}}, {});
+    scoped_feature_list_.InitAndEnableFeatureWithParameters(
+        ::features::kPageActionsMigration,
+        {
+            {features::kPageActionsMigrationZoom.name, "true"},
+        });
   }
 
   void SetUpOnMainThread() override {
diff --git a/chrome/browser/ui/views/page_action/BUILD.gn b/chrome/browser/ui/views/page_action/BUILD.gn
index dfd648d..d463474 100644
--- a/chrome/browser/ui/views/page_action/BUILD.gn
+++ b/chrome/browser/ui/views/page_action/BUILD.gn
@@ -97,6 +97,7 @@
   deps = [
     ":page_action",
     ":test_support",
+    "//chrome/browser/ui:ui_features",
     "//chrome/test:test_support",
     "//components/tabs:test_support",
     "//ui/events:test_support",
diff --git a/chrome/browser/ui/views/page_action/page_action_container_view.cc b/chrome/browser/ui/views/page_action/page_action_container_view.cc
index 87510e0e..925597d 100644
--- a/chrome/browser/ui/views/page_action/page_action_container_view.cc
+++ b/chrome/browser/ui/views/page_action/page_action_container_view.cc
@@ -7,6 +7,7 @@
 #include <memory>
 
 #include "base/functional/bind.h"
+#include "chrome/browser/ui/page_action/page_action_icon_type.h"
 #include "chrome/browser/ui/views/page_action/page_action_controller.h"
 #include "chrome/browser/ui/views/page_action/page_action_properties_provider.h"
 #include "chrome/browser/ui/views/page_action/page_action_view.h"
@@ -42,9 +43,16 @@
   int initial_index = 0;
   for (actions::ActionItem* action_item : action_items) {
     const auto action_item_id = action_item->GetActionId().value();
+    const auto& properties = properties_provider.GetProperties(action_item_id);
+
+    // When the page action migration is not enabled, the view should not be
+    // created to avoid conflicting with the old framework version identifier.
+    if (!IsPageActionMigrated(properties.type)) {
+      continue;
+    }
+
     PageActionView* view = AddChildView(std::make_unique<PageActionView>(
-        action_item, params,
-        properties_provider.GetProperties(action_item_id).element_identifier));
+        action_item, params, properties.element_identifier));
 
     page_action_views_[action_item_id] = view;
     chip_state_changed_callbacks_.push_back(
diff --git a/chrome/browser/ui/views/page_action/page_action_container_view_unittest.cc b/chrome/browser/ui/views/page_action/page_action_container_view_unittest.cc
index b1324ee..ed8af13 100644
--- a/chrome/browser/ui/views/page_action/page_action_container_view_unittest.cc
+++ b/chrome/browser/ui/views/page_action/page_action_container_view_unittest.cc
@@ -4,6 +4,9 @@
 
 #include "chrome/browser/ui/views/page_action/page_action_container_view.h"
 
+#include "base/test/scoped_feature_list.h"
+#include "chrome/browser/ui/actions/chrome_action_id.h"
+#include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/views/page_action/page_action_view.h"
 #include "chrome/browser/ui/views/page_action/page_action_view_params.h"
 #include "chrome/browser/ui/views/page_action/test_support/test_page_action_properties_provider.h"
@@ -17,12 +20,13 @@
 constexpr int kDefaultBetweenIconSpacing = 8;
 constexpr int kDefaultIconSize = 16;
 
-static constexpr actions::ActionId kTestPageActionId = 0;
+static constexpr actions::ActionId kTestPageActionId = kActionZoomNormal;
 static const PageActionPropertiesMap kTestProperties = PageActionPropertiesMap{
     {
         kTestPageActionId,
         PageActionProperties{
-            .histogram_name = "Test",
+            .histogram_name = "TestZoom",
+            .type = PageActionIconType::kZoom,
         },
     },
 };
@@ -41,7 +45,12 @@
 
 class PageActionContainerViewTest : public views::ViewsTestBase {
  public:
-  PageActionContainerViewTest() = default;
+  PageActionContainerViewTest() {
+    scoped_feature_list_.InitAndEnableFeatureWithParameters(
+        features::kPageActionsMigration,
+        {{features::kPageActionsMigrationZoom.name, "true"}});
+  }
+
   ~PageActionContainerViewTest() override = default;
 
   void TearDown() override {
@@ -57,6 +66,7 @@
   }
 
  private:
+  base::test::ScopedFeatureList scoped_feature_list_;
   MockIconLabelViewDelegate icon_label_view_delegate_;
 };
 
@@ -74,12 +84,14 @@
       TestPageActionPropertiesProvider(kTestProperties), DefaultViewParams());
 
   PageActionView* page_action_view =
-      page_action_container->GetPageActionView(0);
+      page_action_container->GetPageActionView(kTestPageActionId);
   ASSERT_TRUE(!!page_action_view);
-  EXPECT_EQ(0, page_action_view->GetActionId());
+  EXPECT_EQ(kTestPageActionId, page_action_view->GetActionId());
 
   // Returns null if the action ID is not found.
-  EXPECT_EQ(nullptr, page_action_container->GetPageActionView(1));
+  static constexpr actions::ActionId kNonExistantPageActionId = 1;
+  EXPECT_EQ(nullptr,
+            page_action_container->GetPageActionView(kNonExistantPageActionId));
 }
 
 }  // namespace
diff --git a/chrome/browser/ui/views/page_action/page_action_interactive_uitest.cc b/chrome/browser/ui/views/page_action/page_action_interactive_uitest.cc
index 0d4fca1..c7406a6 100644
--- a/chrome/browser/ui/views/page_action/page_action_interactive_uitest.cc
+++ b/chrome/browser/ui/views/page_action/page_action_interactive_uitest.cc
@@ -118,10 +118,22 @@
     // TODO(crbug.com/424806660): These tests should not be reliant on
     // kLensOverlayOmniboxEntryPoint being enabled, but disabling it causes them
     // to fail.
-    feature_list_.InitWithFeatures(
-        {features::kPageActionsMigration,
-         lens::features::kLensOverlayOmniboxEntryPoint},
-        {lens::features::kLensOverlay});
+    feature_list_.InitWithFeaturesAndParameters(
+        /*enabled_features=*/
+        {
+            {
+                features::kPageActionsMigration,
+                {
+                    {features::kPageActionsMigrationZoom.name, "true"},
+                    {features::kPageActionsMigrationTranslate.name, "true"},
+                    {features::kPageActionsMigrationMemorySaver.name, "true"},
+                },
+            },
+            {lens::features::kLensOverlayOmniboxEntryPoint, {}},
+        },
+        /*disabled_features=*/{
+            lens::features::kLensOverlay,
+        });
   }
 
   virtual ~PageActionUiTestBase() = default;
@@ -613,13 +625,7 @@
 class PageActionMetricsInteractiveUiTest : public InteractiveBrowserTest,
                                            public PageActionUiTestBase {
  public:
-  PageActionMetricsInteractiveUiTest() {
-    scoped_feature_list_.InitAndEnableFeatureWithParameters(
-        features::kPageActionsMigration,
-        {
-            {features::kPageActionsMigrationZoom.name, "true"},
-        });
-  }
+  PageActionMetricsInteractiveUiTest() = default;
 
   PageActionMetricsInteractiveUiTest(
       const PageActionMetricsInteractiveUiTest&) = delete;
@@ -642,9 +648,6 @@
   auto DoZoomOut() {
     return Do([&]() { SetZoomLevel(content::PAGE_ZOOM_OUT); });
   }
-
- private:
-  base::test::ScopedFeatureList scoped_feature_list_;
 };
 
 IN_PROC_BROWSER_TEST_F(PageActionMetricsInteractiveUiTest, ClickHistogramLogs) {
diff --git a/chrome/browser/ui/views/toolbar/BUILD.gn b/chrome/browser/ui/views/toolbar/BUILD.gn
index fcd0bf4..dc2d80a 100644
--- a/chrome/browser/ui/views/toolbar/BUILD.gn
+++ b/chrome/browser/ui/views/toolbar/BUILD.gn
@@ -132,6 +132,7 @@
     "//chrome/browser/ui/views/page_action:page_action",
     "//chrome/browser/ui/views/side_panel:side_panel_enums",
     "//chrome/browser/ui/web_applications",
+    "//chrome/browser/upgrade_detector",
     "//chrome/browser/web_applications:features",
     "//chrome/common:buildflags",
     "//chrome/common:chrome_features",
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 3e7eab1..f71f2a4d 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
@@ -20,6 +20,7 @@
 #include "base/test/gmock_expected_support.h"
 #include "base/test/icu_test_util.h"
 #include "base/test/run_until.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/test_future.h"
 #include "base/test/test_timeouts.h"
 #include "build/build_config.h"
@@ -34,6 +35,7 @@
 #include "chrome/browser/ui/browser_commands.h"
 #include "chrome/browser/ui/browser_finder.h"
 #include "chrome/browser/ui/download/download_display.h"
+#include "chrome/browser/ui/page_action/page_action_icon_type.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/view_ids.h"
@@ -49,6 +51,7 @@
 #include "chrome/browser/ui/views/infobars/infobar_container_view.h"
 #include "chrome/browser/ui/views/infobars/infobar_view.h"
 #include "chrome/browser/ui/views/page_action/page_action_icon_controller.h"
+#include "chrome/browser/ui/views/page_action/page_action_properties_provider.h"
 #include "chrome/browser/ui/views/page_action/page_action_view.h"
 #include "chrome/browser/ui/views/toolbar/pinned_toolbar_actions_container.h"
 #include "chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_test_helper.h"
@@ -239,12 +242,23 @@
             helper()->web_app_frame_toolbar());
 
   std::vector<const views::View*> page_action_views = {};
+  const auto& properties_provider =
+      page_actions::PageActionPropertiesProvider();
   for (auto action_id : helper()
                             ->app_browser()
                             ->GetAppBrowserController()
                             ->GetTitleBarPageActions()) {
     auto* page_action_view =
         helper()->web_app_frame_toolbar()->GetPageActionView(action_id);
+
+    const auto& properties = properties_provider.GetProperties(action_id);
+
+    // When the page action migration is not enabled, the view should not be
+    // created to avoid conflicting with the old framework version identifier.
+    if (!IsPageActionMigrated(properties.type)) {
+      continue;
+    }
+
     ASSERT_NE(nullptr, page_action_view);
     EXPECT_EQ(page_action_view->parent(),
               toolbar_right_container->page_action_container());
diff --git a/chrome/browser/ui/webui/new_tab_page/composebox/BUILD.gn b/chrome/browser/ui/webui/new_tab_page/composebox/BUILD.gn
index dd3efeab..82099a9 100644
--- a/chrome/browser/ui/webui/new_tab_page/composebox/BUILD.gn
+++ b/chrome/browser/ui/webui/new_tab_page/composebox/BUILD.gn
@@ -22,3 +22,13 @@
   sources = [ "composebox.mojom" ]
   webui_module_path = "/"
 }
+
+source_set("unit_tests") {
+  testonly = true
+  sources = [ "composebox_handler_unittest.cc" ]
+  deps = [
+    ":composebox",
+    "//base/test:test_support",
+    "//components/omnibox/composebox",
+  ]
+}
diff --git a/chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler.cc b/chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler.cc
index fc29e75..179ec68 100644
--- a/chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler.cc
+++ b/chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler.cc
@@ -4,11 +4,11 @@
 
 #include "chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler.h"
 
-// TODO(420700441) Add unittest coverage.
 ComposeboxHandler::ComposeboxHandler(
-    mojo::PendingReceiver<composebox::mojom::ComposeboxPageHandler> handler)
+    mojo::PendingReceiver<composebox::mojom::ComposeboxPageHandler> handler,
+    std::unique_ptr<ComposeboxQueryController> query_controller)
     : handler_(this, std::move(handler)),
-      query_controller_{std::make_unique<ComposeboxQueryController>()} {}
+      query_controller_(std::move(query_controller)) {}
 
 ComposeboxHandler::~ComposeboxHandler() = default;
 
diff --git a/chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler.h b/chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler.h
index 396b236..7d0bbe2 100644
--- a/chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler.h
+++ b/chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler.h
@@ -13,7 +13,8 @@
 class ComposeboxHandler : public composebox::mojom::ComposeboxPageHandler {
  public:
   explicit ComposeboxHandler(
-      mojo::PendingReceiver<composebox::mojom::ComposeboxPageHandler> handler);
+      mojo::PendingReceiver<composebox::mojom::ComposeboxPageHandler> handler,
+      std::unique_ptr<ComposeboxQueryController> query_controller);
   ~ComposeboxHandler() override;
 
   // composebox::mojom::ComposeboxPageHandler:
diff --git a/chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler_unittest.cc b/chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler_unittest.cc
new file mode 100644
index 0000000..f79567dd
--- /dev/null
+++ b/chrome/browser/ui/webui/new_tab_page/composebox/composebox_handler_unittest.cc
@@ -0,0 +1,45 @@
+// Copyright 2025 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/ui/webui/new_tab_page/composebox/composebox_handler.h"
+
+#include "chrome/browser/ui/webui/new_tab_page/composebox/composebox.mojom.h"
+#include "components/omnibox/composebox/composebox_query_controller.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+class MockQueryController : public ComposeboxQueryController {
+ public:
+  MockQueryController() = default;
+  ~MockQueryController() override = default;
+
+  MOCK_METHOD(void, NotifySessionStarted, ());
+  MOCK_METHOD(void, NotifySessionAbandoned, ());
+};
+
+class ComposeboxHandlerTest : public testing::Test {
+ public:
+  ComposeboxHandlerTest() = default;
+  ~ComposeboxHandlerTest() override = default;
+
+  void SetUp() override {
+    auto query_controller_ptr = std::make_unique<MockQueryController>();
+    query_controller_ = query_controller_ptr.get();
+    handler_ = std::make_unique<ComposeboxHandler>(
+        mojo::PendingReceiver<composebox::mojom::ComposeboxPageHandler>(),
+        std::move(query_controller_ptr));
+  }
+
+  ComposeboxHandler& handler() { return *handler_; }
+  MockQueryController& query_controller() { return *query_controller_; }
+
+ private:
+  std::unique_ptr<ComposeboxHandler> handler_;
+  raw_ptr<MockQueryController> query_controller_;
+};
+
+TEST_F(ComposeboxHandlerTest, NotifySessionStarted) {
+  EXPECT_CALL(query_controller(), NotifySessionStarted).Times(1);
+  handler().NotifySessionStarted();
+}
diff --git a/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.cc b/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.cc
index 466a778..10b4dbfc 100644
--- a/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.cc
+++ b/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.cc
@@ -75,6 +75,7 @@
 #include "components/grit/components_scaled_resources.h"
 #include "components/history_clusters/core/features.h"
 #include "components/omnibox/browser/omnibox_prefs.h"
+#include "components/omnibox/composebox/composebox_query_controller.h"
 #include "components/page_image_service/image_service.h"
 #include "components/page_image_service/image_service_handler.h"
 #include "components/prefs/pref_registry_simple.h"
@@ -735,8 +736,9 @@
 void NewTabPageUI::BindInterface(
     mojo::PendingReceiver<composebox::mojom::ComposeboxPageHandler>
         pending_receiver) {
-  composebox_handler_ =
-      std::make_unique<ComposeboxHandler>(std::move(pending_receiver));
+  composebox_handler_ = std::make_unique<ComposeboxHandler>(
+      std::move(pending_receiver),
+      std::make_unique<ComposeboxQueryController>());
 }
 
 void NewTabPageUI::BindInterface(
diff --git a/chrome/browser/upgrade_detector/BUILD.gn b/chrome/browser/upgrade_detector/BUILD.gn
index f2fa29b..0fe53010 100644
--- a/chrome/browser/upgrade_detector/BUILD.gn
+++ b/chrome/browser/upgrade_detector/BUILD.gn
@@ -7,3 +7,16 @@
 
   public_deps = [ "//base" ]
 }
+
+source_set("upgrade_detector") {
+  sources = [ "upgrade_observer.h" ]
+  public_deps = []
+
+  if (!is_android) {
+    sources += [ "upgrade_detector.h" ]
+    public_deps = [
+      "//base",
+      "//components/prefs",
+    ]
+  }
+}
diff --git a/chrome/build/android-arm32.pgo.txt b/chrome/build/android-arm32.pgo.txt
index d8af2af..8ba3835a 100644
--- a/chrome/build/android-arm32.pgo.txt
+++ b/chrome/build/android-arm32.pgo.txt
@@ -1 +1 @@
-chrome-android32-main-1749837587-dc74434184fd38f82c739e0bce155bc22605b7a3-a841b09d34eaaae88f2d5d1c3133bc31f6644401.profdata
+chrome-android32-main-1749859065-3a39a0a8c5909a0ecd3659f2b686d601766e8944-2547103072959024f14c03c5e63b282d70732547.profdata
diff --git a/chrome/build/android-arm64.pgo.txt b/chrome/build/android-arm64.pgo.txt
index 782e772..c44cb035 100644
--- a/chrome/build/android-arm64.pgo.txt
+++ b/chrome/build/android-arm64.pgo.txt
@@ -1 +1 @@
-chrome-android64-main-1749838109-993cb23ef058f9c53d3f078c25ca1bd80885b51f-b4b2f68779c4f8ced8b70999f6f308a50c00aa50.profdata
+chrome-android64-main-1749873206-3ba42b7b98b1e49b18ba3b93006b81affc74d4c0-ca82c71509e66359013536d1f746da84e624584c.profdata
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index 96271e0..bc6ff56 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-main-1749837587-dfd4560370021fdc9be0d898832572f40a4abef5-a841b09d34eaaae88f2d5d1c3133bc31f6644401.profdata
+chrome-linux-main-1749859065-6b7219937452527d2ea4d10e8890d3a841d6b1c8-2547103072959024f14c03c5e63b282d70732547.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 49465378..57ea0094 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1749844660-a31350277cc3f3bb603b7603310cfa6e7abecbbb-b0219b4ce2b576512bd7698922697c75e7ddb569.profdata
+chrome-mac-arm-main-1749873206-588f4cac38cdfc0135540e0f967469f86b7e68d6-ca82c71509e66359013536d1f746da84e624584c.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index 5ed2165b..9f16c94 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-main-1749837587-682f385701cb67c2306078d62b477beea6ae34bd-a841b09d34eaaae88f2d5d1c3133bc31f6644401.profdata
+chrome-mac-main-1749859065-387941b4719e71ad8482b82d23f1e1d6b0c4036e-2547103072959024f14c03c5e63b282d70732547.profdata
diff --git a/chrome/build/win-arm64.pgo.txt b/chrome/build/win-arm64.pgo.txt
index e0c9eaf..492e9d9 100644
--- a/chrome/build/win-arm64.pgo.txt
+++ b/chrome/build/win-arm64.pgo.txt
@@ -1 +1 @@
-chrome-win-arm64-main-1749837587-0d75c735f64ac784762067f38317d80af0897482-a841b09d34eaaae88f2d5d1c3133bc31f6644401.profdata
+chrome-win-arm64-main-1749859065-221bd629013277b9011d1dbcf131371e9e815223-2547103072959024f14c03c5e63b282d70732547.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 58c34c7..34a4985 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1749826705-78c2b5ce6ec32fc1cbc26855aaa8a025beeb4583-ec1b13fe44cea60c605fa6193cedd87f2e43821b.profdata
+chrome-win32-main-1749847676-4d20a04a5bd7f83ecf3bd29cbfa216b2dd8d81f8-0393af2ee7d050c595324df6afb1fc93a27242c2.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index b0824eb..07f83b34 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1749815741-51068494ebe1454ff4f94d6ce9f526c03829c3d3-41d4f9a106e824de554d27e7ce69ba8e9af88d7d.profdata
+chrome-win64-main-1749826705-22d1a4c3e4b2d1b2d0566cf18d69e0cbd6aceecf-ec1b13fe44cea60c605fa6193cedd87f2e43821b.profdata
diff --git a/chrome/common/extensions/api/api_sources.gni b/chrome/common/extensions/api/api_sources.gni
index 48557a0..1baac29d 100644
--- a/chrome/common/extensions/api/api_sources.gni
+++ b/chrome/common/extensions/api/api_sources.gni
@@ -20,6 +20,7 @@
   "font_settings.json",
   "history.json",
   "identity.idl",
+  "instance_id.json",
   "notifications.idl",
   "permissions.json",
   "processes.idl",
@@ -63,7 +64,6 @@
     "enterprise_reporting_private.idl",
     "gcm.json",
     "image_writer_private.idl",
-    "instance_id.json",
     "language_settings_private.idl",
     "omnibox.json",
     "page_capture.json",
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 8fca949..da901b2 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -1420,7 +1420,7 @@
     "//components/metrics:test_support",
     "//components/metrics_services_manager",
     "//components/network_session_configurator/common",
-    "//components/optimization_guide/core:model_executor",
+    "//components/optimization_guide/core",
     "//components/optimization_guide/core:test_support",
     "//components/policy:generated",
     "//components/policy:policy_code_generate",
@@ -1535,6 +1535,7 @@
       "../browser/extensions/api/history/history_apitest.cc",
       "../browser/extensions/api/i18n/i18n_apitest.cc",
       "../browser/extensions/api/idle/idle_apitest.cc",
+      "../browser/extensions/api/instance_id/instance_id_apitest.cc",
       "../browser/extensions/api/messaging/messaging_apitest.cc",
       "../browser/extensions/api/messaging/service_worker_messaging_apitest.cc",
       "../browser/extensions/api/module/module_apitest.cc",
@@ -4453,7 +4454,6 @@
         "../browser/extensions/api/image_writer_private/extractor_browsertest.cc",
         "../browser/extensions/api/image_writer_private/test_utils.cc",
         "../browser/extensions/api/image_writer_private/test_utils.h",
-        "../browser/extensions/api/instance_id/instance_id_apitest.cc",
         "../browser/extensions/api/management/management_api_browsertest.cc",
         "../browser/extensions/api/management/management_api_non_persistent_apitest.cc",
         "../browser/extensions/api/management/management_apitest.cc",
@@ -8222,6 +8222,7 @@
       "//chrome/browser/ui/webui/access_code_cast:unit_tests",
       "//chrome/browser/ui/webui/app_management:unit_tests",
       "//chrome/browser/ui/webui/new_tab_footer:unit_tests",
+      "//chrome/browser/ui/webui/new_tab_page/composebox:unit_tests",
       "//chrome/browser/ui/webui/privacy_sandbox:unit_tests",
       "//chrome/browser/ui/webui/settings",
       "//chrome/browser/ui/webui/signin:unit_tests",
@@ -8681,6 +8682,7 @@
       "../browser/ui/ash/quick_answers/quick_answers_controller_unittest.cc",
       "../browser/ui/ash/quick_answers/quick_answers_state_ash_unittest.cc",
       "../browser/ui/ash/quick_answers/quick_answers_ui_controller_unittest.cc",
+      "../browser/ui/ash/quick_answers/ui/magic_boost_user_consent_view_unittest.cc",
       "../browser/ui/ash/quick_answers/ui/quick_answers_view_unittest.cc",
       "../browser/ui/ash/quick_answers/ui/user_consent_view_unittest.cc",
       "../browser/ui/views/frame/immersive_mode_controller_chromeos_unittest.cc",
diff --git a/chrome/test/base/ash/extension_js_browser_test.cc b/chrome/test/base/ash/extension_js_browser_test.cc
index a2cea46..1d32863 100644
--- a/chrome/test/base/ash/extension_js_browser_test.cc
+++ b/chrome/test/base/ash/extension_js_browser_test.cc
@@ -5,8 +5,14 @@
 #include "chrome/test/base/ash/extension_js_browser_test.h"
 
 #include <memory>
+#include <optional>
+#include <string>
+#include <utility>
 #include <vector>
 
+#include "base/command_line.h"
+#include "base/containers/contains.h"
+#include "base/files/file_path.h"
 #include "base/functional/callback.h"
 #include "base/json/json_reader.h"
 #include "base/strings/strcat.h"
@@ -117,7 +123,10 @@
   }
 
   if (!libs_loaded_) {
-    BuildJavascriptLibraries(&scripts);
+    if (!BuildJavascriptLibraries(&scripts)) {
+      ADD_FAILURE() << "Failed to build JavaScript libraries";
+      return false;
+    }
     libs_loaded_ = true;
   }
 
diff --git a/chrome/test/base/ash/javascript_browser_test.cc b/chrome/test/base/ash/javascript_browser_test.cc
index 949f4ad..7cadc93 100644
--- a/chrome/test/base/ash/javascript_browser_test.cc
+++ b/chrome/test/base/ash/javascript_browser_test.cc
@@ -4,7 +4,11 @@
 
 #include "chrome/test/base/ash/javascript_browser_test.h"
 
+#include <optional>
+
+#include "base/files/file_path.h"
 #include "base/files/file_util.h"
+#include "base/logging.h"
 #include "base/path_service.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/threading/thread_restrictions.h"
@@ -44,59 +48,78 @@
   library_search_paths_.push_back(source_root_directory);
 }
 
-// TODO(dtseng): Make this return bool (success/failure) and remove ASSERt_TRUE
-// calls.
-void JavaScriptBrowserTest::BuildJavascriptLibraries(
+bool JavaScriptBrowserTest::BuildJavascriptLibraries(
     std::vector<std::u16string>* libraries) {
   base::ScopedAllowBlockingForTesting allow_blocking;
-  ASSERT_TRUE(libraries != nullptr);
-  std::vector<base::FilePath>::iterator user_libraries_iterator;
-  for (user_libraries_iterator = user_libraries_.begin();
-       user_libraries_iterator != user_libraries_.end();
-       ++user_libraries_iterator) {
+  if (!libraries) {
+    LOG(ERROR) << "BuildJavascriptLibraries called with null libraries pointer";
+    return false;
+  }
+
+  // Path processing logic.
+  auto resolve_mapped_or_absolute_path =
+      [&](const base::FilePath& path) -> std::optional<base::FilePath> {
+    const auto components = path.GetComponents();
+    if (components.front() == FILE_PATH_LITERAL("ROOT_GEN_DIR")) {
+      base::FilePath exe_dir;
+      if (!base::PathService::Get(base::DIR_EXE, &exe_dir)) {
+        LOG(ERROR) << "Failed to get base::DIR_EXE for ROOT_GEN_DIR: "
+                   << path.value();
+        return std::nullopt;
+      }
+      base::FilePath abs_path = exe_dir.AppendASCII("gen");
+      for (size_t i = 1; i < components.size(); ++i) {
+        abs_path = abs_path.Append(components[i]);
+      }
+      return abs_path.NormalizePathSeparators();
+    }
+    return path.IsAbsolute() ? std::optional<base::FilePath>(path)
+                             : std::nullopt;
+  };
+
+  for (const auto& library_path : user_libraries_) {
     std::string library_content;
     base::FilePath library_absolute_path;
-    std::vector<base::FilePath::StringType> components =
-        user_libraries_iterator->GetComponents();
-    if (components[0] == FILE_PATH_LITERAL("ROOT_GEN_DIR")) {
-      base::FilePath exe_dir;
-      base::PathService::Get(base::DIR_EXE, &exe_dir);
-      library_absolute_path = exe_dir.AppendASCII("gen");
-      for (size_t i = 1; i < components.size(); i++)
-        library_absolute_path = library_absolute_path.Append(components[i]);
-      library_absolute_path = library_absolute_path.NormalizePathSeparators();
-      ASSERT_TRUE(
-          base::ReadFileToString(library_absolute_path, &library_content))
-          << user_libraries_iterator->value();
-    } else if (user_libraries_iterator->IsAbsolute()) {
-      library_absolute_path = *user_libraries_iterator;
-      ASSERT_TRUE(
-          base::ReadFileToString(library_absolute_path, &library_content))
-          << user_libraries_iterator->value();
-    } else {
-      bool ok = false;
-      std::vector<base::FilePath>::iterator library_search_path_iterator;
-      for (library_search_path_iterator = library_search_paths_.begin();
-           library_search_path_iterator != library_search_paths_.end();
-           ++library_search_path_iterator) {
-        library_absolute_path = base::MakeAbsoluteFilePath(
-            library_search_path_iterator->Append(*user_libraries_iterator));
-        ok = base::ReadFileToString(library_absolute_path, &library_content);
-        if (ok)
-          break;
+    bool found = false;
+    // Resolve mapped paths or absolute paths.
+    if (auto resolved_path = resolve_mapped_or_absolute_path(library_path)) {
+      if (base::ReadFileToString(*resolved_path, &library_content)) {
+        library_absolute_path = *resolved_path;
+        found = true;
+      } else {
+        LOG(ERROR) << "Failed to read resolved library file: "
+                   << resolved_path->value();
       }
-      ASSERT_TRUE(ok) << "User library not found: "
-                      << user_libraries_iterator->value();
+    } else {
+      // Handling relative paths.
+      for (const auto& search_path : library_search_paths_) {
+        base::FilePath candidate =
+            base::MakeAbsoluteFilePath(search_path.Append(library_path));
+        if (base::ReadFileToString(candidate, &library_content)) {
+          library_absolute_path = candidate;
+          found = true;
+          break;
+        }
+      }
     }
-    library_content.append(";\n");
+
+    if (!found) {
+      LOG(ERROR) << "Failed to load JS library: " << library_path.value();
+      return false;
+    }
 
     // This magic code puts filenames in stack traces.
-    library_content.append("//# sourceURL=");
-    library_content.append(
-        net::FilePathToFileURL(library_absolute_path).spec());
-    library_content.append("\n");
+    std::string source_url =
+        "//# sourceURL=" +
+        net::FilePathToFileURL(library_absolute_path).spec() + "\n";
+
+    library_content.reserve(library_content.size() + source_url.size() + 3);
+    library_content += ";\n";
+    library_content += source_url;
+
     libraries->push_back(base::UTF8ToUTF16(library_content));
   }
+  return true;
 }
 
 std::u16string JavaScriptBrowserTest::BuildRunTestJSCall(
diff --git a/chrome/test/base/ash/javascript_browser_test.h b/chrome/test/base/ash/javascript_browser_test.h
index e7abe59..42ee60f3 100644
--- a/chrome/test/base/ash/javascript_browser_test.h
+++ b/chrome/test/base/ash/javascript_browser_test.h
@@ -8,6 +8,7 @@
 #include <string>
 #include <vector>
 
+#include "base/files/file_path.h"
 #include "base/values.h"
 #include "chrome/test/base/in_process_browser_test.h"
 
@@ -29,9 +30,9 @@
   // InProcessBrowserTest overrides.
   void SetUpOnMainThread() override;
 
-  // Builds a vector of strings of all added javascript libraries suitable for
-  // execution by subclasses.
-  void BuildJavascriptLibraries(std::vector<std::u16string>* libraries);
+  // Builds a vector of javascript library contents for execution. Returns true
+  // on success, false if any library fails to load or if `libraries` is null.
+  bool BuildJavascriptLibraries(std::vector<std::u16string>* libraries);
 
   // Builds a string with a call to the runTest JS function, passing the
   // given |is_async|, |test_name| and its |args|.
diff --git a/chrome/test/base/ash/web_ui_browser_test.cc b/chrome/test/base/ash/web_ui_browser_test.cc
index 71cb380f..ff565509 100644
--- a/chrome/test/base/ash/web_ui_browser_test.cc
+++ b/chrome/test/base/ash/web_ui_browser_test.cc
@@ -7,10 +7,12 @@
 #include <stddef.h>
 
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "base/command_line.h"
 #include "base/containers/contains.h"
+#include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/functional/bind.h"
 #include "base/lazy_instance.h"
@@ -451,7 +453,10 @@
                                   : test_handler_->GetRenderFrameHostForTest();
   if (!base::Contains(libraries_preloaded_for_frames_,
                       frame_for_libraries->GetGlobalId())) {
-    BuildJavascriptLibraries(&libraries);
+    if (!BuildJavascriptLibraries(&libraries)) {
+      ADD_FAILURE() << "Failed to build JavaScript libraries";
+      return false;
+    }
     if (!preload_frame) {
       content = base::JoinString(libraries, u"\n");
       libraries.clear();
diff --git a/chrome/test/data/extensions/api_test/instance_id/delete_token/delete_token.js b/chrome/test/data/extensions/api_test/instance_id/delete_token/delete_token.js
index 51954b7..2c5fd85 100644
--- a/chrome/test/data/extensions/api_test/instance_id/delete_token/delete_token.js
+++ b/chrome/test/data/extensions/api_test/instance_id/delete_token/delete_token.js
@@ -86,6 +86,13 @@
 }
 
 function deleteTokenBeforeGetToken() {
+  if (/Android/.test(navigator.userAgent)) {
+    // Skip this test on Android because the underlying call to
+    // com.google.android.gms.iid.InstanceID.deleteToken() always succeeds,
+    // even for non-existent tokens.
+    chrome.test.succeed("skipped");
+    return;
+  }
   chrome.instanceID.deleteToken(
     {"authorizedEntity": "1", "scope": "GCM"},
     function() {
diff --git a/chrome/test/data/webui/settings/settings_focus_test.cc b/chrome/test/data/webui/settings/settings_focus_test.cc
index 90cbc09..9197b1ae 100644
--- a/chrome/test/data/webui/settings/settings_focus_test.cc
+++ b/chrome/test/data/webui/settings/settings_focus_test.cc
@@ -75,7 +75,13 @@
   base::test::ScopedFeatureList scoped_feature_list_;
 };
 
-IN_PROC_BROWSER_TEST_F(SettingsGlicPageFocusTest, GlicPageFocus) {
+// TODO(crbug.com/424864547): Investigate flakiness and enable on Mac64.
+#if BUILDFLAG(IS_MAC)
+#define MAYBE_GlicPageFocus DISABLED_GlicPageFocus
+#else
+#define MAYBE_GlicPageFocus GlicPageFocus
+#endif  // BUILDFLAG(IS_MAC)
+IN_PROC_BROWSER_TEST_F(SettingsGlicPageFocusTest, MAYBE_GlicPageFocus) {
   RunTest("settings/glic_page_focus_test.js", "mocha.run()");
 }
 #endif
diff --git a/chromeos/CHROMEOS_LKGM b/chromeos/CHROMEOS_LKGM
index 20e8e106..1577052 100644
--- a/chromeos/CHROMEOS_LKGM
+++ b/chromeos/CHROMEOS_LKGM
@@ -1 +1 @@
-16318.0.0-1069562
\ No newline at end of file
+16318.0.0-1069568
\ No newline at end of file
diff --git a/clank b/clank
index b62d38f..4e7cdb1 160000
--- a/clank
+++ b/clank
@@ -1 +1 @@
-Subproject commit b62d38ffc9bd9bfc6011eabe6143a4a40e45bd9a
+Subproject commit 4e7cdb1917bb21338750038cdc8389b643f96cd5
diff --git a/components/autofill/ios/common/features.h b/components/autofill/ios/common/features.h
index 589101cb..4f978c5 100644
--- a/components/autofill/ios/common/features.h
+++ b/components/autofill/ios/common/features.h
@@ -65,6 +65,9 @@
 // expanding forms.
 BASE_DECLARE_FEATURE(kAutofillRefillForFormsIos);
 
+// Reports JS errors that occur upon handling form submission in the renderer.
+BASE_DECLARE_FEATURE(kAutofillReportFormSubmissionErrors);
+
 // Makes the autofill and password infobars sticky on iOS. The sticky infobar
 // sticks there until navigating from an explicit user gesture (e.g. reload or
 // load a new page from the omnibox). This includes the infobar UI and the
diff --git a/components/autofill/ios/common/features.mm b/components/autofill/ios/common/features.mm
index 41c5adee..82003ee 100644
--- a/components/autofill/ios/common/features.mm
+++ b/components/autofill/ios/common/features.mm
@@ -64,6 +64,12 @@
              "AutofillRefillForFormsIos",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+// LINT.IfChange(autofill_report_form_submission_errors)
+BASE_FEATURE(kAutofillReportFormSubmissionErrors,
+             "AutofillReportFormSubmissionErrors",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+// LINT.ThenChange(/components/autofill/ios/form_util/resources/autofill_form_features.ts:autofill_report_form_submission_errors)
+
 BASE_FEATURE(kAutofillStickyInfobarIos,
              "AutofillStickyInfobarIos",
              base::FEATURE_ENABLED_BY_DEFAULT);
diff --git a/components/autofill/ios/form_util/autofill_form_features_injector.mm b/components/autofill/ios/form_util/autofill_form_features_injector.mm
index 92542b4..60724a7c 100644
--- a/components/autofill/ios/form_util/autofill_form_features_injector.mm
+++ b/components/autofill/ios/form_util/autofill_form_features_injector.mm
@@ -63,6 +63,11 @@
       ->SetAutofillDedupeFormSubmission(
           web_frame,
           base::FeatureList::IsEnabled(kAutofillDedupeFormSubmission));
+
+  AutofillFormFeaturesJavaScriptFeature::GetInstance()
+      ->SetAutofillReportFormSubmissionErrors(
+          web_frame,
+          base::FeatureList::IsEnabled(kAutofillReportFormSubmissionErrors));
 }
 
 AutofillFormFeaturesInjector::~AutofillFormFeaturesInjector() = default;
diff --git a/components/autofill/ios/form_util/autofill_form_features_injector_unittest.mm b/components/autofill/ios/form_util/autofill_form_features_injector_unittest.mm
index a20323a..df07fb8 100644
--- a/components/autofill/ios/form_util/autofill_form_features_injector_unittest.mm
+++ b/components/autofill/ios/form_util/autofill_form_features_injector_unittest.mm
@@ -63,7 +63,8 @@
        autofill::features::kAutofillAcrossIframesIos,
        autofill::features::kAutofillAcrossIframesIosThrottling,
        kAutofillCorrectUserEditedBitInParsedField,
-       kAutofillAllowDefaultPreventedSubmission, kAutofillDedupeFormSubmission},
+       kAutofillAllowDefaultPreventedSubmission, kAutofillDedupeFormSubmission,
+       kAutofillReportFormSubmissionErrors},
       /* disabled_features= */ {});
 
   AutofillFormFeaturesInjector injector(&fake_web_state_,
@@ -86,7 +87,9 @@
                     u"__gCrWeb.autofill_form_features."
                     u"setAutofillAllowDefaultPreventedSubmission(true);",
                     u"__gCrWeb.autofill_form_features."
-                    u"setAutofillDedupeFormSubmission(true);"));
+                    u"setAutofillDedupeFormSubmission(true);",
+                    u"__gCrWeb.autofill_form_features."
+                    u"setAutofillReportFormSubmissionErrors(true);"));
   }
 }
 
diff --git a/components/autofill/ios/form_util/autofill_form_features_java_script_feature.h b/components/autofill/ios/form_util/autofill_form_features_java_script_feature.h
index 8051bba..a05a8af 100644
--- a/components/autofill/ios/form_util/autofill_form_features_java_script_feature.h
+++ b/components/autofill/ios/form_util/autofill_form_features_java_script_feature.h
@@ -49,6 +49,10 @@
   // Enables/disables deduping form submission for Autofill.
   void SetAutofillDedupeFormSubmission(web::WebFrame* frame, bool enabled);
 
+  // Enables/disables reporting form submission errors.
+  void SetAutofillReportFormSubmissionErrors(web::WebFrame* frame,
+                                             bool enabled);
+
  private:
   friend class base::NoDestructor<AutofillFormFeaturesJavaScriptFeature>;
 
diff --git a/components/autofill/ios/form_util/autofill_form_features_java_script_feature.mm b/components/autofill/ios/form_util/autofill_form_features_java_script_feature.mm
index 41747cb..6c37edb 100644
--- a/components/autofill/ios/form_util/autofill_form_features_java_script_feature.mm
+++ b/components/autofill/ios/form_util/autofill_form_features_java_script_feature.mm
@@ -97,4 +97,12 @@
       base::Value::List().Append(enabled));
 }
 
+// Enables/disables reporting form submission errors.
+void AutofillFormFeaturesJavaScriptFeature::
+    SetAutofillReportFormSubmissionErrors(web::WebFrame* frame, bool enabled) {
+  frame->CallJavaScriptFunction(
+      "autofill_form_features.setAutofillReportFormSubmissionErrors",
+      base::Value::List().Append(enabled));
+}
+
 }  // namespace autofill
diff --git a/components/autofill/ios/form_util/form_activity_tab_helper.h b/components/autofill/ios/form_util/form_activity_tab_helper.h
index b33a4cab..909c4b47 100644
--- a/components/autofill/ios/form_util/form_activity_tab_helper.h
+++ b/components/autofill/ios/form_util/form_activity_tab_helper.h
@@ -24,7 +24,7 @@
     "Autofill.iOS.FormSubmission.IsProgrammatic";
 
 inline constexpr char kFormSubmissionOutcomeHistogram[] =
-    "Autofill.iOS.FormSubmission.Outcome";
+    "Autofill.iOS.FormSubmission.OutcomeV2";
 
 inline constexpr char kInvalidSubmittedFormReasonHistogram[] =
     "Autofill.iOS.FormSubmission.Outcome.InvalidFormReason";
diff --git a/components/autofill/ios/form_util/form_activity_tab_helper.mm b/components/autofill/ios/form_util/form_activity_tab_helper.mm
index bdd78504..89259de 100644
--- a/components/autofill/ios/form_util/form_activity_tab_helper.mm
+++ b/components/autofill/ios/form_util/form_activity_tab_helper.mm
@@ -72,7 +72,13 @@
   // Autofill.iOS.FormSubmission.Outcome.InvalidFormReason to investigate the
   // cause.
   kFormExtractionFailure = 7,
-  kMaxValue = kFormExtractionFailure,
+  // There was an error while handling the form submission event in the
+  // renderer.
+  kRendererError = 8,
+  // There was an error while handling the form submission event in the renderer
+  // but the error couldn't be parsed.
+  kUnparsedRendererError = 9,
+  kMaxValue = kUnparsedRendererError,
 };
 // LINT.ThenChange(//tools/metrics/histograms/metadata/autofill/enums.xml:FormSubmissionOutcomeIOS)
 
@@ -219,6 +225,25 @@
   observers_.RemoveObserver(observer);
 }
 
+void HandleSubmissionError(const base::Value::Dict& message) {
+  const std::string* error_stack = message.FindString("errorStack");
+  const std::string* error_message = message.FindString("errorMessage");
+  std::optional<bool> is_programmatic =
+      message.FindBool("programmaticSubmission");
+
+  if (!error_stack || !error_message || !is_programmatic) {
+    RecordFormSubmissionOutcome(FormSubmissionOutcome::kUnparsedRendererError);
+    return;
+  }
+
+  SCOPED_CRASH_KEY_STRING256("FormSubmissionError", "msg", *error_message);
+  SCOPED_CRASH_KEY_STRING1024("FormSubmissionError", "stack", *error_stack);
+
+  base::debug::DumpWithoutCrashing();
+
+  RecordFormSubmissionOutcome(FormSubmissionOutcome::kRendererError);
+}
+
 void FormActivityTabHelper::OnFormMessageReceived(
     web::WebState* web_state,
     const web::ScriptMessage& message) {
@@ -227,9 +252,11 @@
     return;
   }
 
-  RecordMetrics(message.body()->GetDict());
+  const auto& message_body = message.body()->GetDict();
 
-  const std::string* command = message.body()->GetDict().FindString("command");
+  RecordMetrics(message_body);
+
+  const std::string* command = message_body.FindString("command");
   if (!command) {
     DLOG(WARNING) << "JS message parameter not found: command";
   } else if (*command == "form.submit") {
@@ -238,6 +265,8 @@
     HandleFormActivity(web_state, message);
   } else if (*command == "form.removal") {
     HandleFormRemoval(web_state, message);
+  } else if (*command == "form.submit.error") {
+    HandleSubmissionError(message_body);
   }
 }
 
diff --git a/components/autofill/ios/form_util/resources/autofill_form_features.ts b/components/autofill/ios/form_util/resources/autofill_form_features.ts
index 36f532d3..561e7b92 100644
--- a/components/autofill/ios/form_util/resources/autofill_form_features.ts
+++ b/components/autofill/ios/form_util/resources/autofill_form_features.ts
@@ -55,6 +55,13 @@
 let autofillDedupeFormSubmission: boolean = false;
 // LINT.ThenChange(//components/autofill/ios/common/features.mm:autofill_dedupe_form_submission)
 
+// LINT.IfChange(autofill_report_form_submission_errors)
+/**
+ * Reports JS errors that occur upon handling form submission in the renderer.
+ */
+let autofillReportFormSubmissionErrors: boolean = false;
+// LINT.ThenChange(//components/autofill/ios/common/features.mm:autofill_report_form_submission_errors)
+
 /**
  * @see autofillAcrossIframes
  */
@@ -140,6 +147,20 @@
   return autofillDedupeFormSubmission;
 }
 
+/**
+ * @see autofillReportFormSubmissionErrors
+ */
+function setAutofillReportFormSubmissionErrors(enabled: boolean): void {
+  autofillReportFormSubmissionErrors = enabled;
+}
+
+/**
+ * @see autofillReportFormSubmissionErrors
+ */
+function isAutofillReportFormSubmissionErrorsEnabled(): boolean {
+  return autofillReportFormSubmissionErrors;
+}
+
 // Expose globally via `gCrWeb` instead of `export` to ensure state (feature
 // on/off) is maintained across imports.
 gCrWebLegacy.autofill_form_features = {
@@ -155,4 +176,6 @@
   isAutofillAllowDefaultPreventedSubmission,
   setAutofillDedupeFormSubmission,
   isAutofillDedupeFormSubmissionEnabled,
+  setAutofillReportFormSubmissionErrors,
+  isAutofillReportFormSubmissionErrorsEnabled,
 };
diff --git a/components/autofill/ios/form_util/resources/form.ts b/components/autofill/ios/form_util/resources/form.ts
index a678639..32c6ef7 100644
--- a/components/autofill/ios/form_util/resources/form.ts
+++ b/components/autofill/ios/form_util/resources/form.ts
@@ -325,7 +325,7 @@
 }
 
 // Send the form data to the browser.
-function formSubmitted(
+function formSubmittedInternal(
     form: HTMLFormElement,
     messageHandler: string,
     programmaticSubmission: boolean,
@@ -358,6 +358,65 @@
   sendWebKitMessage(messageHandler, message);
 }
 
+/**
+ * Sends the form data to the browser. Errors that are caught via the try/catch
+ * are reported to the browser. This is done before the error bubbles above
+ * `formSubmitted()` so the generic JS errors wrapper doesn't intercept the
+ * error before this custom error handler.
+ *
+ * @param form The form that was submitted.
+ * @param messageHandler The name of the message handler to send the message to.
+ * @param programmaticSubmission True if the form submission is programmatic.
+ * @includeRemoteFrameToken True if the remote frame token should be included
+ *   in the payload of the message sent to the browser.
+ */
+function formSubmitted(
+    form: HTMLFormElement,
+    messageHandler: string,
+    programmaticSubmission: boolean,
+    includeRemoteFrameToken: boolean = false,
+    ): void {
+  try {
+    formSubmittedInternal(
+        form, messageHandler, programmaticSubmission, includeRemoteFrameToken);
+  } catch (error) {
+    if (gCrWebLegacy.autofill_form_features
+            .isAutofillReportFormSubmissionErrorsEnabled()) {
+      reportFormSubmissionError(error, programmaticSubmission, messageHandler);
+    } else {
+      // Just let the error go through if not reported.
+      throw error;
+    }
+  }
+}
+
+/**
+ * Reports a form submission error to the browser.
+ * @param error Object that holds information on the error.
+ * @param programmaticSubmission True if the submission that errored was
+ *   programmatic.
+ * @param handler The name of the handler to send the error message to.
+ */
+function reportFormSubmissionError(
+    error: any, programmaticSubmission: boolean, handler: string) {
+  let errorMessage = '';
+  let errorStack = '';
+  if (error && error instanceof Error) {
+    errorMessage = error.message;
+    if (error.stack) {
+      errorStack = error.stack;
+    }
+  }
+
+  const message = {
+    command: 'form.submit.error',
+    errorStack,
+    errorMessage,
+    programmaticSubmission,
+  };
+  sendWebKitMessage(handler, message);
+}
+
 gCrWebLegacy.form = {
   wasEditedByUser,
   isFormControlElement,
@@ -370,4 +429,5 @@
   getFormElementFromRendererId,
   fieldWasEditedByUser,
   formSubmitted,
+  reportFormSubmissionError,
 };
diff --git a/components/autofill/ios/form_util/resources/form_handlers.ts b/components/autofill/ios/form_util/resources/form_handlers.ts
index d087a66..d9e167b 100644
--- a/components/autofill/ios/form_util/resources/form_handlers.ts
+++ b/components/autofill/ios/form_util/resources/form_handlers.ts
@@ -207,6 +207,26 @@
 }
 
 /**
+ * A wrapper around `submitHandler()` that catches and reports errors that
+ * happen before calling gCrWebLegacy.form.formSubmitted().
+ */
+function submitHandlerWithErrorWrapper(evt: Event): void {
+  try {
+    submitHandler(evt);
+  } catch (error) {
+    if (gCrWebLegacy.autofill_form_features
+            .isAutofillReportFormSubmissionErrorsEnabled()) {
+      gCrWebLegacy.form.reportFormSubmissionError(
+          error, /*programmaticSubmission=*/ false,
+          /*handler=*/ NATIVE_MESSAGE_HANDLER);
+    } else {
+      // Just let the error go through if not reported.
+      throw error;
+    }
+  }
+}
+
+/**
  * Schedules `messages` to be sent back-to-back after `delay` and with a `delay`
  * between them.
  *
@@ -279,7 +299,7 @@
    */
   document.addEventListener('keyup', formActivity, false);
   document.addEventListener(
-      'submit', submitHandler,
+      'submit', submitHandlerWithErrorWrapper,
       shouldListenToFormSubmissionEventsInCaptureMode());
 
   /**
diff --git a/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/TimeTextResolver.java b/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/TimeTextResolver.java
index 7a5e2eba..7596394 100644
--- a/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/TimeTextResolver.java
+++ b/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/TimeTextResolver.java
@@ -4,12 +4,12 @@
 
 package org.chromium.components.browser_ui.util;
 
-import android.content.res.Resources;
 import android.util.Pair;
 
+import org.chromium.base.TimeUtils;
 import org.chromium.build.annotations.NullMarked;
+import android.content.res.Resources;
 
-import java.time.Clock;
 import java.time.temporal.ChronoUnit;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -25,38 +25,27 @@
                     new Pair<>(ChronoUnit.MONTHS, R.plurals.time_last_accessed_months),
                     new Pair<>(ChronoUnit.WEEKS, R.plurals.time_last_accessed_weeks),
                     new Pair<>(ChronoUnit.DAYS, R.plurals.time_last_accessed_days));
-    private final Resources mResources;
-    private final Clock mClock;
-
-    /**
-     * @param resources Used to resolve strings against.
-     * @param clock Used to get the current time.
-     */
-    public TimeTextResolver(Resources resources, Clock clock) {
-        mResources = resources;
-        mClock = clock;
-    }
 
     /**
      * @param timestamp The timestamp in milliseconds.
-     * @return Simple text representing the time since a given timestamp.
-     *     Represented as one of: Today > Yesterday > x days ago > x weeks ago > x months ago.
+     * @return Simple text representing the time since a given timestamp. Represented as one of:
+     *     Today > Yesterday > x days ago > x weeks ago > x months ago.
      */
     // duration.getSeconds should be toSeconds after api 31.
     @SuppressWarnings("JavaDurationGetSecondsToToSeconds")
-    public String resolveTimeAgoText(long timestamp) {
+    public static String resolveTimeAgoText(Resources resources, long timestamp) {
         List<Pair<ChronoUnit, Integer>> selectedChronoUnitAndPluralRes =
                 LAST_ACCESSED_CHRONO_UNIT_AND_PLURAL_RES;
 
-        long nowMillis = mClock.millis();
+        long nowMillis = TimeUtils.currentTimeMillis();
         int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(nowMillis - timestamp);
         for (Pair<ChronoUnit, Integer> pair : selectedChronoUnitAndPluralRes) {
             int count = (int) (seconds / pair.first.getDuration().getSeconds());
             if (count >= 1) {
-                return mResources.getQuantityString(pair.second, count, count);
+                return resources.getQuantityString(pair.second, count, count);
             }
         }
 
-        return mResources.getString(R.string.time_last_accessed_today);
+        return resources.getString(R.string.time_last_accessed_today);
     }
 }
diff --git a/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/TimeTextResolverUnitTest.java b/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/TimeTextResolverUnitTest.java
index 0e6a04a..277718b 100644
--- a/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/TimeTextResolverUnitTest.java
+++ b/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/TimeTextResolverUnitTest.java
@@ -5,7 +5,6 @@
 package org.chromium.components.browser_ui.util;
 
 import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.when;
 
 import android.app.Activity;
 
@@ -13,33 +12,29 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
-
-import org.chromium.base.test.BaseRobolectricTestRunner;
-
-import java.time.Clock;
 import org.robolectric.Robolectric;
 
+import org.chromium.base.TimeUtils;
+import org.chromium.base.test.BaseRobolectricTestRunner;
+
 /** Unit tests for {@link TimeTextResolver}. */
 @RunWith(BaseRobolectricTestRunner.class)
 public class TimeTextResolverUnitTest {
     private Activity mActivity;
     @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
 
-    @Mock private Clock mClock;
     TimeTextResolver mResolver;
 
     @Before
     public void setup() {
-        when(mClock.millis()).thenReturn(0L);
         mActivity = Robolectric.setupActivity(Activity.class);
-        mResolver = new TimeTextResolver(mActivity.getResources(), mClock);
     }
 
     private String fromSecondsAgo(long secondsDelta) {
-        return mResolver.resolveTimeAgoText(0 - 1000 * secondsDelta);
+        return TimeTextResolver.resolveTimeAgoText(
+                mActivity.getResources(), TimeUtils.currentTimeMillis() - 1000 * secondsDelta);
     }
 
     @Test
diff --git a/components/data_sharing/public/features.cc b/components/data_sharing/public/features.cc
index 88c2e879..e519b66 100644
--- a/components/data_sharing/public/features.cc
+++ b/components/data_sharing/public/features.cc
@@ -10,11 +10,15 @@
 namespace data_sharing::features {
 namespace {
 const char kDataSharingDefaultUrl[] = "https://www.google.com/chrome/tabshare/";
-const char kLearnMoreSharedTabGroupPageDefaultUrl[] = "https://support.google.com/chrome/?p=chrome_collaboration";
-const char kLearnAboutBlockedAccountsDefaultUrl[] = "https://support.google.com/accounts/answer/6388749";
-const char kActivityLogsDefaultUrl[] = "https://myactivity.google.com/product/chrome_shared_tab_group_activity?utm_source=chrome_collab";
+const char kLearnMoreSharedTabGroupPageDefaultUrl[] =
+    "https://support.google.com/chrome/?p=chrome_collaboration";
+const char kLearnAboutBlockedAccountsDefaultUrl[] =
+    "https://support.google.com/accounts/answer/6388749";
+const char kActivityLogsDefaultUrl[] =
+    "https://myactivity.google.com/product/"
+    "chrome_shared_tab_group_activity?utm_source=chrome_collab";
 
-}
+}  // namespace
 
 BASE_FEATURE(kCollaborationAutomotive,
              "CollaborationAutomotive",
@@ -32,6 +36,10 @@
              "DataSharingJoinOnly",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+BASE_FEATURE(kDataSharingEnableUpdateChromeUI,
+             "DataSharingEnableUpdateChromeUI",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 bool IsDataSharingFunctionalityEnabled() {
   return base::FeatureList::IsEnabled(
              data_sharing::features::kDataSharingFeature) ||
diff --git a/components/data_sharing/public/features.h b/components/data_sharing/public/features.h
index 26270b4..334e9d76 100644
--- a/components/data_sharing/public/features.h
+++ b/components/data_sharing/public/features.h
@@ -29,6 +29,13 @@
 // autopush server environment is set.
 BASE_DECLARE_FEATURE(kDataSharingNonProductionEnvironment);
 
+// Feature flag to show UI that prompts the user to update Chrome when a version
+// mismatch is detected.
+// Note: Do not clean up this feature as it is meant to be used in unforeseen
+// situations as a kill switch in future from finch when the shared tab groups
+// feature becomes incompatible for the current chrome client.
+BASE_DECLARE_FEATURE(kDataSharingEnableUpdateChromeUI);
+
 extern const base::FeatureParam<std::string> kDataSharingURL;
 extern const base::FeatureParam<std::string> kLearnMoreSharedTabGroupPageURL;
 extern const base::FeatureParam<std::string> kLearnAboutBlockedAccountsURL;
diff --git a/components/input/child_frame_input_helper.cc b/components/input/child_frame_input_helper.cc
index fd95023..fcd8c03 100644
--- a/components/input/child_frame_input_helper.cc
+++ b/components/input/child_frame_input_helper.cc
@@ -4,6 +4,7 @@
 
 #include "components/input/child_frame_input_helper.h"
 
+#include "base/trace_event/trace_event.h"
 #include "components/input/features.h"
 #include "components/input/render_input_router.h"
 #include "components/input/render_widget_host_input_event_router.h"
diff --git a/components/input/mouse_wheel_event_queue.cc b/components/input/mouse_wheel_event_queue.cc
index b3b56de..a30575b 100644
--- a/components/input/mouse_wheel_event_queue.cc
+++ b/components/input/mouse_wheel_event_queue.cc
@@ -6,6 +6,7 @@
 
 #include "base/functional/bind.h"
 #include "base/metrics/histogram_macros.h"
+#include "base/trace_event/trace_event.h"
 #include "build/build_config.h"
 #include "ui/events/base_event_utils.h"
 #include "ui/events/blink/web_input_event_traits.h"
diff --git a/components/input/render_input_router.cc b/components/input/render_input_router.cc
index fea9f03..a9e2404 100644
--- a/components/input/render_input_router.cc
+++ b/components/input/render_input_router.cc
@@ -12,6 +12,7 @@
 #include "base/location.h"
 #include "base/memory/ptr_util.h"
 #include "base/task/single_thread_task_runner.h"
+#include "base/trace_event/trace_event.h"
 #include "base/tracing/protos/chrome_track_event.pbzero.h"
 #include "cc/input/browser_controls_offset_tag_modifications.h"
 #include "components/input/input_constants.h"
diff --git a/components/input/render_widget_targeter.cc b/components/input/render_widget_targeter.cc
index 091c237ab..5bf5c12 100644
--- a/components/input/render_widget_targeter.cc
+++ b/components/input/render_widget_targeter.cc
@@ -11,6 +11,7 @@
 #include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/rand_util.h"
+#include "base/trace_event/trace_event.h"
 #include "components/input/render_widget_host_view_input.h"
 #include "third_party/blink/public/common/input/web_input_event.h"
 #include "ui/events/blink/blink_event_util.h"
diff --git a/components/lens/lens_overlay_metrics.cc b/components/lens/lens_overlay_metrics.cc
index 5a557ea..009aedf5 100644
--- a/components/lens/lens_overlay_metrics.cc
+++ b/components/lens/lens_overlay_metrics.cc
@@ -522,4 +522,9 @@
       "Lens.Overlay.SidePanel.SelectedMoreInfoMenuOption", menu_option);
 }
 
+void RecordHandleTextDirectiveResult(
+    lens::LensOverlayTextDirectiveResult result) {
+  base::UmaHistogramEnumeration("Lens.Overlay.TextDirectiveResult", result);
+}
+
 }  // namespace lens
diff --git a/components/lens/lens_overlay_metrics.h b/components/lens/lens_overlay_metrics.h
index e37e63a..95eb54b 100644
--- a/components/lens/lens_overlay_metrics.h
+++ b/components/lens/lens_overlay_metrics.h
@@ -55,6 +55,19 @@
   bool searchbox_shown_ = false;
 };
 
+// LINT.IfChange(LensOverlayTextDirectiveResult)
+enum class LensOverlayTextDirectiveResult {
+  // The text directive was found on the page.
+  kFoundOnPage = 0,
+  // The URL with a text directive was opened in a new tab because it did not
+  // match the current page.
+  kOpenedInNewTab = 1,
+  // The text directive was not found on the page.
+  kNotFoundOnPage = 2,
+  kMaxValue = kNotFoundOnPage,
+};
+// LINT.ThenChange(//tools/metrics/histograms/metadata/lens/enums.xml:LensOverlayTextDirectiveResult)
+
 // Returns the string representation of the invocation source.
 std::string InvocationSourceToString(
     LensOverlayInvocationSource invocation_source);
@@ -174,6 +187,10 @@
 void RecordSidePanelMenuOptionSelected(
     lens::LensOverlaySidePanelMenuOption menu_option);
 
+// Records the result of handling a text directive in the Lens Overlay.
+void RecordHandleTextDirectiveResult(
+    lens::LensOverlayTextDirectiveResult result);
+
 }  // namespace lens
 
 #endif  // COMPONENTS_LENS_LENS_OVERLAY_METRICS_H_
diff --git a/components/omnibox/composebox/composebox_query_controller.h b/components/omnibox/composebox/composebox_query_controller.h
index 670947d1..53b37da 100644
--- a/components/omnibox/composebox/composebox_query_controller.h
+++ b/components/omnibox/composebox/composebox_query_controller.h
@@ -19,9 +19,9 @@
   ComposeboxQueryController() = default;
   virtual ~ComposeboxQueryController();
 
-  // Session management.
-  void NotifySessionStarted();
-  void NotifySessionAbandoned();
+  // Session management. Virtual for testing.
+  virtual void NotifySessionStarted();
+  virtual void NotifySessionAbandoned();
 
   SessionState session_state() { return session_state_; }
 
diff --git a/components/optimization_guide/content/browser/BUILD.gn b/components/optimization_guide/content/browser/BUILD.gn
index 903587e..cc06cd3f 100644
--- a/components/optimization_guide/content/browser/BUILD.gn
+++ b/components/optimization_guide/content/browser/BUILD.gn
@@ -33,15 +33,6 @@
     "//services/metrics/public/cpp:ukm_builders",
     "//services/network/public/cpp",
   ]
-  if (build_with_tflite_lib) {
-    public_deps += [
-      "//components/optimization_guide/core:machine_learning",
-      "//third_party/tflite",
-      "//third_party/tflite:tflite_public_headers",
-      "//third_party/tflite_support",
-      "//third_party/tflite_support:tflite_support_proto",
-    ]
-  }
   configs += [ "//build/config/compiler:wexit_time_destructors" ]
 }
 
diff --git a/components/optimization_guide/core/BUILD.gn b/components/optimization_guide/core/BUILD.gn
index c1f1c62d..28bcc7c2 100644
--- a/components/optimization_guide/core/BUILD.gn
+++ b/components/optimization_guide/core/BUILD.gn
@@ -7,10 +7,7 @@
 if (is_android) {
   import("//build/config/android/rules.gni")
 }
-import("//build/buildflag_header.gni")
 import("//components/optimization_guide/features.gni")
-import("//third_party/mediapipe/features.gni")
-import("//third_party/tflite/features.gni")
 
 # bloomfilter is isolated because of the allowlisted murmurhash3 dep.
 source_set("bloomfilter") {
@@ -50,98 +47,12 @@
   visibility = [ ":*" ]
 }
 
-static_library("model_executor") {
-  sources = [
-    "inference/execution_status.cc",
-    "inference/execution_status.h",
-    "inference/model_executor.h",
-    "inference/model_handler.h",
-  ]
-  if (build_with_tflite_lib) {
-    sources += [
-      "inference/base_model_executor.h",
-      "inference/base_model_executor_helpers.h",
-      "inference/bert_model_executor.cc",
-      "inference/bert_model_executor.h",
-      "inference/bert_model_handler.cc",
-      "inference/bert_model_handler.h",
-      "inference/model_execution_timeout_watchdog.cc",
-      "inference/model_execution_timeout_watchdog.h",
-      "inference/model_validator.cc",
-      "inference/model_validator.h",
-      "inference/signature_model_executor.h",
-      "inference/tflite_model_executor.h",
-    ]
-  }
-
-  if (build_with_mediapipe_lib) {
-    assert(build_with_tflite_lib)
-
-    sources += [
-      "inference/mediapipe_text_model_executor.cc",
-      "inference/mediapipe_text_model_executor.h",
-    ]
-  }
-
-  public_deps = [
-    "//components/optimization_guide:machine_learning_tflite_buildflags",
-    "//components/optimization_guide/proto:optimization_guide_proto",
-    "//third_party/re2",
-  ]
-
-  deps = [
-    ":core_util",
-    ":features",
-    ":prediction",
-    "//base",
-    "//net",
-    "//url",
-  ]
-
-  configs += [ "//build/config/compiler:wexit_time_destructors" ]
-
-  if (build_with_tflite_lib) {
-    public_deps += [
-      "//components/optimization_guide/core:machine_learning",
-      "//third_party/abseil-cpp:absl",
-      "//third_party/tflite",
-      "//third_party/tflite:tflite_public_headers",
-      "//third_party/tflite_support",
-      "//third_party/tflite_support:tflite_support_proto",
-    ]
-    deps += [ ":machine_learning" ]
-  }
-
-  if (build_with_mediapipe_lib) {
-    assert(build_with_tflite_lib)
-
-    public_deps += [ "//third_party/mediapipe" ]
-  }
-}
-
 mojom("interfaces") {
   sources = [ "optimization_guide_common.mojom" ]
   webui_module_path = "/"
   public_deps = [ "//mojo/public/mojom/base" ]
 }
 
-if (build_with_tflite_lib) {
-  static_library("machine_learning") {
-    sources = [
-      "tflite_op_resolver.cc",
-      "tflite_op_resolver.h",
-    ]
-    deps = [
-      ":features",
-      "//components/optimization_guide:machine_learning_tflite_buildflags",
-      "//third_party/tflite",
-      "//third_party/tflite:buildflags",
-      "//third_party/tflite:tflite_public_headers",
-    ]
-    configs += [ "//build/config/compiler:wexit_time_destructors" ]
-  }
-}
-
 component("features") {
   sources = [
     "feature_registry/enterprise_policy_registry.cc",
@@ -271,7 +182,7 @@
     "//ui/accessibility:ax_enums_mojo",
     "//url",
   ]
-  visibility = [ ":*" ]
+  visibility = [ "./*" ]
 }
 
 source_set("model_execution") {
@@ -385,7 +296,6 @@
     ]
     deps += [
       ":core_util",
-      ":model_executor",
       ":on_device_model_execution_proto_generator",
       ":prediction",
       "//components/component_updater",
@@ -417,7 +327,6 @@
     ":filters",
     ":hints",
     ":model_execution",
-    ":model_executor",
     ":prediction",
     "//components/autofill/core/common",
     "//components/leveldb_proto",
@@ -428,12 +337,9 @@
   ]
   if (build_with_tflite_lib) {
     public_deps += [
-      "//components/optimization_guide/core:machine_learning",
-      "//third_party/abseil-cpp:absl",
-      "//third_party/tflite",
-      "//third_party/tflite:tflite_public_headers",
-      "//third_party/tflite_support",
-      "//third_party/tflite_support:tflite_support_proto",
+      "//components/optimization_guide/core/inference",
+
+      # TODO: crbug.com/421262905 - Don't provide compression_utils
       "//third_party/zlib/google:compression_utils",
     ]
   }
@@ -550,27 +456,7 @@
     "//base",
     "//components/optimization_guide/proto:optimization_guide_proto",
   ]
-}
-
-static_library("inference_test_support") {
-  testonly = true
-  sources = [
-    "inference/test_model_executor.cc",
-    "inference/test_model_executor.h",
-    "inference/test_model_handler.h",
-  ]
-  deps = [
-    ":model_executor",
-    "//base",
-  ]
-  if (build_with_tflite_lib) {
-    sources += [
-      "inference/test_tflite_model_executor.cc",
-      "inference/test_tflite_model_executor.h",
-      "inference/test_tflite_model_handler.h",
-    ]
-    deps += [ "//third_party/abseil-cpp:absl" ]
-  }
+  visibility = [ "./*" ]
 }
 
 static_library("model_execution_test_support") {
@@ -631,9 +517,12 @@
     ":delivery_test_support",
     ":filters_test_support",
     ":hints_test_support",
-    ":inference_test_support",
     ":model_execution_test_support",
   ]
+  if (build_with_tflite_lib) {
+    public_deps +=
+        [ "//components/optimization_guide/core/inference:test_support" ]
+  }
 }
 
 if (is_ios) {
@@ -745,43 +634,6 @@
   ]
 }
 
-source_set("inference_unit_tests") {
-  testonly = true
-  sources = [ "inference/model_handler_unittest.cc" ]
-  deps = [
-    ":delivery_test_support",
-    ":inference_test_support",
-    ":model_executor",
-    "//base",
-    "//base/test:test_support",
-    "//components/optimization_guide/proto:optimization_guide_proto",
-    "//testing/gtest",
-  ]
-  if (build_with_tflite_lib) {
-    sources += [
-      "inference/bert_model_executor_unittest.cc",
-      "inference/model_validator_unittest.cc",
-      "inference/tflite_model_executor_unittest.cc",
-    ]
-    deps += [
-      ":core_util",
-      ":features",
-      ":prediction",
-      "//third_party/abseil-cpp:absl",
-      "//third_party/tflite",
-      "//third_party/tflite:tflite_public_headers",
-      "//third_party/tflite_support",
-      "//third_party/tflite_support:tflite_support_proto",
-    ]
-  }
-  if (build_with_mediapipe_lib) {
-    assert(build_with_tflite_lib)
-
-    sources += [ "mediapipe_text_model_executor_unittest.cc" ]
-    deps += [ "//third_party/mediapipe" ]
-  }
-}
-
 source_set("model_execution_unit_tests") {
   testonly = true
   sources = [
@@ -865,14 +717,12 @@
     "optimization_guide_switches_unittest.cc",
     "optimization_guide_util_unittest.cc",
   ]
-
   deps = [
     ":core",
     ":delivery_unit_tests",
     ":features",
     ":filters_unit_tests",
     ":hints_unit_tests",
-    ":inference_unit_tests",
     ":model_execution_unit_tests",
     "//base",
     "//base/test:test_support",
@@ -884,6 +734,9 @@
     "//testing/gmock",
     "//testing/gtest",
   ]
+  if (build_with_tflite_lib) {
+    deps += [ "//components/optimization_guide/core/inference:unit_tests" ]
+  }
 }
 
 bundle_data("unit_tests_bundle_data") {
diff --git a/components/optimization_guide/core/inference/BUILD.gn b/components/optimization_guide/core/inference/BUILD.gn
new file mode 100644
index 0000000..9f98ca3
--- /dev/null
+++ b/components/optimization_guide/core/inference/BUILD.gn
@@ -0,0 +1,123 @@
+# Copyright 2025 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//components/optimization_guide/features.gni")
+import("//third_party/mediapipe/features.gni")
+
+assert(build_with_tflite_lib)
+
+visibility = [ "//components/optimization_guide/core/*" ]
+
+static_library("op_resolver") {
+  # TODO: crbug.com/422803641 - Move these files to this directory.
+  sources = [
+    "../tflite_op_resolver.cc",
+    "../tflite_op_resolver.h",
+  ]
+  deps = [
+    "//components/optimization_guide:machine_learning_tflite_buildflags",
+    "//components/optimization_guide/core:features",
+    "//third_party/tflite",
+    "//third_party/tflite:buildflags",
+    "//third_party/tflite:tflite_public_headers",
+  ]
+  configs += [ "//build/config/compiler:wexit_time_destructors" ]
+  visibility += [ "//services/passage_embeddings/*" ]
+}
+
+static_library("inference") {
+  sources = [
+    "base_model_executor.h",
+    "base_model_executor_helpers.h",
+    "bert_model_executor.cc",
+    "bert_model_executor.h",
+    "bert_model_handler.cc",
+    "bert_model_handler.h",
+    "execution_status.cc",
+    "execution_status.h",
+    "model_execution_timeout_watchdog.cc",
+    "model_execution_timeout_watchdog.h",
+    "model_executor.h",
+    "model_handler.h",
+    "model_validator.cc",
+    "model_validator.h",
+    "signature_model_executor.h",
+    "tflite_model_executor.h",
+  ]
+  public_deps = [
+    ":op_resolver",
+    "//components/optimization_guide:machine_learning_tflite_buildflags",
+    "//components/optimization_guide/proto:optimization_guide_proto",
+    "//third_party/abseil-cpp:absl",
+    "//third_party/re2",
+    "//third_party/tflite",
+    "//third_party/tflite:tflite_public_headers",
+    "//third_party/tflite_support",
+    "//third_party/tflite_support:tflite_support_proto",
+  ]
+  deps = [
+    "//base",
+    "//components/optimization_guide/core:core_util",
+    "//components/optimization_guide/core:features",
+    "//components/optimization_guide/core:prediction",
+    "//net",
+    "//url",
+  ]
+  configs += [ "//build/config/compiler:wexit_time_destructors" ]
+  if (build_with_mediapipe_lib) {
+    sources += [
+      "mediapipe_text_model_executor.cc",
+      "mediapipe_text_model_executor.h",
+    ]
+    public_deps += [ "//third_party/mediapipe" ]
+  }
+}
+
+static_library("test_support") {
+  testonly = true
+  sources = [
+    "test_model_executor.cc",
+    "test_model_executor.h",
+    "test_model_handler.h",
+    "test_tflite_model_executor.cc",
+    "test_tflite_model_executor.h",
+    "test_tflite_model_handler.h",
+  ]
+  deps = [
+    ":inference",
+    "//base",
+    "//third_party/abseil-cpp:absl",
+  ]
+}
+
+source_set("unit_tests") {
+  testonly = true
+  sources = [
+    "bert_model_executor_unittest.cc",
+    "model_handler_unittest.cc",
+    "model_validator_unittest.cc",
+    "tflite_model_executor_unittest.cc",
+  ]
+  deps = [
+    ":inference",
+    ":test_support",
+    "//base",
+    "//base/test:test_support",
+    "//components/optimization_guide/core:core_util",
+    "//components/optimization_guide/core:delivery_test_support",
+    "//components/optimization_guide/core:features",
+    "//components/optimization_guide/core:prediction",
+    "//components/optimization_guide/proto:optimization_guide_proto",
+    "//testing/gtest",
+    "//third_party/abseil-cpp:absl",
+    "//third_party/tflite",
+    "//third_party/tflite:tflite_public_headers",
+    "//third_party/tflite_support",
+    "//third_party/tflite_support:tflite_support_proto",
+  ]
+  if (build_with_mediapipe_lib) {
+    sources += [ "mediapipe_text_model_executor_unittest.cc" ]
+    deps += [ "//third_party/mediapipe" ]
+  }
+}
diff --git a/components/page_content_annotations/core/BUILD.gn b/components/page_content_annotations/core/BUILD.gn
index 8b707fb..5176040 100644
--- a/components/page_content_annotations/core/BUILD.gn
+++ b/components/page_content_annotations/core/BUILD.gn
@@ -56,7 +56,6 @@
     "//components/omnibox/common",
     "//components/optimization_guide/core",
     "//components/optimization_guide/core:features",
-    "//components/optimization_guide/core:model_executor",
     "//components/search",
     "//components/search_engines",
     "//services/metrics/public/cpp:metrics_cpp",
diff --git a/components/safe_browsing/content/browser/BUILD.gn b/components/safe_browsing/content/browser/BUILD.gn
index 1fc1f8d..2c5ebe0 100644
--- a/components/safe_browsing/content/browser/BUILD.gn
+++ b/components/safe_browsing/content/browser/BUILD.gn
@@ -173,7 +173,6 @@
     "//base:base",
     "//base/test:test_support",
     "//components/optimization_guide/core",
-    "//components/optimization_guide/core:model_executor",
     "//components/optimization_guide/core:test_support",
     "//components/optimization_guide/proto:optimization_guide_proto",
     "//components/safe_browsing:buildflags",
diff --git a/components/safe_browsing/content/browser/notification_content_detection/BUILD.gn b/components/safe_browsing/content/browser/notification_content_detection/BUILD.gn
index 4bee9255..f84f0be 100644
--- a/components/safe_browsing/content/browser/notification_content_detection/BUILD.gn
+++ b/components/safe_browsing/content/browser/notification_content_detection/BUILD.gn
@@ -60,7 +60,6 @@
     "//base:base",
     "//base/test:test_support",
     "//components/optimization_guide/core",
-    "//components/optimization_guide/core:model_executor",
     "//components/optimization_guide/core:test_support",
     "//components/optimization_guide/proto:optimization_guide_proto",
     "//components/permissions:test_support",
diff --git a/components/saved_tab_groups/components_unittests.filter b/components/saved_tab_groups/components_unittests.filter
index ba4f59c4..1e8dee1 100644
--- a/components/saved_tab_groups/components_unittests.filter
+++ b/components/saved_tab_groups/components_unittests.filter
@@ -13,3 +13,4 @@
 *TabGroupSyncMetricsLoggerTest.*
 *TabGroupSyncServiceAndroidTest.*
 *TabGroupSyncServiceTest.*
+*VersioningMessageControllerImplTest.*
diff --git a/components/saved_tab_groups/internal/BUILD.gn b/components/saved_tab_groups/internal/BUILD.gn
index 27a9ed71..dc3d2e2 100644
--- a/components/saved_tab_groups/internal/BUILD.gn
+++ b/components/saved_tab_groups/internal/BUILD.gn
@@ -22,6 +22,8 @@
     "tab_group_sync_service_impl.h",
     "tab_group_type_observer.cc",
     "tab_group_type_observer.h",
+    "versioning_message_controller_impl.cc",
+    "versioning_message_controller_impl.h",
   ]
   deps = [
     ":tab_group_sync_bridge",
@@ -48,6 +50,8 @@
     sources += [
       "android/tab_group_sync_service_android.cc",
       "android/tab_group_sync_service_android.h",
+      "android/versioning_message_controller_android.cc",
+      "android/versioning_message_controller_android.h",
     ]
 
     deps += [
@@ -81,7 +85,10 @@
 
 if (is_android) {
   android_library("java") {
-    sources = [ "android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceImpl.java" ]
+    sources = [
+      "android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceImpl.java",
+      "android/java/src/org/chromium/components/tab_group_sync/VersioningMessageControllerImpl.java",
+    ]
 
     deps = [
       "//base:base_java",
@@ -103,7 +110,10 @@
       "//chrome/browser",
     ]
 
-    sources = [ "android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceImpl.java" ]
+    sources = [
+      "android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceImpl.java",
+      "android/java/src/org/chromium/components/tab_group_sync/VersioningMessageControllerImpl.java",
+    ]
   }
 
   android_library("native_java_unittests_java") {
@@ -168,6 +178,7 @@
     "tab_group_sync_metrics_logger_unittest.cc",
     "tab_group_sync_service_unittest.cc",
     "tab_group_type_observer_unittest.cc",
+    "versioning_message_controller_impl_unittest.cc",
   ]
   deps = [
     ":internal",
diff --git a/components/saved_tab_groups/internal/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceAndroidUnitTest.java b/components/saved_tab_groups/internal/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceAndroidUnitTest.java
index 30c55c0..0de6851 100644
--- a/components/saved_tab_groups/internal/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceAndroidUnitTest.java
+++ b/components/saved_tab_groups/internal/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceAndroidUnitTest.java
@@ -16,6 +16,7 @@
 import org.junit.Assert;
 import org.mockito.ArgumentCaptor;
 
+import org.chromium.base.Callback;
 import org.chromium.base.ThreadUtils;
 import org.chromium.base.Token;
 import org.chromium.components.tab_groups.TabGroupColorId;
@@ -41,11 +42,11 @@
     private static final int LOCAL_TAB_ID_1 = 2;
     private static final int LOCAL_TAB_ID_2 = 4;
     private static final int TAB_POSITION = 3;
-
     private TabGroupSyncService mService;
     private TabGroupSyncService.Observer mObserver;
     private final ArgumentCaptor<SavedTabGroup> mTabGroupCaptor =
             ArgumentCaptor.forClass(SavedTabGroup.class);
+    private Callback<Boolean> mCallback;
 
     @CalledByNative
     private TabGroupSyncServiceAndroidUnitTest() {
@@ -257,4 +258,25 @@
     public void testUpdateArchivalStatus(String uuid, boolean archivalStatus) {
         mService.updateArchivalStatus(uuid, archivalStatus);
     }
+
+    @CalledByNative
+    public void testShouldShowMessageUiAsync() {
+        mCallback = mock(Callback.class);
+        mService.getVersioningMessageController()
+                .shouldShowMessageUiAsync(
+                        MessageType.VERSION_OUT_OF_DATE_INSTANT_MESSAGE, mCallback);
+        verify(mCallback).onResult(true);
+    }
+
+    @CalledByNative
+    public void testOnMessageUiShown() {
+        mService.getVersioningMessageController()
+                .onMessageUiShown(MessageType.VERSION_OUT_OF_DATE_INSTANT_MESSAGE);
+    }
+
+    @CalledByNative
+    public void testOnMessageUiDismissed() {
+        mService.getVersioningMessageController()
+                .onMessageUiDismissed(MessageType.VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE);
+    }
 }
diff --git a/components/saved_tab_groups/internal/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceImpl.java b/components/saved_tab_groups/internal/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceImpl.java
index 42e0d6e..e6bbc87 100644
--- a/components/saved_tab_groups/internal/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceImpl.java
+++ b/components/saved_tab_groups/internal/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncServiceImpl.java
@@ -257,6 +257,11 @@
     }
 
     @Override
+    public VersioningMessageController getVersioningMessageController() {
+        return TabGroupSyncServiceImplJni.get().getVersioningMessageController(mNativePtr, this);
+    }
+
+    @Override
     public void setCollaborationAvailableInFinderForTesting(String collaborationId) {
         if (mNativePtr == 0) return;
         TabGroupSyncServiceImplJni.get()
@@ -454,6 +459,9 @@
                 String syncTabGroupId,
                 boolean archivalStatus);
 
+        VersioningMessageController getVersioningMessageController(
+                long nativeTabGroupSyncServiceAndroid, TabGroupSyncServiceImpl caller);
+
         void setCollaborationAvailableInFinderForTesting(
                 long nativeTabGroupSyncServiceAndroid,
                 TabGroupSyncServiceImpl caller,
diff --git a/components/saved_tab_groups/internal/android/java/src/org/chromium/components/tab_group_sync/VersioningMessageControllerImpl.java b/components/saved_tab_groups/internal/android/java/src/org/chromium/components/tab_group_sync/VersioningMessageControllerImpl.java
new file mode 100644
index 0000000..bafaf89
--- /dev/null
+++ b/components/saved_tab_groups/internal/android/java/src/org/chromium/components/tab_group_sync/VersioningMessageControllerImpl.java
@@ -0,0 +1,74 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.tab_group_sync;
+
+import org.jni_zero.CalledByNative;
+import org.jni_zero.JNINamespace;
+import org.jni_zero.NativeMethods;
+
+import org.chromium.base.Callback;
+import org.chromium.build.annotations.NullMarked;
+
+@JNINamespace("tab_groups")
+@NullMarked
+public class VersioningMessageControllerImpl implements VersioningMessageController {
+    private long mNativePtr;
+
+    @CalledByNative
+    private static VersioningMessageControllerImpl create(long nativePtr) {
+        return new VersioningMessageControllerImpl(nativePtr);
+    }
+
+    private VersioningMessageControllerImpl(long nativePtr) {
+        mNativePtr = nativePtr;
+    }
+
+    @Override
+    public void shouldShowMessageUiAsync(@MessageType int messageType, Callback<Boolean> callback) {
+        if (mNativePtr == 0) {
+            callback.onResult(false);
+            return;
+        }
+        VersioningMessageControllerImplJni.get()
+                .shouldShowMessageUiAsync(mNativePtr, this, messageType, callback);
+    }
+
+    @Override
+    public void onMessageUiShown(@MessageType int messageType) {
+        if (mNativePtr == 0) return;
+        VersioningMessageControllerImplJni.get().onMessageUiShown(mNativePtr, this, messageType);
+    }
+
+    @Override
+    public void onMessageUiDismissed(@MessageType int messageType) {
+        if (mNativePtr == 0) return;
+        VersioningMessageControllerImplJni.get()
+                .onMessageUiDismissed(mNativePtr, this, messageType);
+    }
+
+    @CalledByNative
+    private void clearNativePtr() {
+        mNativePtr = 0;
+    }
+
+    @NativeMethods
+    interface Natives {
+        void shouldShowMessageUiAsync(
+                long nativeVersioningMessageControllerAndroid,
+                VersioningMessageControllerImpl caller,
+                int messageType,
+                Callback<Boolean> callback);
+
+        void onMessageUiShown(
+                long nativeVersioningMessageControllerAndroid,
+                VersioningMessageControllerImpl caller,
+                int messageType);
+
+        void onMessageUiDismissed(
+                long nativeVersioningMessageControllerAndroid,
+                VersioningMessageControllerImpl caller,
+                int messageType);
+    }
+}
diff --git a/components/saved_tab_groups/internal/android/tab_group_sync_service_android.cc b/components/saved_tab_groups/internal/android/tab_group_sync_service_android.cc
index a13c26ef..137e1bf 100644
--- a/components/saved_tab_groups/internal/android/tab_group_sync_service_android.cc
+++ b/components/saved_tab_groups/internal/android/tab_group_sync_service_android.cc
@@ -12,6 +12,7 @@
 #include "base/android/scoped_java_ref.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/scoped_refptr.h"
+#include "components/saved_tab_groups/internal/android/versioning_message_controller_android.h"
 #include "components/saved_tab_groups/public/android/tab_group_sync_conversions_bridge.h"
 #include "components/saved_tab_groups/public/android/tab_group_sync_conversions_utils.h"
 #include "components/saved_tab_groups/public/collaboration_finder.h"
@@ -59,6 +60,9 @@
     : tab_group_sync_service_(tab_group_sync_service) {
   DCHECK(tab_group_sync_service_);
   JNIEnv* env = base::android::AttachCurrentThread();
+  versioning_messaging_controller_android_ =
+      std::make_unique<VersioningMessageControllerAndroid>(
+          tab_group_sync_service_->GetVersioningMessageController());
   java_obj_.Reset(env, Java_TabGroupSyncServiceImpl_create(
                            env, reinterpret_cast<int64_t>(this))
                            .obj());
@@ -443,6 +447,13 @@
                                                 j_archival_status);
 }
 
+ScopedJavaLocalRef<jobject>
+TabGroupSyncServiceAndroid::GetVersioningMessageController(
+    JNIEnv* env,
+    const JavaParamRef<jobject>& j_caller) {
+  return versioning_messaging_controller_android_->GetJavaObject(env);
+}
+
 void TabGroupSyncServiceAndroid::SetCollaborationAvailableInFinderForTesting(
     JNIEnv* env,
     const JavaParamRef<jobject>& j_caller,
diff --git a/components/saved_tab_groups/internal/android/tab_group_sync_service_android.h b/components/saved_tab_groups/internal/android/tab_group_sync_service_android.h
index e4be94b7..eecc47c1 100644
--- a/components/saved_tab_groups/internal/android/tab_group_sync_service_android.h
+++ b/components/saved_tab_groups/internal/android/tab_group_sync_service_android.h
@@ -9,6 +9,7 @@
 #include "base/android/scoped_java_ref.h"
 #include "base/memory/raw_ptr.h"
 #include "base/supports_user_data.h"
+#include "components/saved_tab_groups/internal/android/versioning_message_controller_android.h"
 #include "components/saved_tab_groups/public/tab_group_sync_service.h"
 
 using base::android::JavaParamRef;
@@ -167,6 +168,10 @@
                             const JavaParamRef<jstring>& j_sync_group_id,
                             const jboolean j_archival_status);
 
+  ScopedJavaLocalRef<jobject> GetVersioningMessageController(
+      JNIEnv* env,
+      const JavaParamRef<jobject>& j_caller);
+
   void SetCollaborationAvailableInFinderForTesting(
       JNIEnv* env,
       const JavaParamRef<jobject>& j_caller,
@@ -177,6 +182,10 @@
   // TabGroupSyncServiceImpl.java.
   ScopedJavaGlobalRef<jobject> java_obj_;
 
+  // Owns the JNI bridge for VersioningMessageController.
+  std::unique_ptr<VersioningMessageControllerAndroid>
+      versioning_messaging_controller_android_;
+
   // Not owned. This is safe because the JNI bridge is destroyed and the
   // native pointer in Java is cleared whenever TabGroupSyncService is
   // destroyed. Hence there will be no subsequent unsafe calls to native.
diff --git a/components/saved_tab_groups/internal/android/tab_group_sync_service_android_unittest.cc b/components/saved_tab_groups/internal/android/tab_group_sync_service_android_unittest.cc
index 6f442a2..6d7790d 100644
--- a/components/saved_tab_groups/internal/android/tab_group_sync_service_android_unittest.cc
+++ b/components/saved_tab_groups/internal/android/tab_group_sync_service_android_unittest.cc
@@ -13,6 +13,7 @@
 #include "base/test/task_environment.h"
 #include "components/saved_tab_groups/public/android/tab_group_sync_conversions_bridge.h"
 #include "components/saved_tab_groups/public/android/tab_group_sync_conversions_utils.h"
+#include "components/saved_tab_groups/public/versioning_message_controller.h"
 #include "components/saved_tab_groups/test_support/mock_tab_group_sync_service.h"
 #include "components/saved_tab_groups/test_support/saved_tab_group_test_utils.h"
 #include "components/sync/test/test_matchers.h"
@@ -51,6 +52,19 @@
          static_cast<int>(arg.position()) == position;
 }
 
+class MockVersioningMessageController : public VersioningMessageController {
+ public:
+  MockVersioningMessageController() = default;
+  ~MockVersioningMessageController() override = default;
+
+  MOCK_METHOD(void,
+              ShouldShowMessageUiAsync,
+              (MessageType, base::OnceCallback<void(bool)>),
+              (override));
+  MOCK_METHOD(void, OnMessageUiShown, (MessageType), (override));
+  MOCK_METHOD(void, OnMessageUiDismissed, (MessageType), (override));
+};
+
 }  // namespace
 
 class TabGroupSyncServiceAndroidTest : public testing::Test {
@@ -66,6 +80,8 @@
   }
 
   void CreateBridge() {
+    EXPECT_CALL(tab_group_sync_service_, GetVersioningMessageController())
+        .WillOnce(Return(&versioning_message_controller_));
     EXPECT_CALL(tab_group_sync_service_, AddObserver(_));
     bridge_ =
         std::make_unique<TabGroupSyncServiceAndroid>(&tab_group_sync_service_);
@@ -87,6 +103,7 @@
   base::android::ScopedJavaLocalRef<jobject> j_service_;
   base::android::ScopedJavaGlobalRef<jobject> j_test_;
   LocalTabGroupID test_tab_group_id_ = base::Token(4, 5);
+  MockVersioningMessageController versioning_message_controller_;
 };
 
 TEST_F(TabGroupSyncServiceAndroidTest, OnInitialized) {
@@ -433,4 +450,38 @@
       env, j_test_, j_uuid, true);
 }
 
+TEST_F(TabGroupSyncServiceAndroidTest, ShouldShowMessageUiAsync) {
+  auto* env = AttachCurrentThread();
+  EXPECT_CALL(
+      versioning_message_controller_,
+      ShouldShowMessageUiAsync(VersioningMessageController::MessageType::
+                                   VERSION_OUT_OF_DATE_INSTANT_MESSAGE,
+                               _))
+      .WillOnce(
+          testing::WithArg<1>([](base::OnceCallback<void(bool)> callback) {
+            std::move(callback).Run(true);
+          }));
+  Java_TabGroupSyncServiceAndroidUnitTest_testShouldShowMessageUiAsync(env,
+                                                                       j_test_);
+}
+
+TEST_F(TabGroupSyncServiceAndroidTest, OnMessageUiShown) {
+  auto* env = AttachCurrentThread();
+  EXPECT_CALL(versioning_message_controller_,
+              OnMessageUiShown(VersioningMessageController::MessageType::
+                                   VERSION_OUT_OF_DATE_INSTANT_MESSAGE))
+      .Times(1);
+  Java_TabGroupSyncServiceAndroidUnitTest_testOnMessageUiShown(env, j_test_);
+}
+
+TEST_F(TabGroupSyncServiceAndroidTest, OnMessageUiDismissed) {
+  auto* env = AttachCurrentThread();
+  EXPECT_CALL(versioning_message_controller_,
+              OnMessageUiDismissed(VersioningMessageController::MessageType::
+                                       VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE))
+      .Times(1);
+  Java_TabGroupSyncServiceAndroidUnitTest_testOnMessageUiDismissed(env,
+                                                                   j_test_);
+}
+
 }  // namespace tab_groups
diff --git a/components/saved_tab_groups/internal/android/versioning_message_controller_android.cc b/components/saved_tab_groups/internal/android/versioning_message_controller_android.cc
new file mode 100644
index 0000000..8e9f38f
--- /dev/null
+++ b/components/saved_tab_groups/internal/android/versioning_message_controller_android.cc
@@ -0,0 +1,67 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/saved_tab_groups/internal/android/versioning_message_controller_android.h"
+
+#include "base/android/callback_android.h"
+#include "base/android/jni_android.h"
+#include "base/android/jni_string.h"
+#include "components/saved_tab_groups/internal/jni_headers/VersioningMessageControllerImpl_jni.h"
+
+using base::android::JavaParamRef;
+
+namespace tab_groups {
+using MessageType = VersioningMessageController::MessageType;
+
+VersioningMessageControllerAndroid::VersioningMessageControllerAndroid(
+    VersioningMessageController* versioning_message_controller)
+    : versioning_message_controller_(versioning_message_controller) {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  java_obj_.Reset(env, Java_VersioningMessageControllerImpl_create(
+                           env, reinterpret_cast<int64_t>(this))
+                           .obj());
+}
+
+VersioningMessageControllerAndroid::~VersioningMessageControllerAndroid() {
+  Java_VersioningMessageControllerImpl_clearNativePtr(
+      base::android::AttachCurrentThread(), java_obj_);
+}
+
+base::android::ScopedJavaLocalRef<jobject>
+VersioningMessageControllerAndroid::GetJavaObject(JNIEnv* env) {
+  return base::android::ScopedJavaLocalRef<jobject>(java_obj_);
+}
+
+void VersioningMessageControllerAndroid::ShouldShowMessageUiAsync(
+    JNIEnv* env,
+    const JavaParamRef<jobject>& j_caller,
+    jint j_message_type,
+    const JavaParamRef<jobject>& j_callback) {
+  MessageType message_type = static_cast<MessageType>(j_message_type);
+  base::OnceCallback<void(bool)> callback = base::BindOnce(
+      [](const base::android::JavaRef<jobject>& j_callback, bool result) {
+        base::android::RunBooleanCallbackAndroid(j_callback, result);
+      },
+      base::android::ScopedJavaGlobalRef<jobject>(j_callback));
+  versioning_message_controller_->ShouldShowMessageUiAsync(message_type,
+                                                           std::move(callback));
+}
+
+void VersioningMessageControllerAndroid::OnMessageUiShown(
+    JNIEnv* env,
+    const JavaParamRef<jobject>& j_caller,
+    jint j_message_type) {
+  MessageType message_type = static_cast<MessageType>(j_message_type);
+  versioning_message_controller_->OnMessageUiShown(message_type);
+}
+
+void VersioningMessageControllerAndroid::OnMessageUiDismissed(
+    JNIEnv* env,
+    const JavaParamRef<jobject>& j_caller,
+    jint j_message_type) {
+  MessageType message_type = static_cast<MessageType>(j_message_type);
+  versioning_message_controller_->OnMessageUiDismissed(message_type);
+}
+
+}  // namespace tab_groups
diff --git a/components/saved_tab_groups/internal/android/versioning_message_controller_android.h b/components/saved_tab_groups/internal/android/versioning_message_controller_android.h
new file mode 100644
index 0000000..495557f
--- /dev/null
+++ b/components/saved_tab_groups/internal/android/versioning_message_controller_android.h
@@ -0,0 +1,51 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_SAVED_TAB_GROUPS_INTERNAL_ANDROID_VERSIONING_MESSAGE_CONTROLLER_ANDROID_H_
+#define COMPONENTS_SAVED_TAB_GROUPS_INTERNAL_ANDROID_VERSIONING_MESSAGE_CONTROLLER_ANDROID_H_
+
+#include "base/android/jni_android.h"
+#include "base/android/scoped_java_ref.h"
+#include "base/memory/raw_ptr.h"
+#include "components/saved_tab_groups/public/versioning_message_controller.h"
+
+using base::android::JavaParamRef;
+
+namespace tab_groups {
+
+// Helper class responsible for bridging the VersioningMessageController between
+// C++ and Java.
+class VersioningMessageControllerAndroid {
+ public:
+  explicit VersioningMessageControllerAndroid(
+      VersioningMessageController* versioning_message_controller);
+  ~VersioningMessageControllerAndroid();
+
+  base::android::ScopedJavaLocalRef<jobject> GetJavaObject(JNIEnv* env);
+
+  void ShouldShowMessageUiAsync(JNIEnv* env,
+                                const JavaParamRef<jobject>& j_caller,
+                                jint j_message_type,
+                                const JavaParamRef<jobject>& j_callback);
+  void OnMessageUiShown(JNIEnv* env,
+                        const JavaParamRef<jobject>& j_caller,
+                        jint j_message_type);
+  void OnMessageUiDismissed(JNIEnv* env,
+                            const JavaParamRef<jobject>& j_caller,
+                            jint j_message_type);
+
+ private:
+  // A reference to the Java counterpart of this class.  See
+  // VersioningMessageControllerImpl.java.
+  base::android::ScopedJavaGlobalRef<jobject> java_obj_;
+
+  // Not owned. This is safe because the JNI bridge is destroyed and the
+  // native pointer in Java is cleared whenever native is destroyed. Hence there
+  // will be no subsequent unsafe calls to native.
+  raw_ptr<VersioningMessageController> versioning_message_controller_;
+};
+
+}  // namespace tab_groups
+
+#endif  // COMPONENTS_SAVED_TAB_GROUPS_INTERNAL_ANDROID_VERSIONING_MESSAGE_CONTROLLER_ANDROID_H_
diff --git a/components/saved_tab_groups/internal/tab_group_sync_service_impl.cc b/components/saved_tab_groups/internal/tab_group_sync_service_impl.cc
index 428f16c..0423827f 100644
--- a/components/saved_tab_groups/internal/tab_group_sync_service_impl.cc
+++ b/components/saved_tab_groups/internal/tab_group_sync_service_impl.cc
@@ -31,6 +31,7 @@
 #include "components/saved_tab_groups/internal/sync_data_type_configuration.h"
 #include "components/saved_tab_groups/internal/tab_group_sync_bridge_mediator.h"
 #include "components/saved_tab_groups/internal/tab_group_sync_coordinator_impl.h"
+#include "components/saved_tab_groups/internal/versioning_message_controller_impl.h"
 #include "components/saved_tab_groups/public/collaboration_finder.h"
 #include "components/saved_tab_groups/public/features.h"
 #include "components/saved_tab_groups/public/pref_names.h"
@@ -191,7 +192,10 @@
       collaboration_finder_(std::move(collaboration_finder)),
       logger_(logger),
       pref_service_(pref_service),
-      opt_guide_(optimization_guide_decider) {
+      opt_guide_(optimization_guide_decider),
+      versioning_message_controller_(
+          std::make_unique<VersioningMessageControllerImpl>(pref_service_,
+                                                            this)) {
   if (shared_tab_group_account_configuration) {
     shared_tab_group_account_data_bridge_ =
         std::make_unique<SharedTabGroupAccountDataSyncBridge>(
@@ -259,6 +263,12 @@
                                 : had_shared_tab_groups_on_startup_;
 }
 
+VersioningMessageController*
+TabGroupSyncServiceImpl::GetVersioningMessageController() {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  return versioning_message_controller_.get();
+}
+
 void TabGroupSyncServiceImpl::AddObserver(
     TabGroupSyncService::Observer* observer) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
diff --git a/components/saved_tab_groups/internal/tab_group_sync_service_impl.h b/components/saved_tab_groups/internal/tab_group_sync_service_impl.h
index f1fdcfcf..d1e6ce2 100644
--- a/components/saved_tab_groups/internal/tab_group_sync_service_impl.h
+++ b/components/saved_tab_groups/internal/tab_group_sync_service_impl.h
@@ -33,6 +33,7 @@
 #include "components/saved_tab_groups/public/tab_group_sync_metrics_logger.h"
 #include "components/saved_tab_groups/public/tab_group_sync_service.h"
 #include "components/saved_tab_groups/public/types.h"
+#include "components/saved_tab_groups/public/versioning_message_controller.h"
 #include "components/signin/public/identity_manager/identity_manager.h"
 #include "components/sync/base/collaboration_id.h"
 
@@ -187,6 +188,7 @@
   std::unique_ptr<std::vector<SavedTabGroup>>
   TakeSharedTabGroupsAvailableAtStartupForMessaging() override;
   bool HadSharedTabGroupsLastSession(bool open_shared_tab_groups) override;
+  VersioningMessageController* GetVersioningMessageController() override;
   void OnLastTabClosed(const SavedTabGroup& saved_tab_group) override;
 
   void AddObserver(TabGroupSyncService::Observer* observer) override;
@@ -464,6 +466,10 @@
   std::map<base::Uuid, TabGroupSharingTimeoutInfo>
       tab_group_sharing_timeout_info_;
 
+  // The versioning message controller which is responsible for business logic
+  // related to shared tab groups versioning related messages.
+  std::unique_ptr<VersioningMessageController> versioning_message_controller_;
+
   base::ScopedObservation<signin::IdentityManager,
                           signin::IdentityManager::Observer>
       identity_manager_observation_{this};
diff --git a/components/saved_tab_groups/internal/tab_group_sync_service_unittest.cc b/components/saved_tab_groups/internal/tab_group_sync_service_unittest.cc
index deba3efa..5bd55468 100644
--- a/components/saved_tab_groups/internal/tab_group_sync_service_unittest.cc
+++ b/components/saved_tab_groups/internal/tab_group_sync_service_unittest.cc
@@ -204,6 +204,13 @@
                                                      base::Value::Dict());
     pref_service_.registry()->RegisterDictionaryPref(
         prefs::kLocallyClosedRemoteTabGroupIds, base::Value::Dict());
+    pref_service_.registry()->RegisterBooleanPref(
+        prefs::kDataSharingHasShownVersionUpdatedMessage, false);
+    pref_service_.registry()->RegisterBooleanPref(
+        prefs::kDataSharingHasShownVersionOutOfDateInstantMessage, false);
+    pref_service_.registry()->RegisterBooleanPref(
+        prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage,
+        false);
 
     auto metrics_logger =
         std::make_unique<TabGroupSyncMetricsLoggerImpl>(&device_info_tracker_);
@@ -406,6 +413,7 @@
 
 TEST_F(TabGroupSyncServiceTest, ServiceConstruction) {
   EXPECT_TRUE(tab_group_sync_service_->GetSavedTabGroupControllerDelegate());
+  EXPECT_TRUE(tab_group_sync_service_->GetVersioningMessageController());
 }
 
 TEST_F(TabGroupSyncServiceTest, GetAllGroups) {
diff --git a/components/saved_tab_groups/internal/versioning_message_controller_impl.cc b/components/saved_tab_groups/internal/versioning_message_controller_impl.cc
new file mode 100644
index 0000000..0d51344
--- /dev/null
+++ b/components/saved_tab_groups/internal/versioning_message_controller_impl.cc
@@ -0,0 +1,149 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/saved_tab_groups/internal/versioning_message_controller_impl.h"
+
+#include "base/functional/bind.h"
+#include "components/data_sharing/public/features.h"
+#include "components/prefs/pref_service.h"
+#include "components/saved_tab_groups/public/pref_names.h"
+#include "components/saved_tab_groups/public/tab_group_sync_service.h"
+
+namespace tab_groups {
+namespace {
+bool IsVersionOutOfDate() {
+  return base::FeatureList::IsEnabled(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+}
+
+bool MeetsEligibilityCriteria(
+    PrefService* pref_service,
+    VersioningMessageController::MessageType message_type) {
+  switch (message_type) {
+    case VersioningMessageController::MessageType::
+        VERSION_OUT_OF_DATE_INSTANT_MESSAGE:
+      return !pref_service->GetBoolean(
+          prefs::kDataSharingHasShownVersionOutOfDateInstantMessage);
+    case VersioningMessageController::MessageType::
+        VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE:
+      return !pref_service->GetBoolean(
+          prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage);
+    case VersioningMessageController::MessageType::VERSION_UPDATED_MESSAGE:
+      return !pref_service->GetBoolean(
+          prefs::kDataSharingHasShownVersionUpdatedMessage);
+    default:
+      return false;
+  }
+}
+
+}  // namespace
+
+VersioningMessageControllerImpl::VersioningMessageControllerImpl(
+    PrefService* pref_service,
+    TabGroupSyncService* tab_group_sync_service)
+    : pref_service_(pref_service),
+      tab_group_sync_service_(tab_group_sync_service) {
+  ResetMessagePrefsOnStartup();
+  tab_group_sync_service_->AddObserver(this);
+}
+
+VersioningMessageControllerImpl::~VersioningMessageControllerImpl() {
+  tab_group_sync_service_->RemoveObserver(this);
+}
+
+void VersioningMessageControllerImpl::ResetMessagePrefsOnStartup() {
+  // On startup, reset the message prefs based on the state of the
+  // feature flag.
+  if (IsVersionOutOfDate()) {
+    // If versioning is disabled, reset the pref used for showing the re-enabled
+    // message shown in the enabled state.
+    pref_service_->SetBoolean(prefs::kDataSharingHasShownVersionUpdatedMessage,
+                              false);
+  } else {
+    // If versioning is enabled, reset the prefs used for showing the instant
+    // and persistent messages shown in the disabled state.
+    pref_service_->SetBoolean(
+        prefs::kDataSharingHasShownVersionOutOfDateInstantMessage, false);
+    pref_service_->SetBoolean(
+        prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage,
+        false);
+  }
+}
+
+void VersioningMessageControllerImpl::ShouldShowMessageUiAsync(
+    MessageType message_type,
+    base::OnceCallback<void(bool)> callback) {
+  if (!is_initialized_) {
+    pending_callbacks_.push_back(base::BindOnce(
+        &VersioningMessageControllerImpl::ShouldShowMessageUiAsync,
+        weak_ptr_factory_.GetWeakPtr(), message_type, std::move(callback)));
+
+    return;
+  }
+
+  std::move(callback).Run(ShouldShowMessageUi(message_type));
+}
+
+bool VersioningMessageControllerImpl::ShouldShowMessageUi(
+    MessageType message_type) {
+  CHECK(is_initialized_);
+  bool is_version_out_of_date = IsVersionOutOfDate();
+  bool had_open_shared_tab_groups =
+      tab_group_sync_service_->HadSharedTabGroupsLastSession(
+          /*open_shared_tab_groups=*/true);
+  bool had_shared_tab_groups =
+      tab_group_sync_service_->HadSharedTabGroupsLastSession(
+          /*open_shared_tab_groups=*/false);
+  // Determines if the message can be shown, considering past displays or
+  // dismissals.
+  bool meets_eligibility_criteria =
+      MeetsEligibilityCriteria(pref_service_, message_type);
+
+  switch (message_type) {
+    case MessageType::VERSION_OUT_OF_DATE_INSTANT_MESSAGE:
+      return is_version_out_of_date && had_open_shared_tab_groups &&
+             meets_eligibility_criteria;
+    case MessageType::VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE:
+      return is_version_out_of_date && had_shared_tab_groups &&
+             meets_eligibility_criteria;
+    case MessageType::VERSION_UPDATED_MESSAGE:
+      return !is_version_out_of_date && meets_eligibility_criteria;
+    default:
+      return false;
+  }
+}
+
+void VersioningMessageControllerImpl::OnMessageUiShown(
+    MessageType message_type) {
+  switch (message_type) {
+    case MessageType::VERSION_OUT_OF_DATE_INSTANT_MESSAGE:
+      pref_service_->SetBoolean(
+          prefs::kDataSharingHasShownVersionOutOfDateInstantMessage, true);
+      break;
+    case MessageType::VERSION_UPDATED_MESSAGE:
+      pref_service_->SetBoolean(
+          prefs::kDataSharingHasShownVersionUpdatedMessage, true);
+      break;
+    default:
+      break;
+  }
+}
+
+void VersioningMessageControllerImpl::OnMessageUiDismissed(
+    MessageType message_type) {
+  if (message_type == MessageType::VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE) {
+    pref_service_->SetBoolean(
+        prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage, true);
+  }
+}
+
+void VersioningMessageControllerImpl::OnInitialized() {
+  is_initialized_ = true;
+  for (auto& callback : pending_callbacks_) {
+    std::move(callback).Run();
+  }
+  pending_callbacks_.clear();
+}
+
+}  // namespace tab_groups
diff --git a/components/saved_tab_groups/internal/versioning_message_controller_impl.h b/components/saved_tab_groups/internal/versioning_message_controller_impl.h
new file mode 100644
index 0000000..dd9137f
--- /dev/null
+++ b/components/saved_tab_groups/internal/versioning_message_controller_impl.h
@@ -0,0 +1,56 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_SAVED_TAB_GROUPS_INTERNAL_VERSIONING_MESSAGE_CONTROLLER_IMPL_H_
+#define COMPONENTS_SAVED_TAB_GROUPS_INTERNAL_VERSIONING_MESSAGE_CONTROLLER_IMPL_H_
+
+#include "base/functional/callback.h"
+#include "base/functional/callback_forward.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/observer_list_types.h"
+#include "components/prefs/pref_service.h"
+#include "components/saved_tab_groups/public/tab_group_sync_service.h"
+#include "components/saved_tab_groups/public/versioning_message_controller.h"
+
+namespace tab_groups {
+
+class TabGroupSyncService;
+
+class VersioningMessageControllerImpl : public VersioningMessageController,
+                                        public TabGroupSyncService::Observer {
+ public:
+  VersioningMessageControllerImpl(PrefService* pref_service,
+                                  TabGroupSyncService* tab_group_sync_service);
+  ~VersioningMessageControllerImpl() override;
+
+  // VersioningMessageController implementation.
+  void ShouldShowMessageUiAsync(
+      MessageType message_type,
+      base::OnceCallback<void(bool)> callback) override;
+  void OnMessageUiShown(MessageType message_type) override;
+  void OnMessageUiDismissed(MessageType message_type) override;
+
+  // TabGroupSyncService::Observer implementation.
+  void OnInitialized() override;
+
+ private:
+  bool ShouldShowMessageUi(MessageType message_type);
+  void ResetMessagePrefsOnStartup();
+
+  raw_ptr<PrefService> pref_service_;
+  raw_ptr<TabGroupSyncService> tab_group_sync_service_;
+
+  // Callbacks that are waiting for initialization to complete.
+  std::vector<base::OnceClosure> pending_callbacks_;
+
+  // Whether the TabGroupSyncService has been initialized.
+  bool is_initialized_ = false;
+
+  base::WeakPtrFactory<VersioningMessageControllerImpl> weak_ptr_factory_{this};
+};
+
+}  // namespace tab_groups
+
+#endif  // COMPONENTS_SAVED_TAB_GROUPS_INTERNAL_VERSIONING_MESSAGE_CONTROLLER_IMPL_H_
diff --git a/components/saved_tab_groups/internal/versioning_message_controller_impl_unittest.cc b/components/saved_tab_groups/internal/versioning_message_controller_impl_unittest.cc
new file mode 100644
index 0000000..01934d4
--- /dev/null
+++ b/components/saved_tab_groups/internal/versioning_message_controller_impl_unittest.cc
@@ -0,0 +1,271 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/saved_tab_groups/internal/versioning_message_controller_impl.h"
+
+#include "base/functional/callback.h"
+#include "base/run_loop.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
+#include "components/data_sharing/public/features.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/testing_pref_service.h"
+#include "components/saved_tab_groups/public/pref_names.h"
+#include "components/saved_tab_groups/public/versioning_message_controller.h"
+#include "components/saved_tab_groups/test_support/mock_tab_group_sync_service.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace tab_groups {
+using MessageType = VersioningMessageController::MessageType;
+
+namespace {
+
+class VersioningMessageControllerImplTest : public testing::Test {
+ public:
+  VersioningMessageControllerImplTest() {
+    pref_service_.registry()->RegisterBooleanPref(
+        prefs::kDataSharingHasShownVersionOutOfDateInstantMessage, false);
+    pref_service_.registry()->RegisterBooleanPref(
+        prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage,
+        false);
+    pref_service_.registry()->RegisterBooleanPref(
+        prefs::kDataSharingHasShownVersionUpdatedMessage, false);
+
+    SetTabGroupSyncServiceExpectation(/*has_shared_tab_groups=*/false,
+                                      /*has_open_shared_tab_groups=*/false);
+    controller_ = std::make_unique<VersioningMessageControllerImpl>(
+        &pref_service_, &mock_tab_group_sync_service_);
+  }
+
+  void SetPref(const std::string& pref_name, bool value) {
+    pref_service_.SetBoolean(pref_name, value);
+  }
+
+  bool GetPref(const std::string& pref_name) {
+    return pref_service_.GetBoolean(pref_name);
+  }
+
+  void SetTabGroupSyncServiceExpectation(bool had_shared_tab_groups,
+                                         bool had_open_shared_tab_groups) {
+    EXPECT_CALL(mock_tab_group_sync_service_,
+                HadSharedTabGroupsLastSession(/*open_shared_tab_groups=*/false))
+        .WillRepeatedly(testing::Return(had_shared_tab_groups));
+    EXPECT_CALL(mock_tab_group_sync_service_,
+                HadSharedTabGroupsLastSession(/*open_shared_tab_groups=*/true))
+        .WillRepeatedly(testing::Return(had_open_shared_tab_groups));
+  }
+
+  void InitializeController() { controller_->OnInitialized(); }
+
+  void ExpectMessageUiShouldBeShown(MessageType message_type,
+                                    bool expected_value) {
+    base::RunLoop run_loop;
+    controller_->ShouldShowMessageUiAsync(
+        message_type, base::BindOnce(
+                          [](base::RunLoop* run_loop, bool expected_value,
+                             bool actual_value) {
+                            EXPECT_EQ(expected_value, actual_value);
+                            run_loop->Quit();
+                          },
+                          &run_loop, expected_value));
+    run_loop.Run();
+  }
+
+ protected:
+  base::test::TaskEnvironment task_environment_;
+  TestingPrefServiceSimple pref_service_;
+  MockTabGroupSyncService mock_tab_group_sync_service_;
+  std::unique_ptr<VersioningMessageControllerImpl> controller_;
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+TEST_F(VersioningMessageControllerImplTest,
+       TestPrefs_Startup_VersionOutOfDate) {
+  scoped_feature_list_.InitAndEnableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  SetPref(prefs::kDataSharingHasShownVersionOutOfDateInstantMessage, true);
+  SetPref(prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage,
+          true);
+  SetPref(prefs::kDataSharingHasShownVersionUpdatedMessage, true);
+  InitializeController();
+  controller_ = std::make_unique<VersioningMessageControllerImpl>(
+      &pref_service_, &mock_tab_group_sync_service_);
+
+  EXPECT_TRUE(
+      GetPref(prefs::kDataSharingHasShownVersionOutOfDateInstantMessage));
+  EXPECT_TRUE(GetPref(
+      prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage));
+  EXPECT_FALSE(GetPref(prefs::kDataSharingHasShownVersionUpdatedMessage));
+}
+
+TEST_F(VersioningMessageControllerImplTest, TestPrefs_Startup_VersionUpdated) {
+  scoped_feature_list_.InitAndDisableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  SetPref(prefs::kDataSharingHasShownVersionOutOfDateInstantMessage, true);
+  SetPref(prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage,
+          true);
+  SetPref(prefs::kDataSharingHasShownVersionUpdatedMessage, true);
+  InitializeController();
+  controller_ = std::make_unique<VersioningMessageControllerImpl>(
+      &pref_service_, &mock_tab_group_sync_service_);
+
+  EXPECT_FALSE(
+      GetPref(prefs::kDataSharingHasShownVersionOutOfDateInstantMessage));
+  EXPECT_FALSE(GetPref(
+      prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage));
+  EXPECT_TRUE(GetPref(prefs::kDataSharingHasShownVersionUpdatedMessage));
+}
+
+TEST_F(VersioningMessageControllerImplTest,
+       QueryIfMessageUiShouldBeShown_InstantMessage_VersionOutOfDate) {
+  scoped_feature_list_.InitAndEnableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  SetTabGroupSyncServiceExpectation(/*has_shared_tab_groups=*/true,
+                                    /*has_open_shared_tab_groups=*/true);
+  InitializeController();
+  ExpectMessageUiShouldBeShown(MessageType::VERSION_OUT_OF_DATE_INSTANT_MESSAGE,
+                               true);
+}
+
+TEST_F(
+    VersioningMessageControllerImplTest,
+    QueryIfMessageUiShouldBeShown_InstantMessage_VersionOutOfDate_NoSharedGroups) {
+  scoped_feature_list_.InitAndEnableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  SetTabGroupSyncServiceExpectation(/*has_shared_tab_groups=*/false,
+                                    /*has_open_shared_tab_groups=*/false);
+  InitializeController();
+  ExpectMessageUiShouldBeShown(MessageType::VERSION_OUT_OF_DATE_INSTANT_MESSAGE,
+                               false);
+}
+
+TEST_F(VersioningMessageControllerImplTest,
+       QueryIfMessageUiShouldBeShown_InstantMessage_VersionOutOfDate_PrefSet) {
+  scoped_feature_list_.InitAndEnableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  SetPref(prefs::kDataSharingHasShownVersionOutOfDateInstantMessage, true);
+  SetTabGroupSyncServiceExpectation(/*has_shared_tab_groups=*/true,
+                                    /*has_open_shared_tab_groups=*/true);
+  InitializeController();
+  ExpectMessageUiShouldBeShown(MessageType::VERSION_OUT_OF_DATE_INSTANT_MESSAGE,
+                               false);
+}
+
+TEST_F(VersioningMessageControllerImplTest,
+       QueryIfMessageUiShouldBeShown_InstantMessage_VersionUpdated) {
+  scoped_feature_list_.InitAndDisableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  InitializeController();
+  ExpectMessageUiShouldBeShown(MessageType::VERSION_OUT_OF_DATE_INSTANT_MESSAGE,
+                               false);
+}
+
+TEST_F(VersioningMessageControllerImplTest,
+       QueryIfMessageUiShouldBeShown_PersistentMessage_VersionOutOfDate) {
+  scoped_feature_list_.InitAndEnableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  SetTabGroupSyncServiceExpectation(/*has_shared_tab_groups=*/true,
+                                    /*has_open_shared_tab_groups=*/false);
+  InitializeController();
+  ExpectMessageUiShouldBeShown(
+      MessageType::VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE, true);
+}
+
+TEST_F(
+    VersioningMessageControllerImplTest,
+    QueryIfMessageUiShouldBeShown_PersistentMessage_VersionOutOfDate_NoSharedGroups) {
+  scoped_feature_list_.InitAndEnableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  SetTabGroupSyncServiceExpectation(/*has_shared_tab_groups=*/false,
+                                    /*has_open_shared_tab_groups=*/false);
+  InitializeController();
+  ExpectMessageUiShouldBeShown(
+      MessageType::VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE, false);
+}
+
+TEST_F(
+    VersioningMessageControllerImplTest,
+    QueryIfMessageUiShouldBeShown_PersistentMessage_VersionOutOfDate_PrefSet) {
+  scoped_feature_list_.InitAndEnableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  SetPref(prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage,
+          true);
+  SetTabGroupSyncServiceExpectation(/*has_shared_tab_groups=*/true,
+                                    /*has_open_shared_tab_groups=*/false);
+  InitializeController();
+  ExpectMessageUiShouldBeShown(
+      MessageType::VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE, false);
+}
+
+TEST_F(VersioningMessageControllerImplTest,
+       QueryIfMessageUiShouldBeShown_PersistentMessage_VersionUpdated) {
+  scoped_feature_list_.InitAndDisableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  InitializeController();
+  ExpectMessageUiShouldBeShown(
+      MessageType::VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE, false);
+}
+
+TEST_F(VersioningMessageControllerImplTest,
+       ExpectMessageUiShouldBeShown_ReenabledMessage_VersionOutOfDate) {
+  scoped_feature_list_.InitAndEnableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  InitializeController();
+  ExpectMessageUiShouldBeShown(MessageType::VERSION_UPDATED_MESSAGE, false);
+}
+
+TEST_F(VersioningMessageControllerImplTest,
+       QueryIfMessageUiShouldBeShown_ReenabledMessage_VersionUpdated) {
+  scoped_feature_list_.InitAndDisableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  InitializeController();
+  ExpectMessageUiShouldBeShown(MessageType::VERSION_UPDATED_MESSAGE, true);
+}
+
+TEST_F(VersioningMessageControllerImplTest,
+       QueryIfMessageUiShouldBeShown_ReenabledMessage_VersionUpdated_PrefSet) {
+  scoped_feature_list_.InitAndDisableFeature(
+      data_sharing::features::kDataSharingEnableUpdateChromeUI);
+  SetPref(prefs::kDataSharingHasShownVersionUpdatedMessage, true);
+  InitializeController();
+  ExpectMessageUiShouldBeShown(MessageType::VERSION_UPDATED_MESSAGE, false);
+}
+
+TEST_F(VersioningMessageControllerImplTest, OnMessageUiShown_InstantMessage) {
+  InitializeController();
+  controller_->OnMessageUiShown(
+      MessageType::VERSION_OUT_OF_DATE_INSTANT_MESSAGE);
+  EXPECT_TRUE(
+      GetPref(prefs::kDataSharingHasShownVersionOutOfDateInstantMessage));
+}
+
+TEST_F(VersioningMessageControllerImplTest, OnMessageUiShown_ReenabledMessage) {
+  controller_->OnMessageUiShown(MessageType::VERSION_UPDATED_MESSAGE);
+  EXPECT_TRUE(GetPref(prefs::kDataSharingHasShownVersionUpdatedMessage));
+}
+
+TEST_F(VersioningMessageControllerImplTest,
+       OnMessageUiDismissed_PersistentMessage) {
+  InitializeController();
+  controller_->OnMessageUiDismissed(
+      MessageType::VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE);
+  EXPECT_TRUE(GetPref(
+      prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage));
+}
+
+TEST_F(VersioningMessageControllerImplTest,
+       Initialization_QueuedCallbacksFlushed) {
+  bool callback_called = false;
+  controller_->ShouldShowMessageUiAsync(
+      MessageType::VERSION_OUT_OF_DATE_INSTANT_MESSAGE,
+      base::BindOnce(
+          [](bool* callback_called, bool result) { *callback_called = true; },
+          &callback_called));
+  EXPECT_FALSE(callback_called);
+  InitializeController();
+  EXPECT_TRUE(callback_called);
+}
+
+}  // namespace
+}  // namespace tab_groups
diff --git a/components/saved_tab_groups/public/BUILD.gn b/components/saved_tab_groups/public/BUILD.gn
index 46d507f..3b95b71 100644
--- a/components/saved_tab_groups/public/BUILD.gn
+++ b/components/saved_tab_groups/public/BUILD.gn
@@ -25,6 +25,7 @@
     "types.h",
     "utils.cc",
     "utils.h",
+    "versioning_message_controller.h",
   ]
   deps = [
     "//base",
@@ -164,6 +165,7 @@
       "android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncService.java",
       "android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncUtils.java",
       "android/java/src/org/chromium/components/tab_group_sync/TabGroupUiActionHandler.java",
+      "android/java/src/org/chromium/components/tab_group_sync/VersioningMessageController.java",
     ]
 
     deps = [
@@ -184,7 +186,10 @@
       ":*",
       "//components/saved_tab_groups:*",
     ]
-    sources = [ "types.h" ]
+    sources = [
+      "types.h",
+      "versioning_message_controller.h",
+    ]
   }
 
   android_library("test_support_java") {
diff --git a/components/saved_tab_groups/public/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncService.java b/components/saved_tab_groups/public/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncService.java
index 625c37f..f6c2f01 100644
--- a/components/saved_tab_groups/public/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncService.java
+++ b/components/saved_tab_groups/public/android/java/src/org/chromium/components/tab_group_sync/TabGroupSyncService.java
@@ -328,4 +328,10 @@
      * @param collaborationId Collaboration ID with which the collaboration group is associated.
      */
     void setCollaborationAvailableInFinderForTesting(String collaborationId);
+
+    /**
+     * @return The {@link VersioningMessageController} which is responsible for business logic
+     *     related to shared tab groups versioning related messages.
+     */
+    VersioningMessageController getVersioningMessageController();
 }
diff --git a/components/saved_tab_groups/public/android/java/src/org/chromium/components/tab_group_sync/VersioningMessageController.java b/components/saved_tab_groups/public/android/java/src/org/chromium/components/tab_group_sync/VersioningMessageController.java
new file mode 100644
index 0000000..63fa9dad
--- /dev/null
+++ b/components/saved_tab_groups/public/android/java/src/org/chromium/components/tab_group_sync/VersioningMessageController.java
@@ -0,0 +1,40 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.tab_group_sync;
+
+import org.chromium.base.Callback;
+import org.chromium.build.annotations.NullMarked;
+
+/**
+ * The central controller for versioning messages. Determines which versioning messages should be
+ * shown based on whether the current chrome client is able to support shared tab groups. The UI
+ * layer should query this class before showing any versioning messages.
+ */
+@NullMarked
+public interface VersioningMessageController {
+    /**
+     * Invoke this method to query if the given message UI should be shown. This will internally
+     * wait for TabGroupSyncService initialization.
+     *
+     * @param messageType The {@link MessageType} to query.
+     * @param callback The callback to be invoked with the result.
+     */
+    void shouldShowMessageUiAsync(@MessageType int messageType, Callback<Boolean> callback);
+
+    /**
+     * Invoke this after a MessageType.VERSION_OUT_OF_DATE_INSTANT_MESSAGE or
+     * MessageType.VERSION_UPDATED_MESSAGE has been displayed.
+     *
+     * @param messageType The {@link MessageType} that was shown.
+     */
+    void onMessageUiShown(@MessageType int messageType);
+
+    /**
+     * Invoke this after a user dismisses a MessageType.VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE.
+     *
+     * @param messageType The {@link MessageType} that was dismissed.
+     */
+    void onMessageUiDismissed(@MessageType int messageType);
+}
diff --git a/components/saved_tab_groups/public/pref_names.cc b/components/saved_tab_groups/public/pref_names.cc
index 18e4f17..a7e6cbf 100644
--- a/components/saved_tab_groups/public/pref_names.cc
+++ b/components/saved_tab_groups/public/pref_names.cc
@@ -45,6 +45,12 @@
   registry->RegisterBooleanPref(prefs::kDidSyncTabGroupsInLastSession, false);
   registry->RegisterBooleanPref(prefs::kDidEnableSharedTabGroupsInLastSession,
                                 false);
+  registry->RegisterBooleanPref(
+      prefs::kDataSharingHasShownVersionOutOfDateInstantMessage, false);
+  registry->RegisterBooleanPref(
+      prefs::kDataSharingHasDismissedVersionOutOfDatePersistentMessage, false);
+  registry->RegisterBooleanPref(
+      prefs::kDataSharingHasShownVersionUpdatedMessage, false);
 }
 
 void KeepAccountSettingsPrefsOnlyForUsers(
diff --git a/components/saved_tab_groups/public/pref_names.h b/components/saved_tab_groups/public/pref_names.h
index a1518302..013379a0 100644
--- a/components/saved_tab_groups/public/pref_names.h
+++ b/components/saved_tab_groups/public/pref_names.h
@@ -74,6 +74,21 @@
 inline constexpr char kDidEnableSharedTabGroupsInLastSession[] =
     "saved_tab_groups.did_enable_shared_tab_groups_in_last_session";
 
+// Prefs for Data Sharing (Versioning).
+// Stores whether the instant message prompting users to update chrome to
+// continue using shared tab groups has been shown.
+inline constexpr char kDataSharingHasShownVersionOutOfDateInstantMessage[] =
+    "data_sharing.has_shown_out_of_date_instant_message";
+// Stores whether the persistent message prompting users to update chrome to
+// continue using shared tab groups has been shown and dismissed by the user.
+inline constexpr char
+    kDataSharingHasDismissedVersionOutOfDatePersistentMessage[] =
+        "data_sharing.has_dismissed_out_of_date_persistent_message";
+// Stores whether the message that chrome has been updated to support shared tab
+// groups has been shown.
+inline constexpr char kDataSharingHasShownVersionUpdatedMessage[] =
+    "data_sharing.has_shown_version_updated_message";
+
 // Registers the Clear Browsing Data UI prefs.
 void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
 
diff --git a/components/saved_tab_groups/public/tab_group_sync_service.h b/components/saved_tab_groups/public/tab_group_sync_service.h
index 5486451..8fdf8c1 100644
--- a/components/saved_tab_groups/public/tab_group_sync_service.h
+++ b/components/saved_tab_groups/public/tab_group_sync_service.h
@@ -34,6 +34,7 @@
 class CollaborationFinder;
 class TabGroupSyncDelegate;
 class TabGroupSyncMetricsLogger;
+class VersioningMessageController;
 
 // A RAII class that pauses local tab model observers when required.
 class ScopedLocalObservationPauser {
@@ -398,6 +399,10 @@
   virtual void AddObserver(Observer* observer) = 0;
   virtual void RemoveObserver(Observer* observer) = 0;
 
+  // Returns the versioning message controller which is responsible for business
+  // logic related to shared tab groups versioning related messages.
+  virtual VersioningMessageController* GetVersioningMessageController() = 0;
+
   // For testing only. This is needed to test the API calls received before
   // service init as we need to explicitly un-initialize the service for these
   // scenarios. When calling this method the MessagingBackendService will need
diff --git a/components/saved_tab_groups/public/versioning_message_controller.h b/components/saved_tab_groups/public/versioning_message_controller.h
new file mode 100644
index 0000000..3b9a4d7
--- /dev/null
+++ b/components/saved_tab_groups/public/versioning_message_controller.h
@@ -0,0 +1,60 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_SAVED_TAB_GROUPS_PUBLIC_VERSIONING_MESSAGE_CONTROLLER_H_
+#define COMPONENTS_SAVED_TAB_GROUPS_PUBLIC_VERSIONING_MESSAGE_CONTROLLER_H_
+
+#include "base/functional/callback_forward.h"
+
+namespace tab_groups {
+
+// The central controller for versioning messages. Determines which versioning
+// messages should be shown based on whether the current chrome client is
+// able to support shared tab groups. The UI layer should query this class
+// before showing any versioning messages. Owned by TabGroupSyncService.
+// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.components.tab_group_sync
+class VersioningMessageController {
+ public:
+  // Enum representing the different types of messages that needs to be
+  // displayed to the user.
+  enum class MessageType {
+    // Instant message displayed to prompt the user to update chrome to be able
+    // to continue using their shared tab groups. Shown one time only on
+    // startup.
+    // Invoke OnMessageUiShown after displaying the message.
+    VERSION_OUT_OF_DATE_INSTANT_MESSAGE,
+    // Persistent message displayed to prompt the user to update chrome to be
+    // able to continue using their shared tab groups. Continues to be shown on
+    // every session until user explicitly dismisses it.
+    // Invoke OnMessageUiShown after displaying the message.
+    // Invoke OnMessageUiDismissed if the user dismisses the message.
+    VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE,
+    // IPH displayed to notify the user that shared tab groups are available
+    // again. Shown one time only.
+    // Invoke OnMessageUiShown after displaying the message.
+    VERSION_UPDATED_MESSAGE
+  };
+
+  virtual ~VersioningMessageController() = default;
+
+  // Invoke this method to query if the given message UI should be shown. This
+  // will internally wait for TabGroupSyncService initialization. See comments
+  // on MessageType for when the UI should inform this class about display /
+  // dismissed events.
+  virtual void ShouldShowMessageUiAsync(
+      MessageType message_type,
+      base::OnceCallback<void(bool)> callback) = 0;
+
+  // Invoke this after a MessageType::VERSION_OUT_OF_DATE_INSTANT_MESSAGE or
+  // MessageType::VERSION_UPDATED_MESSAGE has been displayed.
+  virtual void OnMessageUiShown(MessageType message_type) = 0;
+
+  // Invoke this after a user dismisses a
+  // MessageType::VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE.
+  virtual void OnMessageUiDismissed(MessageType message_type) = 0;
+};
+
+}  // namespace tab_groups
+
+#endif  // COMPONENTS_SAVED_TAB_GROUPS_PUBLIC_VERSIONING_MESSAGE_CONTROLLER_H_
diff --git a/components/saved_tab_groups/test_support/fake_tab_group_sync_service.cc b/components/saved_tab_groups/test_support/fake_tab_group_sync_service.cc
index 7fc6dc1d..946cc4c6 100644
--- a/components/saved_tab_groups/test_support/fake_tab_group_sync_service.cc
+++ b/components/saved_tab_groups/test_support/fake_tab_group_sync_service.cc
@@ -17,6 +17,7 @@
 #include "components/saved_tab_groups/public/saved_tab_group_tab.h"
 #include "components/saved_tab_groups/public/tab_group_sync_service.h"
 #include "components/saved_tab_groups/public/types.h"
+#include "components/saved_tab_groups/public/versioning_message_controller.h"
 #include "components/sync/base/collaboration_id.h"
 
 namespace tab_groups {
@@ -456,6 +457,11 @@
   return false;
 }
 
+VersioningMessageController*
+FakeTabGroupSyncService::GetVersioningMessageController() {
+  return nullptr;
+}
+
 void FakeTabGroupSyncService::OnLastTabClosed(
     const SavedTabGroup& saved_tab_group) {}
 
diff --git a/components/saved_tab_groups/test_support/fake_tab_group_sync_service.h b/components/saved_tab_groups/test_support/fake_tab_group_sync_service.h
index a094b506..eda117e 100644
--- a/components/saved_tab_groups/test_support/fake_tab_group_sync_service.h
+++ b/components/saved_tab_groups/test_support/fake_tab_group_sync_service.h
@@ -115,6 +115,7 @@
   std::unique_ptr<std::vector<SavedTabGroup>>
   TakeSharedTabGroupsAvailableAtStartupForMessaging() override;
   bool HadSharedTabGroupsLastSession(bool open_shared_tab_groups) override;
+  VersioningMessageController* GetVersioningMessageController() override;
   void OnLastTabClosed(const SavedTabGroup& saved_tab_group) override;
   void AddObserver(Observer* observer) override;
   void RemoveObserver(Observer* observer) override;
diff --git a/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h b/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h
index 8e5718de..706004fc 100644
--- a/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h
+++ b/components/saved_tab_groups/test_support/mock_tab_group_sync_service.h
@@ -8,6 +8,7 @@
 #include "components/saved_tab_groups/delegate/tab_group_sync_delegate.h"
 #include "components/saved_tab_groups/public/tab_group_sync_service.h"
 #include "components/saved_tab_groups/public/types.h"
+#include "components/saved_tab_groups/public/versioning_message_controller.h"
 #include "components/sync/model/data_type_sync_bridge.h"
 #include "testing/gmock/include/gmock/gmock.h"
 
@@ -143,6 +144,10 @@
               TakeSharedTabGroupsAvailableAtStartupForMessaging,
               ());
   MOCK_METHOD(bool, HadSharedTabGroupsLastSession, (bool), (override));
+  MOCK_METHOD(VersioningMessageController*,
+              GetVersioningMessageController,
+              (),
+              (override));
   MOCK_METHOD(void, OnLastTabClosed, (const SavedTabGroup&));
 
   MOCK_METHOD(void, AddObserver, (Observer*));
diff --git a/components/services/storage/dom_storage/async_dom_storage_database.cc b/components/services/storage/dom_storage/async_dom_storage_database.cc
index bb11ef8..99b3c64 100644
--- a/components/services/storage/dom_storage/async_dom_storage_database.cc
+++ b/components/services/storage/dom_storage/async_dom_storage_database.cc
@@ -4,24 +4,12 @@
 
 #include "components/services/storage/dom_storage/async_dom_storage_database.h"
 
-#include <inttypes.h>
-
-#include <algorithm>
-#include <map>
-#include <optional>
-#include <string>
-#include <utility>
-
 #include "base/debug/alias.h"
 #include "base/feature_list.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros.h"
-#include "base/rand_util.h"
-#include "base/strings/string_number_conversions.h"
 #include "base/strings/stringprintf.h"
 #include "base/task/sequenced_task_runner.h"
-#include "third_party/leveldatabase/env_chromium.h"
-#include "third_party/leveldatabase/src/include/leveldb/db.h"
 #include "third_party/leveldatabase/src/include/leveldb/write_batch.h"
 
 namespace storage {
diff --git a/components/services/storage/dom_storage/async_dom_storage_database.h b/components/services/storage/dom_storage/async_dom_storage_database.h
index 55b64d7..2fc14ca 100644
--- a/components/services/storage/dom_storage/async_dom_storage_database.h
+++ b/components/services/storage/dom_storage/async_dom_storage_database.h
@@ -8,6 +8,7 @@
 #include <memory>
 #include <optional>
 #include <set>
+#include <string>
 #include <tuple>
 #include <vector>
 
@@ -15,11 +16,8 @@
 #include "base/memory/scoped_refptr.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/threading/sequence_bound.h"
-#include "base/unguessable_token.h"
 #include "components/services/storage/dom_storage/dom_storage_database.h"
 #include "components/services/storage/dom_storage/features.h"
-#include "third_party/leveldatabase/src/include/leveldb/cache.h"
-#include "third_party/leveldatabase/src/include/leveldb/db.h"
 
 namespace storage {
 
diff --git a/components/viz/common/features.cc b/components/viz/common/features.cc
index e3341f2..acec916 100644
--- a/components/viz/common/features.cc
+++ b/components/viz/common/features.cc
@@ -267,11 +267,6 @@
              base::FEATURE_DISABLED_BY_DEFAULT);
 #endif
 
-// If enabled, complex occluders are generated for quads with rounded corners,
-BASE_FEATURE(kComplexOccluderForQuadsWithRoundedCorners,
-             "ComplexOccluderForQuadsWithRoundedCorners",
-             base::FEATURE_ENABLED_BY_DEFAULT);
-
 // Allow SurfaceAggregator to merge render passes when they contain quads that
 // require overlay (e.g. protected video). See usage in |EmitSurfaceContent|.
 BASE_FEATURE(kAllowForceMergeRenderPassWithRequireOverlayQuads,
@@ -539,12 +534,6 @@
   return base::FeatureList::IsEnabled(features::kOnBeginFrameThrottleVideo);
 }
 
-bool IsComplexOccluderForQuadsWithRoundedCornersEnabled() {
-  static bool enabled = base::FeatureList::IsEnabled(
-      features::kComplexOccluderForQuadsWithRoundedCorners);
-  return enabled;
-}
-
 bool ShouldDrawImmediatelyWhenInteractive() {
   return base::FeatureList::IsEnabled(
       features::kDrawImmediatelyWhenInteractive);
diff --git a/components/viz/common/features.h b/components/viz/common/features.h
index b5eec88..3f970ab 100644
--- a/components/viz/common/features.h
+++ b/components/viz/common/features.h
@@ -162,7 +162,6 @@
 VIZ_COMMON_EXPORT bool UseSurfaceLayerForVideo();
 VIZ_COMMON_EXPORT int MaxOverlaysConsidered();
 VIZ_COMMON_EXPORT bool ShouldOnBeginFrameThrottleVideo();
-VIZ_COMMON_EXPORT bool IsComplexOccluderForQuadsWithRoundedCornersEnabled();
 VIZ_COMMON_EXPORT bool ShouldDrawImmediatelyWhenInteractive();
 VIZ_COMMON_EXPORT bool IsVSyncAlignedPresentEnabled();
 VIZ_COMMON_EXPORT bool ShouldLogFrameQuadInfo();
diff --git a/components/viz/service/display/display_resource_provider_software_unittest.cc b/components/viz/service/display/display_resource_provider_software_unittest.cc
index 9ae61e64..794e64f9 100644
--- a/components/viz/service/display/display_resource_provider_software_unittest.cc
+++ b/components/viz/service/display/display_resource_provider_software_unittest.cc
@@ -98,10 +98,9 @@
 
     std::fill(span.begin(), span.end(), value);
 
-    auto transferable_resource = TransferableResource::MakeSoftwareSharedImage(
-        shared_image, shared_image_interface->GenVerifiedSyncToken(), size,
-        SinglePlaneFormat::kBGRA_8888,
-        TransferableResource::ResourceSource::kTileRasterTask);
+    auto transferable_resource = TransferableResource::Make(
+        shared_image, TransferableResource::ResourceSource::kTileRasterTask,
+        shared_image_interface->GenVerifiedSyncToken());
 
     auto callback = base::BindOnce(&MockReleaseCallback::Released,
                                    base::Unretained(&release_callback),
diff --git a/components/viz/service/display/occlusion_culler.cc b/components/viz/service/display/occlusion_culler.cc
index 9174b4f9..10d1bba2c 100644
--- a/components/viz/service/display/occlusion_culler.cc
+++ b/components/viz/service/display/occlusion_culler.cc
@@ -136,7 +136,6 @@
       lower_left == lower_right || lower_left.IsZero() || lower_right.IsZero();
 
   const bool should_generate_complex_occluder =
-      features::IsComplexOccluderForQuadsWithRoundedCornersEnabled() &&
       generate_complex_occluder_for_rounded_corners && uniform_top_corners &&
       uniform_bottom_corners &&
       bounds_f.size().GetArea() >= minumum_quad_size_with_rounded_corners;
diff --git a/components/viz/service/display/occlusion_culler_unittest.cc b/components/viz/service/display/occlusion_culler_unittest.cc
index ca47cf6..077346b 100644
--- a/components/viz/service/display/occlusion_culler_unittest.cc
+++ b/components/viz/service/display/occlusion_culler_unittest.cc
@@ -3371,10 +3371,6 @@
 };
 
 TEST_P(QuadsWithComplexOccluderTest, OcclusionCullingWithRoundedCorner) {
-  if (!features::IsComplexOccluderForQuadsWithRoundedCornersEnabled()) {
-    GTEST_SKIP();
-  }
-
   RendererSettings::OcclusionCullerSettings settings;
   settings.generate_complex_occluder_for_rounded_corners = true;
   settings.minimum_fragments_reduced = 0;
@@ -3507,10 +3503,6 @@
 // inscribed rectangle within the rounded rectangle, rather than a more complex
 // occluder.
 TEST_F(OcclusionCullerTest, OcclusionCullingWithRoundedCornerOnSmallQuads) {
-  if (!features::IsComplexOccluderForQuadsWithRoundedCornersEnabled()) {
-    GTEST_SKIP();
-  }
-
   RendererSettings::OcclusionCullerSettings settings;
   settings.generate_complex_occluder_for_rounded_corners = true;
   settings.quad_split_limit = 10;
diff --git a/components/viz/service/display/renderer_perftest.cc b/components/viz/service/display/renderer_perftest.cc
index 42f09c3..3393a02 100644
--- a/components/viz/service/display/renderer_perftest.cc
+++ b/components/viz/service/display/renderer_perftest.cc
@@ -165,10 +165,9 @@
       base::as_byte_span(base::allow_nonunique_obj, pixels));
   gpu::SyncToken sync_token = sii->GenVerifiedSyncToken();
 
-  TransferableResource gl_resource = TransferableResource::MakeGpu(
-      client_shared_image, GL_TEXTURE_2D, sync_token, size,
-      SinglePlaneFormat::kRGBA_8888, false /* is_overlay_candidate */);
-  gl_resource.color_space = gfx::ColorSpace();
+  TransferableResource gl_resource = TransferableResource::Make(
+      client_shared_image, TransferableResource::ResourceSource::kTest,
+      sync_token);
   auto release_callback =
       base::BindOnce(&DeleteSharedImage, std::move(child_context_provider),
                      std::move(client_shared_image));
diff --git a/components/viz/service/display/renderer_pixeltest.cc b/components/viz/service/display/renderer_pixeltest.cc
index e3988e1d..15758d49 100644
--- a/components/viz/service/display/renderer_pixeltest.cc
+++ b/components/viz/service/display/renderer_pixeltest.cc
@@ -100,22 +100,6 @@
                     bitmap.computeByteSize());
 }
 
-void AllocateAndRegisterSharedBitmapMemory(
-    scoped_refptr<RasterContextProvider> context_provider,
-    const gfx::Size& size,
-    scoped_refptr<gpu::ClientSharedImage>& shared_image,
-    gpu::SyncToken& sync_token) {
-  DCHECK(context_provider);
-  gpu::SharedImageInterface* shared_image_interface =
-      context_provider->SharedImageInterface();
-  shared_image = shared_image_interface->CreateSharedImageForSoftwareCompositor(
-      {SinglePlaneFormat::kBGRA_8888, size, gfx::ColorSpace(),
-       gpu::SHARED_IMAGE_USAGE_CPU_WRITE_ONLY, "PixelTestSharedBitmap"});
-
-  sync_token = shared_image_interface->GenVerifiedSyncToken();
-  CHECK(shared_image);
-}
-
 void DeleteSharedImage(scoped_refptr<gpu::ClientSharedImage> shared_image,
                        const gpu::SyncToken& sync_token,
                        bool is_lost) {
@@ -301,25 +285,28 @@
   const GrSurfaceOrigin origin = flipped_texture_quad
                                      ? kBottomLeft_GrSurfaceOrigin
                                      : kTopLeft_GrSurfaceOrigin;
+  const SkAlphaType alpha_type =
+      premultiplied_alpha ? kPremul_SkAlphaType : kUnpremul_SkAlphaType;
 
   ResourceId resource;
   if (gpu_resource) {
     resource = CreateGpuResource(
         child_context_provider, child_resource_provider, rect.size(),
-        SinglePlaneFormat::kBGRA_8888,
-        premultiplied_alpha ? kPremul_SkAlphaType : kUnpremul_SkAlphaType,
-        gfx::ColorSpace(), MakePixelSpan(pixels), origin);
+        SinglePlaneFormat::kBGRA_8888, alpha_type, gfx::ColorSpace(),
+        MakePixelSpan(pixels), origin);
   } else {
-    scoped_refptr<gpu::ClientSharedImage> shared_image;
-    gpu::SyncToken sync_token;
-    AllocateAndRegisterSharedBitmapMemory(child_context_provider, rect.size(),
-                                          shared_image, sync_token);
+    auto shared_image =
+        child_context_provider->SharedImageInterface()
+            ->CreateSharedImageForSoftwareCompositor(
+                {SinglePlaneFormat::kBGRA_8888, rect.size(), gfx::ColorSpace(),
+                 origin, alpha_type, gpu::SHARED_IMAGE_USAGE_CPU_WRITE_ONLY,
+                 "PixelTest"});
+
     auto mapping = shared_image->Map();
 
-    auto transferable_resource = TransferableResource::MakeSoftwareSharedImage(
-        shared_image, sync_token, rect.size(), SinglePlaneFormat::kBGRA_8888,
-        TransferableResource::ResourceSource::kTileRasterTask);
-    transferable_resource.origin = origin;
+    auto transferable_resource = TransferableResource::Make(
+        shared_image, TransferableResource::ResourceSource::kTileRasterTask,
+        shared_image->creation_sync_token());
     auto release_callback =
         base::BindOnce(&DeleteSharedImage, std::move(shared_image));
 
@@ -372,23 +359,26 @@
   size_t num_pixels = static_cast<size_t>(rect.width()) * rect.height();
   std::vector<uint32_t> pixels(num_pixels, pixel_color);
 
+  const SkAlphaType alpha_type =
+      premultiplied_alpha ? kPremul_SkAlphaType : kUnpremul_SkAlphaType;
   ResourceId resource;
   if (gpu_resource) {
-    resource = CreateGpuResource(
-        child_context_provider, child_resource_provider, rect.size(),
-        SinglePlaneFormat::kRGBA_8888,
-        premultiplied_alpha ? kPremul_SkAlphaType : kUnpremul_SkAlphaType,
-        gfx::ColorSpace(), MakePixelSpan(pixels));
+    resource =
+        CreateGpuResource(child_context_provider, child_resource_provider,
+                          rect.size(), SinglePlaneFormat::kRGBA_8888,
+                          alpha_type, gfx::ColorSpace(), MakePixelSpan(pixels));
   } else {
-    scoped_refptr<gpu::ClientSharedImage> shared_image;
-    gpu::SyncToken sync_token;
-    AllocateAndRegisterSharedBitmapMemory(child_context_provider, rect.size(),
-                                          shared_image, sync_token);
+    auto shared_image =
+        child_context_provider->SharedImageInterface()
+            ->CreateSharedImageForSoftwareCompositor(
+                {SinglePlaneFormat::kBGRA_8888, rect.size(), gfx::ColorSpace(),
+                 kTopLeft_GrSurfaceOrigin, alpha_type,
+                 gpu::SHARED_IMAGE_USAGE_CPU_WRITE_ONLY, "PixelTest"});
     auto mapping = shared_image->Map();
 
-    auto transferable_resource = TransferableResource::MakeSoftwareSharedImage(
-        shared_image, sync_token, rect.size(), SinglePlaneFormat::kBGRA_8888,
-        TransferableResource::ResourceSource::kTileRasterTask);
+    auto transferable_resource = TransferableResource::Make(
+        shared_image, TransferableResource::ResourceSource::kTileRasterTask,
+        shared_image->creation_sync_token());
     auto release_callback =
         base::BindOnce(&DeleteSharedImage, std::move(shared_image));
 
diff --git a/components/viz/service/display/software_renderer_unittest.cc b/components/viz/service/display/software_renderer_unittest.cc
index f2badb35..10962049 100644
--- a/components/viz/service/display/software_renderer_unittest.cc
+++ b/components/viz/service/display/software_renderer_unittest.cc
@@ -105,10 +105,9 @@
     source.readPixels(info, mapping->GetMemoryForPlane(0).data(),
                       info.minRowBytes(), 0, 0);
 
-    auto transferable_resource = TransferableResource::MakeSoftwareSharedImage(
-        shared_image, shared_image_interface->GenVerifiedSyncToken(), size,
-        SinglePlaneFormat::kBGRA_8888,
-        TransferableResource::ResourceSource::kTileRasterTask);
+    auto transferable_resource = TransferableResource::Make(
+        shared_image, TransferableResource::ResourceSource::kTileRasterTask,
+        shared_image->creation_sync_token());
     auto release_callback =
         base::BindOnce(&DeleteSharedImage, std::move(shared_image));
 
diff --git a/components/viz/service/display/surface_aggregator_perftest.cc b/components/viz/service/display/surface_aggregator_perftest.cc
index 4e5fa0f..44c7215 100644
--- a/components/viz/service/display/surface_aggregator_perftest.cc
+++ b/components/viz/service/display/surface_aggregator_perftest.cc
@@ -119,11 +119,9 @@
                 {SinglePlaneFormat::kBGRA_8888, size, gfx::ColorSpace(),
                  gpu::SHARED_IMAGE_USAGE_CPU_WRITE_ONLY,
                  "SurfaceAggregatorPerfTest"});
-        auto sync_token = shared_image_interface->GenVerifiedSyncToken();
-        TransferableResource resource =
-            TransferableResource::MakeSoftwareSharedImage(
-                shared_image, sync_token, size, SinglePlaneFormat::kBGRA_8888,
-                TransferableResource::ResourceSource::kTileRasterTask);
+        TransferableResource resource = TransferableResource::Make(
+            shared_image, TransferableResource::ResourceSource::kTileRasterTask,
+            shared_image->creation_sync_token());
 
         resource.id = ResourceId(j);
         frame_builder.AddTransferableResource(resource);
@@ -245,13 +243,11 @@
                     {SinglePlaneFormat::kBGRA_8888, quad->rect.size(),
                      gfx::ColorSpace(), gpu::SHARED_IMAGE_USAGE_CPU_WRITE_ONLY,
                      "SurfaceAggregatorPerfTest"});
-            auto sync_token = shared_image_interface->GenVerifiedSyncToken();
 
-            created_resources[resource_id] =
-                TransferableResource::MakeSoftwareSharedImage(
-                    shared_image, sync_token, quad->rect.size(),
-                    SinglePlaneFormat::kBGRA_8888,
-                    TransferableResource::ResourceSource::kTileRasterTask);
+            created_resources[resource_id] = TransferableResource::Make(
+                shared_image,
+                TransferableResource::ResourceSource::kTileRasterTask,
+                shared_image->creation_sync_token());
 
             created_resources[resource_id].id = resource_id;
           }
diff --git a/components/viz/service/display/surface_aggregator_unittest.cc b/components/viz/service/display/surface_aggregator_unittest.cc
index adb0401..ba5abd2 100644
--- a/components/viz/service/display/surface_aggregator_unittest.cc
+++ b/components/viz/service/display/surface_aggregator_unittest.cc
@@ -5858,11 +5858,9 @@
             {SinglePlaneFormat::kBGRA_8888, gfx::Size(1, 1), gfx::ColorSpace(),
              gpu::SHARED_IMAGE_USAGE_CPU_WRITE_ONLY,
              "SurfaceAggregatorWithResourcesTest"});
-    auto sync_token = shared_image_interface->GenVerifiedSyncToken();
-    auto resource = TransferableResource::MakeSoftwareSharedImage(
-        shared_image, sync_token, gfx::Size(1, 1),
-        SinglePlaneFormat::kBGRA_8888,
-        TransferableResource::ResourceSource::kTileRasterTask);
+    auto resource = TransferableResource::Make(
+        shared_image, TransferableResource::ResourceSource::kTileRasterTask,
+        shared_image->creation_sync_token());
 
     resource.id = resource_id;
     if (!valid) {
diff --git a/components/viz/service/input/render_input_router_support_child_frame.cc b/components/viz/service/input/render_input_router_support_child_frame.cc
index 0a047dc..28f1dc566 100644
--- a/components/viz/service/input/render_input_router_support_child_frame.cc
+++ b/components/viz/service/input/render_input_router_support_child_frame.cc
@@ -4,6 +4,7 @@
 
 #include "components/viz/service/input/render_input_router_support_child_frame.h"
 
+#include "base/trace_event/trace_event.h"
 #include "components/input/features.h"
 #include "components/input/render_widget_host_input_event_router.h"
 #include "components/viz/service/input/render_input_router_support_base.h"
diff --git a/components/viz/test/compositor_frame_helpers.cc b/components/viz/test/compositor_frame_helpers.cc
index 33de905..3c4b70a 100644
--- a/components/viz/test/compositor_frame_helpers.cc
+++ b/components/viz/test/compositor_frame_helpers.cc
@@ -601,16 +601,15 @@
       if (quad->resource_id != kInvalidResourceId) {
         // Adds a TransferableResource the first time seeing a ResourceId.
         if (resources_added.insert(quad->resource_id).second) {
-          auto shared_image = gpu::ClientSharedImage::CreateForTesting(
-              SinglePlaneFormat::kBGRA_8888, GL_TEXTURE_2D);
+          auto shared_image =
+              gpu::ClientSharedImage::CreateSoftwareForTesting();
           gpu::SyncToken sync_token;
           sync_token.Set(gpu::GPU_IO, gpu::CommandBufferId::FromUnsafeValue(1),
                          1);
-          frame.resource_list.push_back(
-              TransferableResource::MakeSoftwareSharedImage(
-                  shared_image, sync_token, quad->rect.size(),
-                  SinglePlaneFormat::kBGRA_8888,
-                  TransferableResource::ResourceSource::kTileRasterTask));
+          frame.resource_list.push_back(TransferableResource::Make(
+              shared_image,
+              TransferableResource::ResourceSource::kTileRasterTask,
+              sync_token));
           frame.resource_list.back().id = quad->resource_id;
         }
       }
diff --git a/content/app_shim_remote_cocoa/render_widget_host_view_cocoa.mm b/content/app_shim_remote_cocoa/render_widget_host_view_cocoa.mm
index 5bed786c..97e95c0 100644
--- a/content/app_shim_remote_cocoa/render_widget_host_view_cocoa.mm
+++ b/content/app_shim_remote_cocoa/render_widget_host_view_cocoa.mm
@@ -26,6 +26,7 @@
 #include "base/numerics/safe_conversions.h"
 #include "base/strings/string_util.h"
 #include "base/strings/sys_string_conversions.h"
+#include "base/trace_event/trace_event.h"
 #include "components/input/web_input_event_builders_mac.h"
 #include "components/remote_cocoa/app_shim/ns_view_ids.h"
 #import "content/browser/cocoa/system_hotkey_helper_mac.h"
diff --git a/content/browser/accessibility/ax_style_data.h b/content/browser/accessibility/ax_style_data.h
index 7b8ee80..db1be34 100644
--- a/content/browser/accessibility/ax_style_data.h
+++ b/content/browser/accessibility/ax_style_data.h
@@ -58,7 +58,7 @@
     if (!field) {
       field.emplace();
     }
-    (*field)[value].emplace_back(start, end);
+    (*field)[std::move(value)].emplace_back(start, end);
   }
 
   // Returns a string representation of the style data for testing.
diff --git a/content/browser/accessibility/browser_accessibility_android.cc b/content/browser/accessibility/browser_accessibility_android.cc
index 5b9e4e7..75e3c07 100644
--- a/content/browser/accessibility/browser_accessibility_android.cc
+++ b/content/browser/accessibility/browser_accessibility_android.cc
@@ -9,12 +9,14 @@
 #include "base/check_deref.h"
 #include "base/containers/contains.h"
 #include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
 #include "base/i18n/break_iterator.h"
 #include "base/memory/ptr_util.h"
 #include "base/no_destructor.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
+#include "content/browser/accessibility/ax_style_data.h"
 #include "content/browser/accessibility/browser_accessibility_manager_android.h"
 #include "content/public/common/content_client.h"
 #include "content/public/common/content_features.h"
@@ -23,6 +25,7 @@
 #include "third_party/skia/include/core/SkColor.h"
 #include "ui/accessibility/android/accessibility_state.h"
 #include "ui/accessibility/ax_assistant_structure.h"
+#include "ui/accessibility/ax_enums.mojom.h"
 #include "ui/accessibility/ax_node_position.h"
 #include "ui/accessibility/ax_role_properties.h"
 #include "ui/accessibility/ax_selection.h"
@@ -94,6 +97,8 @@
   ANDROID_VIEW_ACCESSIBILITY_CHECKED_STATE_PARTIAL = 2,
 };
 
+using AXStyleData = content::AXStyleData;
+
 using UniqueIdMap =
     absl::flat_hash_map<int32_t, content::BrowserAccessibilityAndroid*>;
 using LeafMap =
@@ -112,6 +117,76 @@
   return *leaf_map;
 }
 
+// Populates the computed style data from `node` into `style_data`.
+// Non-opaque colors also are blended from ancestors.
+void PopulateStyleData(const content::BrowserAccessibilityAndroid& node,
+                       const std::u16string& parent_text,
+                       const std::u16string& text,
+                       AXStyleData* style_data) {
+  if (!style_data) {
+    return;
+  }
+
+  int start = parent_text.size();
+  int end = start + text.size();
+
+  if (node.IsTextField()) {
+    std::vector<int> suggestion_starts;
+    std::vector<int> suggestion_ends;
+    node.GetSuggestions(&suggestion_starts, &suggestion_ends);
+    CHECK_EQ(suggestion_starts.size(), suggestion_ends.size());
+    for (size_t i = 0; i < suggestion_starts.size(); ++i) {
+      // Currently we don't retrieve the text of each suggestion, so store a
+      // blank string for now.
+      AXStyleData::AddRange(style_data->suggestions, std::u16string(),
+                            start + suggestion_starts[i],
+                            start + suggestion_ends[i]);
+    }
+  }
+
+  if (ui::IsLink(node.GetRole())) {
+    AXStyleData::AddRange(style_data->links, node.GetTargetUrl(), start, end);
+  }
+
+  if (node.GetRole() == ax::mojom::Role::kStaticText) {
+    if (float size = node.GetTextSize(); size != 0) {
+      AXStyleData::AddRange(style_data->text_sizes, size, start, end);
+    }
+    if (node.GetTextStyle() != 0) {
+      // GetTextStyle returns a bit field shifted by ax::mojom::TextStyle enum
+      // values, so we need to parse out the individual enum values. See:
+      // https://source.chromium.org/chromium/chromium/src/+/main:ui/accessibility/ax_node_data.cc?q=HasTextStyle
+      for (int i = static_cast<int>(ax::mojom::TextStyle::kMinValue);
+           i <= static_cast<int>(ax::mojom::TextStyle::kMaxValue); ++i) {
+        ax::mojom::TextStyle style = static_cast<ax::mojom::TextStyle>(i);
+        if (style != ax::mojom::TextStyle::kNone && node.HasTextStyle(style)) {
+          AXStyleData::AddRange(style_data->text_styles, style, start, end);
+        }
+      }
+    }
+    if (auto pos = static_cast<ax::mojom::TextPosition>(node.GetTextPosition());
+        pos != ax::mojom::TextPosition::kNone) {
+      AXStyleData::AddRange(style_data->text_positions, pos, start, end);
+    }
+    // GetColor() gets the blended color.
+    AXStyleData::AddRange(style_data->foreground_colors,
+                          static_cast<int>(node.GetColor()), start, end);
+    // GetBackgroundColor() gets blended background color.
+    AXStyleData::AddRange(style_data->background_colors,
+                          static_cast<int>(node.GetBackgroundColor()), start,
+                          end);
+    if (const auto& family = node.GetInheritedFontFamilyName();
+        !family.empty()) {
+      AXStyleData::AddRange(style_data->font_families, std::move(family), start,
+                            end);
+    }
+    // GetLanguage() gets the inherited language locale.
+    if (const auto& lang = node.GetLanguage(); !lang.empty()) {
+      AXStyleData::AddRange(style_data->locales, std::move(lang), start, end);
+    }
+  }
+}
+
 }  // namespace
 
 namespace ui {
@@ -187,11 +262,11 @@
   }
 
   if (string->empty()) {
-    *string = extra_text;
+    *string = std::move(extra_text);
     return;
   }
 
-  *string += std::u16string(u", ") + extra_text;
+  base::StrAppend(string, {u", ", std::move(extra_text)});
 }
 
 bool BrowserAccessibilityAndroid::IsCheckable() const {
@@ -379,14 +454,12 @@
 }
 
 bool BrowserAccessibilityAndroid::IsSubscript() const {
-  return static_cast<ax::mojom::TextPosition>(
-             GetIntAttribute(ax::mojom::IntAttribute::kTextPosition)) ==
+  return static_cast<ax::mojom::TextPosition>(GetTextPosition()) ==
          ax::mojom::TextPosition::kSubscript;
 }
 
 bool BrowserAccessibilityAndroid::IsSuperscript() const {
-  return static_cast<ax::mojom::TextPosition>(
-             GetIntAttribute(ax::mojom::IntAttribute::kTextPosition)) ==
+  return static_cast<ax::mojom::TextPosition>(GetTextPosition()) ==
          ax::mojom::TextPosition::kSuperscript;
 }
 
@@ -782,16 +855,37 @@
 }
 
 std::u16string BrowserAccessibilityAndroid::GetSubstringTextContentUTF16(
-    std::optional<size_t> min_length) const {
+    std::optional<size_t> min_length,
+    AXStyleData* style_data) const {
+  std::u16string text;
+  AccumulateSubstringTextContentUTF16(&text, min_length, style_data);
+  return text;
+}
+
+void BrowserAccessibilityAndroid::AccumulateSubstringTextContentUTF16(
+    std::u16string* accumulated_text,
+    std::optional<size_t> min_length,
+    AXStyleData* style_data) const {
+  CHECK(accumulated_text);
+  std::u16string text;
+  base::ScopedClosureRunner accumulate_text_and_styling(base::BindOnce(
+      [](const BrowserAccessibilityAndroid& node, std::u16string* dest,
+         std::u16string& src, AXStyleData* style_data) {
+        PopulateStyleData(node, *dest, src, style_data);
+        base::StrAppend(dest, {std::move(src)});
+      },
+      std::cref(*this), base::Unretained(accumulated_text), std::ref(text),
+      base::Unretained(style_data)));
+
   if (ui::IsIframe(GetRole())) {
-    return std::u16string();
+    return;
   }
 
-  // First, always return the |value| attribute if this is an
-  // input field.
+  // First, always return the `value` attribute if this is an input field.
   std::u16string value = GetValueForControl();
   if (ShouldExposeValueAsName(value)) {
-    return value;
+    text = std::move(value);
+    return;
   }
 
   // For color wells, the color is stored in separate attributes.
@@ -799,14 +893,14 @@
   if (GetRole() == ax::mojom::Role::kColorWell) {
     unsigned int color = static_cast<unsigned int>(
         GetIntAttribute(ax::mojom::IntAttribute::kColorValue));
-    return base::UTF8ToUTF16(skia::SkColorToHexString(color));
+    text = base::UTF8ToUTF16(skia::SkColorToHexString(color));
+    return;
   }
 
   // In the case of accessible name from kAttribute, the aria-label will be
   // mapped to one of the container title, content description or supplemental
   // description, we should exclude aria-label from mapping to text.
-  std::u16string text =
-      IsAccessibleNameFromAttribute() ? u"" : GetNameAsString16();
+  text = IsAccessibleNameFromAttribute() ? u"" : GetNameAsString16();
   if (ui::IsRangeValueSupported(GetRole())) {
     // For controls that support range values such as sliders, when a non-empty
     // name is present (e.g. a label), append this to the value so both the
@@ -831,13 +925,13 @@
   // For almost all focusable nodes we try to get text from contents, but for
   // the root node that's redundant and often way too verbose.
   if (ui::IsPlatformDocument(GetRole())) {
-    return text;
+    return;
   }
 
   // A role="separator" is a leaf, and cannot get name from contents, even if
   // author appends text children.
   if (GetRole() == ax::mojom::Role::kSplitter) {
-    return text;
+    return;
   }
 
   // Append image description strings to the text.
@@ -869,38 +963,29 @@
     }
   }
 
-  size_t text_length = text.size();
-  std::vector<std::u16string> inner_text({std::move(text)});
   // This is called from IsLeaf, so don't call PlatformChildCount
   // from within this!
   // Only for roles that do not support naming with child content, we loop
   // through the children, in order to populate the visual content (use Android
   // text API), in addition to populating the aria label information.
-  if (text_length == 0 && !ui::SupportsNamingWithChildContent(GetRole()) &&
+  if (text.size() == 0 && !ui::SupportsNamingWithChildContent(GetRole()) &&
       ((HasOnlyTextChildren() && !HasListMarkerChild()) ||
        (IsFocusable() && HasOnlyTextAndImageChildren()))) {
     for (auto it = InternalChildrenBegin(); it != InternalChildrenEnd(); ++it) {
-      std::u16string child_text =
-          static_cast<BrowserAccessibilityAndroid*>(it.get())
-              ->GetSubstringTextContentUTF16(min_length);
-      text_length += child_text.size();
-      inner_text.push_back(std::move(child_text));
-      if (min_length && text_length >= *min_length) {
+      static_cast<BrowserAccessibilityAndroid*>(it.get())
+          ->AccumulateSubstringTextContentUTF16(&text, min_length, style_data);
+      if (min_length && text.size() >= *min_length) {
         break;
       }
     }
   }
 
-  text = base::JoinString(inner_text, u"");
-
   if (text.empty() &&
       (ui::IsLink(GetRole()) || ui::IsImageOrVideo(GetRole())) &&
       !HasExplicitlyEmptyName()) {
     std::u16string url = GetString16Attribute(ax::mojom::StringAttribute::kUrl);
     text = ui::AXUrlBaseText(url);
   }
-
-  return text;
 }
 
 BrowserAccessibilityAndroid::EarlyExitPredicate
@@ -1455,6 +1540,10 @@
   return GetIntAttribute(ax::mojom::IntAttribute::kTextStyle);
 }
 
+int BrowserAccessibilityAndroid::GetTextPosition() const {
+  return GetIntAttribute(ax::mojom::IntAttribute::kTextPosition);
+}
+
 int BrowserAccessibilityAndroid::GetTextColor() const {
   return GetIntAttribute(ax::mojom::IntAttribute::kColor);
 }
diff --git a/content/browser/accessibility/browser_accessibility_android.h b/content/browser/accessibility/browser_accessibility_android.h
index 38ba1a1..c03d0e0 100644
--- a/content/browser/accessibility/browser_accessibility_android.h
+++ b/content/browser/accessibility/browser_accessibility_android.h
@@ -8,6 +8,7 @@
 #include <stddef.h>
 #include <stdint.h>
 
+#include <optional>
 #include <string>
 #include <vector>
 
@@ -20,6 +21,8 @@
 
 namespace content {
 
+struct AXStyleData;
+
 class CONTENT_EXPORT BrowserAccessibilityAndroid
     : public ui::BrowserAccessibility {
  public:
@@ -133,8 +136,11 @@
 
   typedef base::RepeatingCallback<bool(const std::u16string& partial)>
       EarlyExitPredicate;
+  // Gets the text content of this node, up to at least `min_length` if given.
+  // If `style_data` is provided, it's populated with styling information.
   std::u16string GetSubstringTextContentUTF16(
-      std::optional<size_t> min_length) const;
+      std::optional<size_t> min_length,
+      AXStyleData* style_data = nullptr) const;
   static EarlyExitPredicate NonEmptyPredicate();
   static EarlyExitPredicate LengthAtLeast(size_t length);
 
@@ -178,6 +184,7 @@
   // superscript from the methods above.
   float GetTextSize() const;
   int GetTextStyle() const;
+  int GetTextPosition() const;
   int GetTextColor() const;
   int GetTextBackgroundColor() const;
   std::string GetFontFamily() const;
@@ -308,6 +315,11 @@
   // Returns tree if any child has kSelect action verb.
   bool HasSelectActionVerbChildren() const;
 
+  // Helper function that accumulates the text content for the node.
+  void AccumulateSubstringTextContentUTF16(std::u16string* accumulated_text,
+                                           std::optional<size_t> min_length,
+                                           AXStyleData* style_data) const;
+
   std::u16string cached_text_;
   std::u16string old_value_;
   std::u16string new_value_;
diff --git a/content/browser/accessibility/browser_accessibility_android_unittest.cc b/content/browser/accessibility/browser_accessibility_android_unittest.cc
index b559c084..ef1618ac 100644
--- a/content/browser/accessibility/browser_accessibility_android_unittest.cc
+++ b/content/browser/accessibility/browser_accessibility_android_unittest.cc
@@ -8,15 +8,19 @@
 
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
+#include "content/browser/accessibility/ax_style_data.h"
 #include "content/browser/accessibility/browser_accessibility_manager_android.h"
 #include "content/browser/accessibility/web_contents_accessibility_android.h"
 #include "content/public/test/browser_task_environment.h"
 #include "content/test/test_content_client.h"
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "ui/accessibility/ax_enums.mojom-data-view.h"
 #include "ui/accessibility/platform/browser_accessibility_manager.h"
 #include "ui/accessibility/platform/test_ax_node_id_delegate.h"
 #include "ui/accessibility/platform/test_ax_platform_tree_manager_delegate.h"
 #include "ui/strings/grit/auto_image_annotation_strings.h"
+
 namespace content {
 
 namespace {
@@ -27,6 +31,11 @@
 }  // namespace
 
 using RetargetEventType = ui::AXTreeManager::RetargetEventType;
+using RangePairs = AXStyleData::RangePairs;
+
+using ::testing::Eq;
+using ::testing::Pair;
+using ::testing::UnorderedElementsAre;
 
 class MockContentClient : public TestContentClient {
  public:
@@ -67,6 +76,8 @@
   ~BrowserAccessibilityAndroidTest() override;
 
  protected:
+  static const ui::AXNodeID ROOT_ID = 100;
+
   std::unique_ptr<ui::TestAXPlatformTreeManagerDelegate>
       test_browser_accessibility_delegate_;
   ui::TestAXNodeIdDelegate node_id_delegate_;
@@ -700,6 +711,656 @@
             image_succeeded_with_name->GetSupplementalDescription());
 }
 
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_Suggestions) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101};
+  last_node->role = ax::mojom::Role::kTextField;
+  last_node->SetValue(u"Some very wrrrongly spelled words");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->child_ids = {201, 202, 203};
+  last_node->role = ax::mojom::Role::kGenericContainer;
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 201;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 202;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("very wrrrongly spelled");
+  last_node->AddIntListAttribute(
+      ax::mojom::IntListAttribute::kMarkerTypes,
+      {static_cast<int>(ax::mojom::MarkerType::kSuggestion)});
+  last_node->AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts,
+                                 {5});
+  last_node->AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds,
+                                 {14});
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 203;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" words");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some very wrrrongly spelled words"));
+  ASSERT_TRUE(style_data.suggestions);
+  EXPECT_THAT(*style_data.suggestions,
+              UnorderedElementsAre(Pair(u"", RangePairs{{10, 19}})));
+}
+
+// TODO: aluh - Enable once link nodes are merged into text content.
+TEST_F(BrowserAccessibilityAndroidTest, DISABLED_TextStyling_Links) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103};
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("A ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->child_ids = {201};
+  last_node->role = ax::mojom::Role::kLink;
+  last_node->AddStringAttribute(ax::mojom::StringAttribute::kUrl,
+                                "https://www.example.com/");
+  last_node->SetName("simple");
+  last_node->SetNameFrom(ax::mojom::NameFrom::kContents);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 201;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("simple");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" link");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"A simple link"));
+  ASSERT_TRUE(style_data.links);
+  EXPECT_THAT(*style_data.links,
+              UnorderedElementsAre(
+                  Pair(u"https://www.example.com/", RangePairs{{2, 8}})));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_NestedStyle) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103};
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("bold");
+  last_node->AddTextStyle(ax::mojom::TextStyle::kBold);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some bold text"));
+  ASSERT_TRUE(style_data.text_styles);
+  EXPECT_THAT(*style_data.text_styles,
+              UnorderedElementsAre(
+                  Pair(ax::mojom::TextStyle::kBold, RangePairs{{5, 9}})));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_MixedStyles) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103, 104, 105};
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("bold ");
+  last_node->AddTextStyle(ax::mojom::TextStyle::kBold);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("and");
+  last_node->AddTextStyle(ax::mojom::TextStyle::kBold);
+  last_node->AddTextStyle(ax::mojom::TextStyle::kItalic);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 104;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" italic");
+  last_node->AddTextStyle(ax::mojom::TextStyle::kItalic);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 105;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some bold and italic text"));
+  ASSERT_TRUE(style_data.text_styles);
+  EXPECT_THAT(
+      *style_data.text_styles,
+      UnorderedElementsAre(
+          Pair(ax::mojom::TextStyle::kBold, RangePairs{{5, 10}, {10, 13}}),
+          Pair(ax::mojom::TextStyle::kItalic, RangePairs{{10, 13}, {13, 20}})));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_TextSizes) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103};
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("big");
+  last_node->AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, 24.0f);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some big text"));
+  ASSERT_TRUE(style_data.text_sizes);
+  EXPECT_THAT(*style_data.text_sizes,
+              UnorderedElementsAre(Pair(24.0f, RangePairs{{5, 8}})));
+}
+
+// TODO: aluh - Enable once super/subscript nodes are merged into text content.
+TEST_F(BrowserAccessibilityAndroidTest, DISABLED_TextStyling_TextPositions) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 104};
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kSuperscript;
+  last_node->SetTextPosition(ax::mojom::TextPosition::kSuperscript);
+  last_node->child_ids = {103};
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("superscript");
+  last_node->SetTextPosition(ax::mojom::TextPosition::kSuperscript);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 104;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some superscript text"));
+  ASSERT_TRUE(style_data.text_positions);
+  EXPECT_THAT(*style_data.text_positions,
+              UnorderedElementsAre(Pair(ax::mojom::TextPosition::kSuperscript,
+                                        RangePairs{{5, 16}})));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_ForegroundColors) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103};
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("red");
+  last_node->AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFFF0000);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some red text"));
+  ASSERT_TRUE(style_data.foreground_colors);
+  EXPECT_THAT(
+      *style_data.foreground_colors,
+      UnorderedElementsAre(Pair(0x00000000, RangePairs{{0, 5}, {8, 13}}),
+                           Pair(0xFFFF0000, RangePairs{{5, 8}})));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_BackgroundColors) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103};
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("highlighted");
+  last_node->AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor,
+                             0xFF00FF00);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some highlighted text"));
+  ASSERT_TRUE(style_data.background_colors);
+  EXPECT_THAT(
+      *style_data.background_colors,
+      UnorderedElementsAre(Pair(0x00000000, RangePairs{{0, 5}, {16, 21}}),
+                           Pair(0xFF00FF00, RangePairs{{5, 16}})));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_BlendedColors) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103};
+  last_node->role = ax::mojom::Role::kGenericContainer;
+  last_node->AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFFF0000);
+  last_node->AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor,
+                             0xFFFFFF00);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("blended color");
+  last_node->AddIntAttribute(ax::mojom::IntAttribute::kColor, 0x55007788);
+  last_node->AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor,
+                             0x8800FFFF);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some blended color text"));
+  ASSERT_TRUE(style_data.foreground_colors);
+  EXPECT_THAT(
+      *style_data.foreground_colors,
+      UnorderedElementsAre(Pair(0xFFFF0000, RangePairs{{0, 5}, {18, 23}}),
+                           Pair(0xFFAA282D, RangePairs{{5, 18}})));
+  ASSERT_TRUE(style_data.background_colors);
+  EXPECT_THAT(
+      *style_data.background_colors,
+      UnorderedElementsAre(Pair(0xFFFFFF00, RangePairs{{0, 5}, {18, 23}}),
+                           Pair(0xFF77FF88, RangePairs{{5, 18}})));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_FontFamilies) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103};
+  last_node->AddStringAttribute(ax::mojom::StringAttribute::kFontFamily,
+                                "serif");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("sans serif");
+  last_node->AddStringAttribute(ax::mojom::StringAttribute::kFontFamily,
+                                "sans-serif");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some sans serif text"));
+  ASSERT_TRUE(style_data.font_families);
+  EXPECT_THAT(*style_data.font_families,
+              UnorderedElementsAre(Pair("serif", RangePairs{{0, 5}, {15, 20}}),
+                                   Pair("sans-serif", RangePairs{{5, 15}})));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_Locales) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103};
+  last_node->AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "en-US");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("繁體中文");
+  last_node->AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "zh-TW");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some 繁體中文 text"));
+  ASSERT_TRUE(style_data.locales);
+  EXPECT_THAT(*style_data.locales,
+              UnorderedElementsAre(Pair("en-US", RangePairs{{0, 5}, {9, 14}}),
+                                   Pair("zh-TW", RangePairs{{5, 9}})));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_ManyAttributes) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103};
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("fancy");
+  last_node->AddTextStyle(ax::mojom::TextStyle::kBold);
+  last_node->AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, 32.0f);
+  last_node->AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFFF0000);
+  last_node->AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor,
+                             0xFF0000FF);
+  last_node->AddStringAttribute(ax::mojom::StringAttribute::kFontFamily,
+                                "serif");
+  last_node->AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "ja-JP");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some fancy text"));
+  ASSERT_TRUE(style_data.text_styles);
+  EXPECT_THAT(*style_data.text_styles,
+              UnorderedElementsAre(
+                  Pair(ax::mojom::TextStyle::kBold, RangePairs{{5, 10}})));
+  ASSERT_TRUE(style_data.text_sizes);
+  EXPECT_THAT(*style_data.text_sizes,
+              UnorderedElementsAre(Pair(32.0f, RangePairs{{5, 10}})));
+  ASSERT_TRUE(style_data.foreground_colors);
+  EXPECT_THAT(
+      *style_data.foreground_colors,
+      UnorderedElementsAre(Pair(0x00000000, RangePairs{{0, 5}, {10, 15}}),
+                           Pair(0xFFFF0000, RangePairs{{5, 10}})));
+  ASSERT_TRUE(style_data.background_colors);
+  EXPECT_THAT(
+      *style_data.background_colors,
+      UnorderedElementsAre(Pair(0x00000000, RangePairs{{0, 5}, {10, 15}}),
+                           Pair(0xFF0000FF, RangePairs{{5, 10}})));
+  ASSERT_TRUE(style_data.font_families);
+  EXPECT_THAT(*style_data.font_families,
+              UnorderedElementsAre(Pair("serif", RangePairs{{5, 10}})));
+  ASSERT_TRUE(style_data.locales);
+  EXPECT_THAT(*style_data.locales,
+              UnorderedElementsAre(Pair("ja-JP", RangePairs{{5, 10}})));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_IgnoreInvalidValues) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103};
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("normal");
+  last_node->AddIntAttribute(ax::mojom::IntAttribute::kTextStyle, 0);
+  last_node->AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, 0);
+  last_node->AddStringAttribute(ax::mojom::StringAttribute::kFontFamily, "");
+  last_node->AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some normal text"));
+  EXPECT_FALSE(style_data.text_styles);
+  EXPECT_FALSE(style_data.text_sizes);
+  EXPECT_FALSE(style_data.font_families);
+  EXPECT_FALSE(style_data.locales);
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TextStyling_EmptyStyledText) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = ROOT_ID;
+
+  ui::AXNodeData* last_node = &tree.nodes.emplace_back();
+  last_node->id = ROOT_ID;
+  last_node->child_ids = {101, 102, 103};
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 101;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("Some ");
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 102;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName("");
+  last_node->AddTextStyle(ax::mojom::TextStyle::kBold);
+
+  last_node = &tree.nodes.emplace_back();
+  last_node->id = 103;
+  last_node->role = ax::mojom::Role::kStaticText;
+  last_node->SetName(" text");
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  AXStyleData style_data;
+  BrowserAccessibilityAndroid* container =
+      static_cast<BrowserAccessibilityAndroid*>(manager->GetFromID(ROOT_ID));
+  EXPECT_THAT(
+      container->GetSubstringTextContentUTF16(std::nullopt, &style_data),
+      Eq(u"Some  text"));
+  ASSERT_TRUE(style_data.text_styles);
+  EXPECT_THAT(*style_data.text_styles,
+              UnorderedElementsAre(
+                  Pair(ax::mojom::TextStyle::kBold, RangePairs{{5, 5}})));
+}
+
 TEST_F(BrowserAccessibilityAndroidTest, TestJavaNodeCache_AttributeChange) {
   ui::AXTreeUpdate tree;
   tree.root_id = 1;
diff --git a/content/browser/accessibility/browser_accessibility_state_impl.cc b/content/browser/accessibility/browser_accessibility_state_impl.cc
index b3382fa..6798112 100644
--- a/content/browser/accessibility/browser_accessibility_state_impl.cc
+++ b/content/browser/accessibility/browser_accessibility_state_impl.cc
@@ -57,6 +57,7 @@
 const char kAXModeBundleBasic[] = "basic";
 const char kAXModeBundleFormControls[] = "form-controls";
 const char kAXModeBundleComplete[] = "complete";
+const char kAXModeBundleOnScreen[] = "on-screen";
 
 // A data holder attached to a WebContents while it is hidden and has
 // accessibility enabled. Used only when the disable_on_hide feature of
@@ -355,13 +356,15 @@
     } else {
       // Support
       // --force-renderer-accessibility=[basic|form-controls|complete|
-      //                                 screen-reader]
+      //                                 screen-reader|on-screen]
       if (ax_mode_bundle.compare(kAXModeBundleBasic) == 0) {
         initial_mode = ui::kAXModeBasic;
       } else if (ax_mode_bundle.compare(kAXModeBundleFormControls) == 0) {
         initial_mode = ui::kAXModeFormControls;
       } else if (ax_mode_bundle.compare(kAXModeBundleComplete) == 0) {
         initial_mode = ui::kAXModeComplete;
+      } else if (ax_mode_bundle.compare(kAXModeBundleOnScreen) == 0) {
+        initial_mode = ui::kAXModeOnScreen;
       } else {
         // If 'screen-reader', or invalid, default to screen reader bundle,
         // which is the most useful in development and testing scenarios.
diff --git a/content/browser/navigation_browsertest.cc b/content/browser/navigation_browsertest.cc
index 0df9739..bdbbee5 100644
--- a/content/browser/navigation_browsertest.cc
+++ b/content/browser/navigation_browsertest.cc
@@ -5025,8 +5025,16 @@
   EXPECT_NE(origin_to_commit, origin_committed);
 }
 
+// TODO(crbug.com/424764870): Fix flakiness.
+#if BUILDFLAG(IS_FUCHSIA)
+#define MAYBE_NavigateToAboutBlankWhileFirstNavigationPending \
+  DISABLED_NavigateToAboutBlankWhileFirstNavigationPending
+#else
+#define MAYBE_NavigateToAboutBlankWhileFirstNavigationPending \
+  NavigateToAboutBlankWhileFirstNavigationPending
+#endif  // BUILDFLAG(IS_FUCHSIA)
 IN_PROC_BROWSER_TEST_F(NavigationBrowserTest,
-                       NavigateToAboutBlankWhileFirstNavigationPending) {
+                       MAYBE_NavigateToAboutBlankWhileFirstNavigationPending) {
   GURL url_a = embedded_test_server()->GetURL("a.com", "/empty.html");
   GURL url_b = embedded_test_server()->GetURL("b.com", "/empty.html");
 
diff --git a/content/browser/renderer_host/input/synthetic_gesture_target_base.cc b/content/browser/renderer_host/input/synthetic_gesture_target_base.cc
index 2fb56ef..8fabbea 100644
--- a/content/browser/renderer_host/input/synthetic_gesture_target_base.cc
+++ b/content/browser/renderer_host/input/synthetic_gesture_target_base.cc
@@ -4,6 +4,7 @@
 
 #include "content/browser/renderer_host/input/synthetic_gesture_target_base.h"
 
+#include "base/trace_event/trace_event.h"
 #include "components/input/events_helper.h"
 #include "components/input/render_widget_host_input_event_router.h"
 #include "content/browser/renderer_host/render_widget_host_impl.h"
diff --git a/content/browser/renderer_host/render_widget_host_view_event_handler.cc b/content/browser/renderer_host/render_widget_host_view_event_handler.cc
index 41b34a2..838636b4 100644
--- a/content/browser/renderer_host/render_widget_host_view_event_handler.cc
+++ b/content/browser/renderer_host/render_widget_host_view_event_handler.cc
@@ -8,6 +8,7 @@
 #include "base/metrics/user_metrics.h"
 #include "base/metrics/user_metrics_action.h"
 #include "base/numerics/safe_conversions.h"
+#include "base/trace_event/trace_event.h"
 #include "build/build_config.h"
 #include "components/input/render_widget_host_input_event_router.h"
 #include "components/input/switches.h"
diff --git a/content/browser/renderer_host/text_input_manager.cc b/content/browser/renderer_host/text_input_manager.cc
index 4f1d98e9..705a2ee 100644
--- a/content/browser/renderer_host/text_input_manager.cc
+++ b/content/browser/renderer_host/text_input_manager.cc
@@ -9,6 +9,7 @@
 #include "base/numerics/clamped_math.h"
 #include "base/observer_list.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/trace_event/trace_event.h"
 #include "build/build_config.h"
 #include "content/browser/renderer_host/render_widget_host_impl.h"
 #include "content/browser/renderer_host/render_widget_host_view_base.h"
diff --git a/gpu/command_buffer/client/client_shared_image.cc b/gpu/command_buffer/client/client_shared_image.cc
index 92fe26cb..f3509f1 100644
--- a/gpu/command_buffer/client/client_shared_image.cc
+++ b/gpu/command_buffer/client/client_shared_image.cc
@@ -581,12 +581,19 @@
   return CreateForTesting(viz::SinglePlaneFormat::kRGBA_8888, GL_TEXTURE_2D);
 }
 
+scoped_refptr<ClientSharedImage> ClientSharedImage::CreateSoftwareForTesting() {
+  auto shared_image = CreateForTesting();  // IN-TEST
+  shared_image->is_software_ = true;
+  return shared_image;
+}
+
 // static
 scoped_refptr<ClientSharedImage> ClientSharedImage::CreateForTesting(
     viz::SharedImageFormat format,
     uint32_t texture_target) {
   SharedImageMetadata metadata;
   metadata.format = format;
+  metadata.size = gfx::Size(64, 64);
   metadata.color_space = gfx::ColorSpace::CreateSRGB();
   metadata.surface_origin = kTopLeft_GrSurfaceOrigin;
   metadata.alpha_type = kOpaque_SkAlphaType;
@@ -600,6 +607,7 @@
     SharedImageUsageSet usage) {
   SharedImageMetadata metadata;
   metadata.format = viz::SinglePlaneFormat::kRGBA_8888;
+  metadata.size = gfx::Size(64, 64);
   metadata.color_space = gfx::ColorSpace::CreateSRGB();
   metadata.surface_origin = kTopLeft_GrSurfaceOrigin;
   metadata.alpha_type = kOpaque_SkAlphaType;
diff --git a/gpu/command_buffer/client/client_shared_image.h b/gpu/command_buffer/client/client_shared_image.h
index 29b26a7..ee7fc1e 100644
--- a/gpu/command_buffer/client/client_shared_image.h
+++ b/gpu/command_buffer/client/client_shared_image.h
@@ -201,6 +201,7 @@
   // Creates a ClientSharedImage that is not associated with any
   // SharedImageInterface for testing.
   static scoped_refptr<ClientSharedImage> CreateForTesting();
+  static scoped_refptr<ClientSharedImage> CreateSoftwareForTesting();
   static scoped_refptr<ClientSharedImage> CreateForTesting(
       viz::SharedImageFormat format,
       uint32_t texture_target);
diff --git a/gpu/config/gpu_finch_features.cc b/gpu/config/gpu_finch_features.cc
index ad5d40c..ab4af0d 100644
--- a/gpu/config/gpu_finch_features.cc
+++ b/gpu/config/gpu_finch_features.cc
@@ -222,7 +222,12 @@
              "EnableDrDc",
 #if BUILDFLAG(IS_ANDROID)
              base::FEATURE_ENABLED_BY_DEFAULT
+#elif BUILDFLAG(IS_MAC)
+             // DrDC will not be running if Graphite is disabled on Mac.
+             // Feature incomplete. DO NOT ENABLE!
+             base::FEATURE_DISABLED_BY_DEFAULT
 #else
+             // NOT SUPPORTED. DO NOT ENABLE!
              base::FEATURE_DISABLED_BY_DEFAULT
 #endif
 );
@@ -526,60 +531,6 @@
 #endif
 }
 
-bool IsDrDcEnabled() {
-#if BUILDFLAG(IS_ANDROID)
-  // Enabled on android P+.
-  if (base::android::BuildInfo::GetInstance()->sdk_int() <
-      base::android::SDK_VERSION_P) {
-    return false;
-  }
-
-  // DrDc is supported on android MediaPlayer and MCVD path only when
-  // AImageReader is enabled. Also DrDc requires AImageReader max size to be
-  // at least 2 for each gpu thread. Hence DrDc is disabled on devices which has
-  // only 1 image.
-  if (!base::android::EnableAndroidImageReader() ||
-      LimitAImageReaderMaxSizeToOne()) {
-    return false;
-  }
-
-  // Check block list against build info.
-  const auto* build_info = base::android::BuildInfo::GetInstance();
-  if (IsDeviceBlocked(build_info->device(), kDrDcBlockListByDevice.Get()))
-    return false;
-  if (IsDeviceBlocked(build_info->model(), kDrDcBlockListByModel.Get()))
-    return false;
-  if (IsDeviceBlocked(build_info->hardware(), kDrDcBlockListByHardware.Get()))
-    return false;
-  if (IsDeviceBlocked(build_info->brand(), kDrDcBlockListByBrand.Get()))
-    return false;
-  if (IsDeviceBlocked(build_info->android_build_id(),
-                      kDrDcBlockListByAndroidBuildId.Get()))
-    return false;
-  if (IsDeviceBlocked(build_info->manufacturer(),
-                      kDrDcBlockListByManufacturer.Get()))
-    return false;
-  if (IsDeviceBlocked(build_info->board(), kDrDcBlockListByBoard.Get()))
-    return false;
-  if (IsDeviceBlocked(build_info->android_build_fp(),
-                      kDrDcBlockListByAndroidBuildFP.Get()))
-    return false;
-
-  // Chrome on Android desktop aims to be Vulkan-only, which can result
-  // in crashes when enabled together with DrDc. Re-enable DrDc after
-  // crbug.com/380295059 is fixed if it is shown beneficial on desktop.
-  if (build_info->is_desktop())
-    return false;
-
-  if (!base::FeatureList::IsEnabled(kEnableDrDc))
-    return false;
-
-  return true;
-#else
-  return false;
-#endif
-}
-
 bool IsUsingThreadSafeMediaForWebView() {
 #if BUILDFLAG(IS_ANDROID)
   // SurfaceTexture can't be thread-safe. Also thread safe media code currently
@@ -715,6 +666,69 @@
   return base::FeatureList::IsEnabled(features::kSkiaGraphite);
 }
 
+bool IsDrDcEnabled() {
+#if BUILDFLAG(IS_ANDROID)
+  // Enabled on android P+.
+  if (base::android::BuildInfo::GetInstance()->sdk_int() <
+      base::android::SDK_VERSION_P) {
+    return false;
+  }
+
+  // DrDc is supported on android MediaPlayer and MCVD path only when
+  // AImageReader is enabled. Also DrDc requires AImageReader max size to be
+  // at least 2 for each gpu thread. Hence DrDc is disabled on devices which has
+  // only 1 image.
+  if (!base::android::EnableAndroidImageReader() ||
+      LimitAImageReaderMaxSizeToOne()) {
+    return false;
+  }
+
+  // Check block list against build info.
+  const auto* build_info = base::android::BuildInfo::GetInstance();
+  if (IsDeviceBlocked(build_info->device(), kDrDcBlockListByDevice.Get())) {
+    return false;
+  }
+  if (IsDeviceBlocked(build_info->model(), kDrDcBlockListByModel.Get())) {
+    return false;
+  }
+  if (IsDeviceBlocked(build_info->hardware(), kDrDcBlockListByHardware.Get())) {
+    return false;
+  }
+  if (IsDeviceBlocked(build_info->brand(), kDrDcBlockListByBrand.Get())) {
+    return false;
+  }
+  if (IsDeviceBlocked(build_info->android_build_id(),
+                      kDrDcBlockListByAndroidBuildId.Get())) {
+    return false;
+  }
+  if (IsDeviceBlocked(build_info->manufacturer(),
+                      kDrDcBlockListByManufacturer.Get())) {
+    return false;
+  }
+  if (IsDeviceBlocked(build_info->board(), kDrDcBlockListByBoard.Get())) {
+    return false;
+  }
+  if (IsDeviceBlocked(build_info->android_build_fp(),
+                      kDrDcBlockListByAndroidBuildFP.Get())) {
+    return false;
+  }
+
+  // Chrome on Android desktop aims to be Vulkan-only, which can result
+  // in crashes when enabled together with DrDc. Re-enable DrDc after
+  // crbug.com/380295059 is fixed if it is shown beneficial on desktop.
+  if (build_info->is_desktop()) {
+    return false;
+  }
+
+#elif BUILDFLAG(IS_MAC)
+  if (!IsSkiaGraphiteEnabled(base::CommandLine::ForCurrentProcess())) {
+    return false;
+  }
+#endif
+
+  return base::FeatureList::IsEnabled(kEnableDrDc);
+}
+
 bool IsSkiaGraphitePrecompilationEnabled(
     const base::CommandLine* command_line) {
   if (!IsSkiaGraphiteEnabled(command_line)) {
@@ -877,11 +891,15 @@
 // graphite::context as well as its wrapper class GraphiteSharedContext between
 // GpuMain and CompositorGpuThread. Note: When this feature is disabled,
 // each thread creates its own graphite::context and the context wrapper.
-//
-// Feature incomplete. DO NOT ENABLE!
 BASE_FEATURE(kGraphiteContextIsThreadSafe,
              "GraphiteContextIsThreadSafe",
+#if BUILDFLAG(IS_MAC)
+             // DrDC needs a thread-safe graphite context to work correctly.
+             base::FEATURE_ENABLED_BY_DEFAULT);
+#else
+             // Feature incomplete. DO NOT ENABLE!
              base::FEATURE_DISABLED_BY_DEFAULT);
+#endif
 
 bool IsGraphiteContextThreadSafe() {
   return base::FeatureList::IsEnabled(features::kGraphiteContextIsThreadSafe) &&
diff --git a/infra/config/generated/builder-owners/~unowned.txt b/infra/config/generated/builder-owners/~unowned.txt
index d061eee..c92f36f 100644
--- a/infra/config/generated/builder-owners/~unowned.txt
+++ b/infra/config/generated/builder-owners/~unowned.txt
@@ -33,7 +33,7 @@
 ci/chromeos-js-code-coverage
 ci/fuchsia-code-coverage
 ci/fuchsia-x64-accessibility-rel
-ci/ios-blink-dbg-fyi
+ci/ios-blink-rel-fyi
 ci/ios-catalyst
 ci/ios-device
 ci/ios-fieldtrial-rel
@@ -166,7 +166,7 @@
 try/fuchsia-x64-cast-receiver-rel-compilator
 try/ios-angle-try-intel
 try/ios-asan
-try/ios-blink-dbg-fyi
+try/ios-blink-rel-fyi
 try/ios-catalyst
 try/ios-device
 try/ios-fieldtrial-rel
diff --git a/infra/config/generated/builders/ci/android-16-x64-fyi-rel/targets/chromium.android.fyi.json b/infra/config/generated/builders/ci/android-16-x64-fyi-rel/targets/chromium.android.fyi.json
index a5e7d915..a6a1369 100644
--- a/infra/config/generated/builders/ci/android-16-x64-fyi-rel/targets/chromium.android.fyi.json
+++ b/infra/config/generated/builders/ci/android-16-x64-fyi-rel/targets/chromium.android.fyi.json
@@ -98,6 +98,56 @@
       },
       {
         "args": [
+          "--disable-field-trial-config",
+          "--emulator-debug-tags=all",
+          "--avd-config=../../tools/android/avd/proto/android_36_google_apis_x64.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "ci_only": true,
+        "description": "Run with android_36_google_apis_x64",
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_browsertests_no_fieldtrial"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "android_browsertests_no_fieldtrial",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_36_google_apis_x64",
+              "path": ".android_emulator/android_36_google_apis_x64"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_36_google_apis_x64"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
+        },
+        "test": "android_browsertests",
+        "test_id_prefix": "ninja://chrome/test:android_browsertests/"
+      },
+      {
+        "args": [
           "--test-launcher-batch-limit=1",
           "--emulator-debug-tags=all",
           "--avd-config=../../tools/android/avd/proto/android_36_google_apis_x64.textpb",
@@ -670,6 +720,55 @@
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices"
         ],
+        "ci_only": true,
+        "description": "Run with android_36_google_apis_x64",
+        "experiment_percentage": 100,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_apk_profile_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "chrome_public_apk_profile_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_36_google_apis_x64",
+              "path": ".android_emulator/android_36_google_apis_x64"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_36_google_apis_x64"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "chrome_public_apk_baseline_profile_generator",
+        "test_id_prefix": "ninja://chrome/test/android:chrome_public_apk_baseline_profile_generator/"
+      },
+      {
+        "args": [
+          "--emulator-debug-tags=all",
+          "--avd-config=../../tools/android/avd/proto/android_36_google_apis_x64.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "description": "Run with android_36_google_apis_x64",
         "merge": {
           "args": [
@@ -1829,6 +1928,53 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
+            "jni_zero_sample_apk_test"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "jni_zero_sample_apk_test",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_36_google_apis_x64",
+              "path": ".android_emulator/android_36_google_apis_x64"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_36_google_apis_x64"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "jni_zero_sample_apk_test",
+        "test_id_prefix": "ninja://third_party/jni_zero/sample:jni_zero_sample_apk_test/"
+      },
+      {
+        "args": [
+          "--emulator-debug-tags=all",
+          "--avd-config=../../tools/android/avd/proto/android_36_google_apis_x64.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "description": "Run with android_36_google_apis_x64",
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
             "latency_unittests"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
@@ -2113,6 +2259,53 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
+            "minidump_uploader_test"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "minidump_uploader_test",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_36_google_apis_x64",
+              "path": ".android_emulator/android_36_google_apis_x64"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_36_google_apis_x64"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "minidump_uploader_test",
+        "test_id_prefix": "ninja://components/minidump_uploader:minidump_uploader_test/"
+      },
+      {
+        "args": [
+          "--emulator-debug-tags=all",
+          "--avd-config=../../tools/android/avd/proto/android_36_google_apis_x64.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "description": "Run with android_36_google_apis_x64",
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
             "mojo_test_apk"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
diff --git a/infra/config/generated/builders/ci/ios-blink-dbg-fyi/gn-args.json b/infra/config/generated/builders/ci/ios-blink-rel-fyi/gn-args.json
similarity index 100%
rename from infra/config/generated/builders/ci/ios-blink-dbg-fyi/gn-args.json
rename to infra/config/generated/builders/ci/ios-blink-rel-fyi/gn-args.json
diff --git a/infra/config/generated/builders/ci/ios-blink-dbg-fyi/properties.json b/infra/config/generated/builders/ci/ios-blink-rel-fyi/properties.json
similarity index 88%
rename from infra/config/generated/builders/ci/ios-blink-dbg-fyi/properties.json
rename to infra/config/generated/builders/ci/ios-blink-rel-fyi/properties.json
index 84aaa03..a37b511 100644
--- a/infra/config/generated/builders/ci/ios-blink-dbg-fyi/properties.json
+++ b/infra/config/generated/builders/ci/ios-blink-rel-fyi/properties.json
@@ -2,14 +2,14 @@
   "$build/chromium_tests_builder_config": {
     "builder_config": {
       "additional_exclusions": [
-        "infra/config/generated/builders/ci/ios-blink-dbg-fyi/gn-args.json"
+        "infra/config/generated/builders/ci/ios-blink-rel-fyi/gn-args.json"
       ],
       "builder_db": {
         "entries": [
           {
             "builder_id": {
               "bucket": "ci",
-              "builder": "ios-blink-dbg-fyi",
+              "builder": "ios-blink-rel-fyi",
               "project": "chromium"
             },
             "builder_spec": {
@@ -36,17 +36,17 @@
       "builder_ids": [
         {
           "bucket": "ci",
-          "builder": "ios-blink-dbg-fyi",
+          "builder": "ios-blink-rel-fyi",
           "project": "chromium"
         }
       ],
       "mirroring_builder_group_and_names": [
         {
-          "builder": "ios-blink-dbg-fyi",
+          "builder": "ios-blink-rel-fyi",
           "group": "tryserver.chromium.mac"
         }
       ],
-      "targets_spec_directory": "src/infra/config/generated/builders/ci/ios-blink-dbg-fyi/targets"
+      "targets_spec_directory": "src/infra/config/generated/builders/ci/ios-blink-rel-fyi/targets"
     }
   },
   "$build/siso": {
diff --git a/infra/config/generated/builders/ci/ios-blink-dbg-fyi/shadow-properties.json b/infra/config/generated/builders/ci/ios-blink-rel-fyi/shadow-properties.json
similarity index 100%
rename from infra/config/generated/builders/ci/ios-blink-dbg-fyi/shadow-properties.json
rename to infra/config/generated/builders/ci/ios-blink-rel-fyi/shadow-properties.json
diff --git a/infra/config/generated/builders/ci/ios-blink-dbg-fyi/targets/chromium.fyi.json b/infra/config/generated/builders/ci/ios-blink-rel-fyi/targets/chromium.fyi.json
similarity index 99%
rename from infra/config/generated/builders/ci/ios-blink-dbg-fyi/targets/chromium.fyi.json
rename to infra/config/generated/builders/ci/ios-blink-rel-fyi/targets/chromium.fyi.json
index cc3315f..ef7fcf9 100644
--- a/infra/config/generated/builders/ci/ios-blink-dbg-fyi/targets/chromium.fyi.json
+++ b/infra/config/generated/builders/ci/ios-blink-rel-fyi/targets/chromium.fyi.json
@@ -1,5 +1,5 @@
 {
-  "ios-blink-dbg-fyi": {
+  "ios-blink-rel-fyi": {
     "additional_compile_targets": [
       "all"
     ],
diff --git a/infra/config/generated/builders/gn_args_locations.json b/infra/config/generated/builders/gn_args_locations.json
index 04d1096..70edb45 100644
--- a/infra/config/generated/builders/gn_args_locations.json
+++ b/infra/config/generated/builders/gn_args_locations.json
@@ -308,7 +308,7 @@
     "Win 10 Fast Ring": "ci/Win 10 Fast Ring/gn-args.json",
     "android-fieldtrial-rel": "ci/android-fieldtrial-rel/gn-args.json",
     "android-perfetto-rel": "ci/android-perfetto-rel/gn-args.json",
-    "ios-blink-dbg-fyi": "ci/ios-blink-dbg-fyi/gn-args.json",
+    "ios-blink-rel-fyi": "ci/ios-blink-rel-fyi/gn-args.json",
     "ios-fieldtrial-rel": "ci/ios-fieldtrial-rel/gn-args.json",
     "ios-vm": "ci/ios-vm/gn-args.json",
     "ios-webkit-tot": "ci/ios-webkit-tot/gn-args.json",
@@ -846,7 +846,7 @@
     "gpu-try-mac-amd-retina-dbg": "try/gpu-try-mac-amd-retina-dbg/gn-args.json",
     "gpu-try-mac-intel-dbg": "try/gpu-try-mac-intel-dbg/gn-args.json",
     "ios-asan": "try/ios-asan/gn-args.json",
-    "ios-blink-dbg-fyi": "try/ios-blink-dbg-fyi/gn-args.json",
+    "ios-blink-rel-fyi": "try/ios-blink-rel-fyi/gn-args.json",
     "ios-catalyst": "try/ios-catalyst/gn-args.json",
     "ios-device": "try/ios-device/gn-args.json",
     "ios-fieldtrial-rel": "try/ios-fieldtrial-rel/gn-args.json",
diff --git a/infra/config/generated/builders/try/android-16-x64-fyi-rel/targets/chromium.android.fyi.json b/infra/config/generated/builders/try/android-16-x64-fyi-rel/targets/chromium.android.fyi.json
index a5e7d915..a6a1369 100644
--- a/infra/config/generated/builders/try/android-16-x64-fyi-rel/targets/chromium.android.fyi.json
+++ b/infra/config/generated/builders/try/android-16-x64-fyi-rel/targets/chromium.android.fyi.json
@@ -98,6 +98,56 @@
       },
       {
         "args": [
+          "--disable-field-trial-config",
+          "--emulator-debug-tags=all",
+          "--avd-config=../../tools/android/avd/proto/android_36_google_apis_x64.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "ci_only": true,
+        "description": "Run with android_36_google_apis_x64",
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "android_browsertests_no_fieldtrial"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "android_browsertests_no_fieldtrial",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_36_google_apis_x64",
+              "path": ".android_emulator/android_36_google_apis_x64"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_36_google_apis_x64"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 3
+        },
+        "test": "android_browsertests",
+        "test_id_prefix": "ninja://chrome/test:android_browsertests/"
+      },
+      {
+        "args": [
           "--test-launcher-batch-limit=1",
           "--emulator-debug-tags=all",
           "--avd-config=../../tools/android/avd/proto/android_36_google_apis_x64.textpb",
@@ -670,6 +720,55 @@
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices"
         ],
+        "ci_only": true,
+        "description": "Run with android_36_google_apis_x64",
+        "experiment_percentage": 100,
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
+            "chrome_public_apk_profile_tests"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "chrome_public_apk_profile_tests",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_36_google_apis_x64",
+              "path": ".android_emulator/android_36_google_apis_x64"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_36_google_apis_x64"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "chrome_public_apk_baseline_profile_generator",
+        "test_id_prefix": "ninja://chrome/test/android:chrome_public_apk_baseline_profile_generator/"
+      },
+      {
+        "args": [
+          "--emulator-debug-tags=all",
+          "--avd-config=../../tools/android/avd/proto/android_36_google_apis_x64.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
         "description": "Run with android_36_google_apis_x64",
         "merge": {
           "args": [
@@ -1829,6 +1928,53 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
+            "jni_zero_sample_apk_test"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "jni_zero_sample_apk_test",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_36_google_apis_x64",
+              "path": ".android_emulator/android_36_google_apis_x64"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_36_google_apis_x64"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "jni_zero_sample_apk_test",
+        "test_id_prefix": "ninja://third_party/jni_zero/sample:jni_zero_sample_apk_test/"
+      },
+      {
+        "args": [
+          "--emulator-debug-tags=all",
+          "--avd-config=../../tools/android/avd/proto/android_36_google_apis_x64.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "description": "Run with android_36_google_apis_x64",
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
             "latency_unittests"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
@@ -2113,6 +2259,53 @@
             "--bucket",
             "chromium-result-details",
             "--test-name",
+            "minidump_uploader_test"
+          ],
+          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
+        },
+        "name": "minidump_uploader_test",
+        "resultdb": {
+          "enable": true,
+          "has_native_resultdb_integration": true
+        },
+        "swarming": {
+          "dimensions": {
+            "cores": "8",
+            "cpu": "x86-64",
+            "device_os": null,
+            "device_type": null,
+            "os": "Ubuntu-22.04",
+            "pool": "chromium.tests.avd"
+          },
+          "named_caches": [
+            {
+              "name": "android_36_google_apis_x64",
+              "path": ".android_emulator/android_36_google_apis_x64"
+            }
+          ],
+          "optional_dimensions": {
+            "60": {
+              "caches": "android_36_google_apis_x64"
+            }
+          },
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "minidump_uploader_test",
+        "test_id_prefix": "ninja://components/minidump_uploader:minidump_uploader_test/"
+      },
+      {
+        "args": [
+          "--emulator-debug-tags=all",
+          "--avd-config=../../tools/android/avd/proto/android_36_google_apis_x64.textpb",
+          "--gs-results-bucket=chromium-result-details",
+          "--recover-devices"
+        ],
+        "description": "Run with android_36_google_apis_x64",
+        "merge": {
+          "args": [
+            "--bucket",
+            "chromium-result-details",
+            "--test-name",
             "mojo_test_apk"
           ],
           "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
diff --git a/infra/config/generated/builders/try/ios-blink-dbg-fyi/targets/chromium.fyi.json b/infra/config/generated/builders/try/ios-blink-dbg-fyi/targets/chromium.fyi.json
deleted file mode 100644
index cc3315f..0000000
--- a/infra/config/generated/builders/try/ios-blink-dbg-fyi/targets/chromium.fyi.json
+++ /dev/null
@@ -1,2747 +0,0 @@
-{
-  "ios-blink-dbg-fyi": {
-    "additional_compile_targets": [
-      "all"
-    ],
-    "isolated_scripts": [
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "absl_hardening_tests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "absl_hardening_tests",
-        "test_id_prefix": "ninja://third_party/abseil-cpp:absl_hardening_tests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "angle_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "angle_unittests",
-        "test_id_prefix": "ninja://third_party/angle/src/tests:angle_unittests/",
-        "use_isolated_scripts_api": true,
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.base_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "base_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "base_unittests",
-        "test_id_prefix": "ninja://base:base_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "blink_common_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "blink_common_unittests",
-        "test_id_prefix": "ninja://third_party/blink/common:blink_common_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "blink_fuzzer_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "blink_fuzzer_unittests",
-        "test_id_prefix": "ninja://third_party/blink/renderer/platform:blink_fuzzer_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "blink_heap_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "blink_heap_unittests",
-        "test_id_prefix": "ninja://third_party/blink/renderer/platform/heap:blink_heap_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--git-revision=${got_revision}",
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.blink_platform_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "blink_platform_unittests iPhone 15 18.4",
-        "precommit_args": [
-          "--gerrit-issue=${patch_issue}",
-          "--gerrit-patchset=${patch_set}",
-          "--buildbucket-id=${buildbucket_build_id}"
-        ],
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "blink_platform_unittests",
-        "test_id_prefix": "ninja://third_party/blink/renderer/platform:blink_platform_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "boringssl_crypto_tests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "boringssl_crypto_tests",
-        "test_id_prefix": "ninja://third_party/boringssl:boringssl_crypto_tests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "boringssl_ssl_tests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "boringssl_ssl_tests",
-        "test_id_prefix": "ninja://third_party/boringssl:boringssl_ssl_tests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "capture_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "capture_unittests",
-        "test_id_prefix": "ninja://media/capture:capture_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "cast_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "cast_unittests",
-        "test_id_prefix": "ninja://media/cast:cast_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.cc_unittests.filter",
-          "--use-gpu-in-tests",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "cc_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "cc_unittests",
-        "test_id_prefix": "ninja://cc:cc_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.use_blink.components_browsertests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "components_browsertests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "components_browsertests",
-        "test_id_prefix": "ninja://components:components_browsertests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.use_blink.components_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "components_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "components_unittests",
-        "test_id_prefix": "ninja://components:components_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.compositor_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "compositor_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "compositor_unittests",
-        "test_id_prefix": "ninja://ui/compositor:compositor_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.content_browsertests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "content_browsertests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 10
-        },
-        "test": "content_browsertests",
-        "test_id_prefix": "ninja://content/test:content_browsertests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.content_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "content_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "content_unittests",
-        "test_id_prefix": "ninja://content/test:content_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "crashpad_tests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "crashpad_tests",
-        "test_id_prefix": "ninja://third_party/crashpad/crashpad:crashpad_tests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "crypto_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "crypto_unittests",
-        "test_id_prefix": "ninja://crypto:crypto_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "device_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "device_unittests",
-        "test_id_prefix": "ninja://device:device_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "display_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "display_unittests",
-        "test_id_prefix": "ninja://ui/display:display_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "env_chromium_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "env_chromium_unittests",
-        "test_id_prefix": "ninja://third_party/leveldatabase:env_chromium_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "events_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "events_unittests",
-        "test_id_prefix": "ninja://ui/events:events_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "gcm_unit_tests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "gcm_unit_tests",
-        "test_id_prefix": "ninja://google_apis/gcm:gcm_unit_tests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.gfx_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "gfx_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "gfx_unittests",
-        "test_id_prefix": "ninja://ui/gfx:gfx_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "gin_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "gin_unittests",
-        "test_id_prefix": "ninja://gin:gin_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "gl_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "gl_unittests",
-        "test_id_prefix": "ninja://ui/gl:gl_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "google_apis_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "google_apis_unittests",
-        "test_id_prefix": "ninja://google_apis:google_apis_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.gpu_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "gpu_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "gpu_unittests",
-        "test_id_prefix": "ninja://gpu:gpu_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "gwp_asan_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "gwp_asan_unittests",
-        "test_id_prefix": "ninja://components/gwp_asan:gwp_asan_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "latency_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "latency_unittests",
-        "test_id_prefix": "ninja://ui/latency:latency_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "leveldb_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "leveldb_unittests",
-        "test_id_prefix": "ninja://third_party/leveldatabase:leveldb_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "libjingle_xmpp_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "libjingle_xmpp_unittests",
-        "test_id_prefix": "ninja://third_party/libjingle_xmpp:libjingle_xmpp_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "liburlpattern_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "liburlpattern_unittests",
-        "test_id_prefix": "ninja://third_party/liburlpattern:liburlpattern_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.media_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "media_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "media_unittests",
-        "test_id_prefix": "ninja://media:media_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--enable-features=SkiaGraphite",
-          "--skia-graphite-backend=dawn",
-          "--use-gpu-in-tests",
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.media_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "media_unittests_skia_graphite_dawn iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "media_unittests",
-        "test_id_prefix": "ninja://media:media_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--enable-features=SkiaGraphite",
-          "--skia-graphite-backend=metal",
-          "--use-gpu-in-tests",
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.media_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "media_unittests_skia_graphite_metal iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "media_unittests",
-        "test_id_prefix": "ninja://media:media_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "midi_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "midi_unittests",
-        "test_id_prefix": "ninja://media/midi:midi_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.mojo_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "mojo_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "mojo_unittests",
-        "test_id_prefix": "ninja://mojo:mojo_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "native_theme_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "native_theme_unittests",
-        "test_id_prefix": "ninja://ui/native_theme:native_theme_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "net_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "net_unittests",
-        "test_id_prefix": "ninja://net:net_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "perfetto_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "perfetto_unittests",
-        "test_id_prefix": "ninja://third_party/perfetto:perfetto_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "printing_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "printing_unittests",
-        "test_id_prefix": "ninja://printing:printing_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "sandbox_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "sandbox_unittests",
-        "test_id_prefix": "ninja://sandbox:sandbox_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.services_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "services_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "services_unittests",
-        "test_id_prefix": "ninja://services:services_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "shell_dialogs_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "shell_dialogs_unittests",
-        "test_id_prefix": "ninja://ui/shell_dialogs:shell_dialogs_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "skia_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "skia_unittests",
-        "test_id_prefix": "ninja://skia:skia_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "sql_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "sql_unittests",
-        "test_id_prefix": "ninja://sql:sql_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "storage_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "storage_unittests",
-        "test_id_prefix": "ninja://storage:storage_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.ui_base_unittests.filter",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "ui_base_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "ui_base_unittests",
-        "test_id_prefix": "ninja://ui/base:ui_base_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "ui_touch_selection_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "ui_touch_selection_unittests",
-        "test_id_prefix": "ninja://ui/touch_selection:ui_touch_selection_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "ui_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "ui_unittests",
-        "test_id_prefix": "ninja://ui/tests:ui_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "url_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "url_unittests",
-        "test_id_prefix": "ninja://url:url_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--test-launcher-bot-mode",
-          "--test-launcher-filter-file=testing/buildbot/filters/ios.viz_unittests.filter",
-          "--use-gpu-in-tests",
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "viz_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "viz_unittests",
-        "test_id_prefix": "ninja://components/viz:viz_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "wtf_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "wtf_unittests",
-        "test_id_prefix": "ninja://third_party/blink/renderer/platform/wtf:wtf_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      },
-      {
-        "args": [
-          "--platform",
-          "iPhone 15",
-          "--version",
-          "18.4",
-          "--out-dir",
-          "${ISOLATED_OUTDIR}",
-          "--xcode-build-version",
-          "16f6",
-          "--xctest"
-        ],
-        "merge": {
-          "script": "//testing/merge_scripts/standard_isolated_script_merge.py"
-        },
-        "name": "zlib_unittests iPhone 15 18.4",
-        "resultdb": {
-          "enable": true,
-          "has_native_resultdb_integration": true
-        },
-        "swarming": {
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/mac_toolchain/${platform}",
-              "location": ".",
-              "revision": "git_revision:a18b7d95d26f3c6bf9591978b19cf0ca8268ac7d"
-            }
-          ],
-          "dimensions": {
-            "cpu": "arm64",
-            "os": "Mac-15.5"
-          },
-          "named_caches": [
-            {
-              "name": "runtime_ios_18_4",
-              "path": "Runtime-ios-18.4"
-            },
-            {
-              "name": "xcode_ios_16f6",
-              "path": "Xcode.app"
-            }
-          ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "zlib_unittests",
-        "test_id_prefix": "ninja://third_party/zlib:zlib_unittests/",
-        "variant_id": "iPhone 15 18.4"
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/infra/config/generated/builders/try/ios-blink-dbg-fyi/gn-args.json b/infra/config/generated/builders/try/ios-blink-rel-fyi/gn-args.json
similarity index 100%
rename from infra/config/generated/builders/try/ios-blink-dbg-fyi/gn-args.json
rename to infra/config/generated/builders/try/ios-blink-rel-fyi/gn-args.json
diff --git a/infra/config/generated/builders/try/ios-blink-dbg-fyi/properties.json b/infra/config/generated/builders/try/ios-blink-rel-fyi/properties.json
similarity index 89%
rename from infra/config/generated/builders/try/ios-blink-dbg-fyi/properties.json
rename to infra/config/generated/builders/try/ios-blink-rel-fyi/properties.json
index 0cc5a80..e07bdc6f 100644
--- a/infra/config/generated/builders/try/ios-blink-dbg-fyi/properties.json
+++ b/infra/config/generated/builders/try/ios-blink-rel-fyi/properties.json
@@ -2,14 +2,14 @@
   "$build/chromium_tests_builder_config": {
     "builder_config": {
       "additional_exclusions": [
-        "infra/config/generated/builders/try/ios-blink-dbg-fyi/gn-args.json"
+        "infra/config/generated/builders/try/ios-blink-rel-fyi/gn-args.json"
       ],
       "builder_db": {
         "entries": [
           {
             "builder_id": {
               "bucket": "ci",
-              "builder": "ios-blink-dbg-fyi",
+              "builder": "ios-blink-rel-fyi",
               "project": "chromium"
             },
             "builder_spec": {
@@ -36,11 +36,11 @@
       "builder_ids": [
         {
           "bucket": "ci",
-          "builder": "ios-blink-dbg-fyi",
+          "builder": "ios-blink-rel-fyi",
           "project": "chromium"
         }
       ],
-      "targets_spec_directory": "src/infra/config/generated/builders/try/ios-blink-dbg-fyi/targets"
+      "targets_spec_directory": "src/infra/config/generated/builders/try/ios-blink-rel-fyi/targets"
     }
   },
   "$build/siso": {
diff --git a/infra/config/generated/builders/ci/ios-blink-dbg-fyi/targets/chromium.fyi.json b/infra/config/generated/builders/try/ios-blink-rel-fyi/targets/chromium.fyi.json
similarity index 99%
copy from infra/config/generated/builders/ci/ios-blink-dbg-fyi/targets/chromium.fyi.json
copy to infra/config/generated/builders/try/ios-blink-rel-fyi/targets/chromium.fyi.json
index cc3315f..ef7fcf9 100644
--- a/infra/config/generated/builders/ci/ios-blink-dbg-fyi/targets/chromium.fyi.json
+++ b/infra/config/generated/builders/try/ios-blink-rel-fyi/targets/chromium.fyi.json
@@ -1,5 +1,5 @@
 {
-  "ios-blink-dbg-fyi": {
+  "ios-blink-rel-fyi": {
     "additional_compile_targets": [
       "all"
     ],
diff --git a/infra/config/generated/health-specs/health-specs.json b/infra/config/generated/health-specs/health-specs.json
index 0dc3524..3894ca36 100644
--- a/infra/config/generated/health-specs/health-specs.json
+++ b/infra/config/generated/health-specs/health-specs.json
@@ -8861,7 +8861,7 @@
           }
         ]
       },
-      "ios-blink-dbg-fyi": {
+      "ios-blink-rel-fyi": {
         "problem_specs": [
           {
             "name": "Unhealthy",
diff --git a/infra/config/generated/luci/commit-queue.cfg b/infra/config/generated/luci/commit-queue.cfg
index bac319f..92587132 100644
--- a/infra/config/generated/luci/commit-queue.cfg
+++ b/infra/config/generated/luci/commit-queue.cfg
@@ -4292,7 +4292,7 @@
         disable_reuse_footers: "Include-Ci-Only-Tests"
       }
       builders {
-        name: "chromium/try/ios-blink-dbg-fyi"
+        name: "chromium/try/ios-blink-rel-fyi"
         includable_only: true
         disable_reuse_footers: "Include-Ci-Only-Tests"
       }
diff --git a/infra/config/generated/luci/cr-buildbucket.cfg b/infra/config/generated/luci/cr-buildbucket.cfg
index 9cdcfd1..3350fba 100644
--- a/infra/config/generated/luci/cr-buildbucket.cfg
+++ b/infra/config/generated/luci/cr-buildbucket.cfg
@@ -49465,7 +49465,7 @@
       }
     }
     builders {
-      name: "ios-blink-dbg-fyi"
+      name: "ios-blink-rel-fyi"
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
       dimensions: "cpu:arm64"
@@ -49489,8 +49489,8 @@
         '    }'
         '  },'
         '  "$bootstrap/properties": {'
-        '    "properties_file": "infra/config/generated/builders/ci/ios-blink-dbg-fyi/properties.json",'
-        '    "shadow_properties_file": "infra/config/generated/builders/ci/ios-blink-dbg-fyi/shadow-properties.json",'
+        '    "properties_file": "infra/config/generated/builders/ci/ios-blink-rel-fyi/properties.json",'
+        '    "shadow_properties_file": "infra/config/generated/builders/ci/ios-blink-rel-fyi/shadow-properties.json",'
         '    "top_level_project": {'
         '      "ref": "refs/heads/main",'
         '      "repo": {'
@@ -49551,7 +49551,7 @@
           use_invocation_timestamp: true
         }
       }
-      description_html: "This builder is mirrored by any of the following try builders:<br/><ul><li><a href=\"https://ci.chromium.org/p/chromium/builders/try/ios-blink-dbg-fyi\">ios-blink-dbg-fyi</a></li></ul>"
+      description_html: "This builder is mirrored by any of the following try builders:<br/><ul><li><a href=\"https://ci.chromium.org/p/chromium/builders/try/ios-blink-rel-fyi\">ios-blink-rel-fyi</a></li></ul>"
       shadow_builder_adjustments {
         service_account: "chromium-try-builder@chops-service-accounts.iam.gserviceaccount.com"
         pool: "luci.chromium.try"
@@ -100432,7 +100432,7 @@
       }
     }
     builders {
-      name: "ios-blink-dbg-fyi"
+      name: "ios-blink-rel-fyi"
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
       dimensions: "cpu:arm64"
@@ -100456,7 +100456,7 @@
         '    }'
         '  },'
         '  "$bootstrap/properties": {'
-        '    "properties_file": "infra/config/generated/builders/try/ios-blink-dbg-fyi/properties.json",'
+        '    "properties_file": "infra/config/generated/builders/try/ios-blink-rel-fyi/properties.json",'
         '    "top_level_project": {'
         '      "ref": "refs/heads/main",'
         '      "repo": {'
@@ -100524,7 +100524,7 @@
           use_invocation_timestamp: true
         }
       }
-      description_html: "This builder mirrors the following CI builders:<br/><ul><li><a href=\"https://ci.chromium.org/p/chromium/builders/ci/ios-blink-dbg-fyi\">ios-blink-dbg-fyi</a></li></ul>"
+      description_html: "This builder mirrors the following CI builders:<br/><ul><li><a href=\"https://ci.chromium.org/p/chromium/builders/ci/ios-blink-rel-fyi\">ios-blink-rel-fyi</a></li></ul>"
       custom_metric_definitions {
         name: "/chrome/infra/browser/builds/cached_count"
         predicates: "has(build.output.properties.is_cached)"
diff --git a/infra/config/generated/luci/luci-milo.cfg b/infra/config/generated/luci/luci-milo.cfg
index a3696fac3b..396045ae 100644
--- a/infra/config/generated/luci/luci-milo.cfg
+++ b/infra/config/generated/luci/luci-milo.cfg
@@ -15426,7 +15426,7 @@
     short_name: "dbg"
   }
   builders {
-    name: "buildbucket/luci.chromium.ci/ios-blink-dbg-fyi"
+    name: "buildbucket/luci.chromium.ci/ios-blink-rel-fyi"
     category: "iOS"
     short_name: "ios-blk"
   }
@@ -26765,7 +26765,7 @@
     name: "buildbucket/luci.chromium.try/ios-asan"
   }
   builders {
-    name: "buildbucket/luci.chromium.try/ios-blink-dbg-fyi"
+    name: "buildbucket/luci.chromium.try/ios-blink-rel-fyi"
   }
   builders {
     name: "buildbucket/luci.chromium.try/ios-catalyst"
@@ -28657,7 +28657,7 @@
     name: "buildbucket/luci.chromium.try/ios-asan"
   }
   builders {
-    name: "buildbucket/luci.chromium.try/ios-blink-dbg-fyi"
+    name: "buildbucket/luci.chromium.try/ios-blink-rel-fyi"
   }
   builders {
     name: "buildbucket/luci.chromium.try/ios-catalyst"
diff --git a/infra/config/generated/luci/luci-scheduler.cfg b/infra/config/generated/luci/luci-scheduler.cfg
index 28a38915..034b855 100644
--- a/infra/config/generated/luci/luci-scheduler.cfg
+++ b/infra/config/generated/luci/luci-scheduler.cfg
@@ -4167,12 +4167,12 @@
   }
 }
 job {
-  id: "ios-blink-dbg-fyi"
+  id: "ios-blink-rel-fyi"
   realm: "ci"
   buildbucket {
     server: "cr-buildbucket.appspot.com"
     bucket: "ci"
-    builder: "ios-blink-dbg-fyi"
+    builder: "ios-blink-rel-fyi"
   }
 }
 job {
@@ -6456,7 +6456,7 @@
   triggers: "fuchsia-x64-perf-cast-receiver-rel"
   triggers: "ios-angle-builder"
   triggers: "ios-asan"
-  triggers: "ios-blink-dbg-fyi"
+  triggers: "ios-blink-rel-fyi"
   triggers: "ios-catalyst"
   triggers: "ios-device"
   triggers: "ios-fieldtrial-rel"
diff --git a/infra/config/lib/builder_exemptions.star b/infra/config/lib/builder_exemptions.star
index 483c7ec..7d296946 100644
--- a/infra/config/lib/builder_exemptions.star
+++ b/infra/config/lib/builder_exemptions.star
@@ -224,7 +224,7 @@
         "fuchsia-x64-cast-receiver-rel",
         "ios-angle-builder",
         "ios-angle-intel",
-        "ios-blink-dbg-fyi",
+        "ios-blink-rel-fyi",
         "ios-simulator-code-coverage",
         "ios-webkit-tot",
         "ios-wpt-fyi-rel",
@@ -450,7 +450,7 @@
         "fuchsia-x64-cast-receiver-rel-compilator",
         "ios-angle-try-intel",
         "ios-asan",
-        "ios-blink-dbg-fyi",
+        "ios-blink-rel-fyi",
         "ios-catalyst",
         "ios-device",
         "ios-fieldtrial-rel",
@@ -716,7 +716,7 @@
         "chromeos-js-code-coverage",
         "fuchsia-code-coverage",
         "fuchsia-x64-accessibility-rel",
-        "ios-blink-dbg-fyi",
+        "ios-blink-rel-fyi",
         "ios-catalyst",
         "ios-device",
         "ios-fieldtrial-rel",
@@ -854,7 +854,7 @@
         "fuchsia-x64-cast-receiver-rel-compilator",
         "ios-angle-try-intel",
         "ios-asan",
-        "ios-blink-dbg-fyi",
+        "ios-blink-rel-fyi",
         "ios-catalyst",
         "ios-device",
         "ios-fieldtrial-rel",
diff --git a/infra/config/subprojects/chromium/ci/chromium.fyi.star b/infra/config/subprojects/chromium/ci/chromium.fyi.star
index 58bbcdfa..c0183b0 100644
--- a/infra/config/subprojects/chromium/ci/chromium.fyi.star
+++ b/infra/config/subprojects/chromium/ci/chromium.fyi.star
@@ -1524,7 +1524,7 @@
 )
 
 fyi_ios_builder(
-    name = "ios-blink-dbg-fyi",
+    name = "ios-blink-rel-fyi",
     builder_spec = builder_config.builder_spec(
         gclient_config = builder_config.gclient_config(
             config = "ios",
@@ -1555,7 +1555,7 @@
     ),
     targets = targets.bundle(
         targets = [
-            "ios_blink_dbg_tests",
+            "ios_blink_rel_tests",
         ],
         additional_compile_targets = [
             "all",
diff --git a/infra/config/subprojects/chromium/try/tryserver.chromium.mac.star b/infra/config/subprojects/chromium/try/tryserver.chromium.mac.star
index 1434264..8dadf8d 100644
--- a/infra/config/subprojects/chromium/try/tryserver.chromium.mac.star
+++ b/infra/config/subprojects/chromium/try/tryserver.chromium.mac.star
@@ -594,11 +594,11 @@
 )
 
 ios_builder(
-    name = "ios-blink-dbg-fyi",
+    name = "ios-blink-rel-fyi",
     mirrors = [
-        "ci/ios-blink-dbg-fyi",
+        "ci/ios-blink-rel-fyi",
     ],
-    gn_args = "ci/ios-blink-dbg-fyi",
+    gn_args = "ci/ios-blink-rel-fyi",
     builderless = True,
     cpu = cpu.ARM64,
     execution_timeout = 4 * time.hour,
diff --git a/infra/config/targets/bundles.star b/infra/config/targets/bundles.star
index e144d64d..36f3b7f3 100644
--- a/infra/config/targets/bundles.star
+++ b/infra/config/targets/bundles.star
@@ -296,7 +296,11 @@
         "android_emulator_specific_chrome_public_tests",
         "android_trichrome_smoke_tests",
         "android_smoke_tests",
+        "chrome_profile_generator_tests",
         "chromium_gtests_for_devices_with_graphical_output",
+        "fieldtrial_android_tests",
+        "jni_zero_sample_apk_test",
+        "minidump_uploader_test",
         "linux_flavor_specific_chromium_gtests",
         "system_webview_shell_instrumentation_tests",  # Not an experimental test
         "webview_ui_instrumentation_tests",
@@ -5122,7 +5126,7 @@
 )
 
 targets.bundle(
-    name = "ios_blink_dbg_tests",
+    name = "ios_blink_rel_tests",
     targets = [
         targets.bundle(
             targets = "ios_blink_tests",
diff --git a/ios/chrome/browser/alert_view/ui_bundled/alert_action.h b/ios/chrome/browser/alert_view/ui_bundled/alert_action.h
index ff4d8f12..f78bd41 100644
--- a/ios/chrome/browser/alert_view/ui_bundled/alert_action.h
+++ b/ios/chrome/browser/alert_view/ui_bundled/alert_action.h
@@ -23,6 +23,11 @@
 // Block to be called when this action is triggered.
 @property(nonatomic, readonly) void (^handler)(AlertAction* action);
 
+// Determines whether the action is currently enabled. If set to NO, the
+// corresponding button in the UI will be disabled (e.g., grayed out and
+// non-interactive). Defaults to YES.
+@property(nonatomic, assign, getter=isEnabled) BOOL enabled;
+
 // Initializes an action with `title` and `handler`.
 + (instancetype)actionWithTitle:(NSString*)title
                           style:(UIAlertActionStyle)style
diff --git a/ios/chrome/browser/alert_view/ui_bundled/alert_action.mm b/ios/chrome/browser/alert_view/ui_bundled/alert_action.mm
index 360ac0b38..6ca65c3 100644
--- a/ios/chrome/browser/alert_view/ui_bundled/alert_action.mm
+++ b/ios/chrome/browser/alert_view/ui_bundled/alert_action.mm
@@ -16,6 +16,7 @@
     _title = [title copy];
     _handler = handler;
     _style = style;
+    _enabled = YES;
   }
   return self;
 }
diff --git a/ios/chrome/browser/alert_view/ui_bundled/alert_view_controller.mm b/ios/chrome/browser/alert_view/ui_bundled/alert_view_controller.mm
index 021f212..c6e2403 100644
--- a/ios/chrome/browser/alert_view/ui_bundled/alert_view_controller.mm
+++ b/ios/chrome/browser/alert_view/ui_bundled/alert_view_controller.mm
@@ -6,6 +6,7 @@
 
 #import <ostream>
 
+#import "base/apple/foundation_util.h"
 #import "base/check_op.h"
 #import "base/ios/ios_util.h"
 #import "base/notreached.h"
@@ -23,6 +24,8 @@
 #import "ui/base/l10n/l10n_util.h"
 
 namespace {
+// Tag for the button stack view.
+constexpr NSInteger kButtonStackViewTag = 9998;
 
 // Properties of the alert shadow.
 constexpr CGFloat kShadowOffsetX = 0;
@@ -148,19 +151,49 @@
   }
 }
 
+UIColor* ColorForActionStyle(UIAlertActionStyle style, BOOL enabled) {
+  UIColor* enabledStateDefaultColor = [UIColor colorNamed:kBlueColor];
+  UIColor* enabledStateDestructiveColor = [UIColor colorNamed:kRedColor];
+  UIColor* disabledStateColor = [UIColor lightGrayColor];
+
+  if (!enabled) {
+    return disabledStateColor;
+  }
+
+  switch (style) {
+    case UIAlertActionStyleDefault:
+      return enabledStateDefaultColor;
+    case UIAlertActionStyleCancel:
+      return enabledStateDefaultColor;
+    case UIAlertActionStyleDestructive:
+      return enabledStateDestructiveColor;
+  }
+}
+
+// Update the button foreground color depending on its state.
+void UpdateButtonColorDependingOnEnabledState(UIAlertActionStyle style,
+                                              UIButton* button) {
+  UIButtonConfiguration* configuration = button.configuration;
+  if (!configuration) {
+    return;
+  }
+
+  UIColor* color = ColorForActionStyle(style, button.enabled);
+  if (![configuration.baseForegroundColor isEqual:color]) {
+    configuration = [configuration copy];
+    configuration.baseForegroundColor = color;
+    button.configuration = configuration;
+  }
+}
+
 // Returns a GrayHighlightButton to be added to the alert for `action`.
 GrayHighlightButton* GetButtonForAction(AlertAction* action) {
   UIFont* font = nil;
-  UIColor* textColor = nil;
-  if (action.style == UIAlertActionStyleDefault) {
-    font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
-    textColor = [UIColor colorNamed:kBlueColor];
-  } else if (action.style == UIAlertActionStyleCancel) {
+
+  if (action.style == UIAlertActionStyleCancel) {
     font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
-    textColor = [UIColor colorNamed:kBlueColor];
-  } else {  // Style is UIAlertActionStyleDestructive
+  } else {
     font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
-    textColor = [UIColor colorNamed:kRedColor];
   }
 
   UIButtonConfiguration* buttonConfiguration =
@@ -173,7 +206,9 @@
       [[NSAttributedString alloc] initWithString:action.title
                                       attributes:attributes];
   buttonConfiguration.attributedTitle = title;
-  buttonConfiguration.baseForegroundColor = textColor;
+  buttonConfiguration.baseForegroundColor =
+      ColorForActionStyle(action.style, action.enabled);
+
   GrayHighlightButton* button =
       [GrayHighlightButton buttonWithConfiguration:buttonConfiguration
                                      primaryAction:nil];
@@ -182,6 +217,13 @@
   button.translatesAutoresizingMaskIntoConstraints = NO;
 
   button.tag = action.uniqueIdentifier;
+  button.enabled = action.enabled;
+
+  UIAlertActionStyle style = action.style;
+  button.configurationUpdateHandler = ^(UIButton* updatedButton) {
+    UpdateButtonColorDependingOnEnabledState(style, updatedButton);
+  };
+
   return button;
 }
 
@@ -447,6 +489,7 @@
 
   if ([self.actions count] > 0) {
     UIStackView* buttonStackView = [self createButtonStackView];
+    buttonStackView.tag = kButtonStackViewTag;
     [stackView addArrangedSubview:buttonStackView];
     AddSameConstraintsToSides(buttonStackView, self.contentView,
                               LayoutSides::kTrailing | LayoutSides::kLeading);
@@ -615,6 +658,56 @@
   }
 }
 
+- (void)setActions:(NSArray<NSArray<AlertAction*>*>*)newActions {
+  if ([_actions isEqual:newActions]) {
+    return;
+  }
+
+  _actions = [newActions copy];
+  _buttonAlertActionsDictionary = nil;
+
+  if (!self.isViewLoaded) {
+    return;
+  }
+
+  UIStackView* mainContentStackView = [self mainContentStackView];
+
+  if (!mainContentStackView) {
+    return;
+  }
+
+  UIView* oldButtonStackContainer =
+      [mainContentStackView viewWithTag:kButtonStackViewTag];
+  if (oldButtonStackContainer) {
+    [oldButtonStackContainer removeFromSuperview];
+  }
+
+  if (_actions.count > 0) {
+    UIStackView* newButtonStackContainer = [self createButtonStackView];
+    newButtonStackContainer.tag = kButtonStackViewTag;
+    [mainContentStackView addArrangedSubview:newButtonStackContainer];
+    AddSameConstraintsToSides(newButtonStackContainer, self.contentView,
+                              (LayoutSides::kTrailing | LayoutSides::kLeading));
+  }
+}
+
+- (UIStackView*)mainContentStackView {
+  for (UIView* subview_content in self.contentView.subviews) {
+    if (![subview_content isKindOfClass:[UIScrollView class]]) {
+      continue;
+    }
+
+    UIScrollView* scrollView =
+        base::apple::ObjCCastStrict<UIScrollView>(subview_content);
+    for (UIView* subview_scroll in scrollView.subviews) {
+      if ([subview_scroll isKindOfClass:[UIStackView class]]) {
+        return base::apple::ObjCCastStrict<UIStackView>(subview_scroll);
+      }
+    }
+  }
+  return nil;
+}
+
 - (void)setProgressState:(ProgressIndicatorState)progressState {
   if (_progressState != progressState) {
     _progressState = progressState;
@@ -764,9 +857,11 @@
       GrayHighlightButton* button = GetButtonForAction(action);
       if (self.actionButtonsAreInitiallyDisabled) {
         button.enabled = NO;
-        [self performSelector:@selector(enableActionButton:)
+        [self performSelector:@selector(updateButtonEnabledState:)
                    withObject:button
                    afterDelay:kEnableActionButtonsDelay];
+      } else {
+        [self updateButtonEnabledState:button];
       }
       [button addTarget:self
                     action:@selector(didSelectActionForButton:)
@@ -821,9 +916,11 @@
   [self.lastFocusedTextField resignFirstResponder];
 }
 
-// Enables `button`.
-- (void)enableActionButton:(UIButton*)actionButton {
-  actionButton.enabled = YES;
+- (void)updateButtonEnabledState:(UIButton*)button {
+  AlertAction* action = self.buttonAlertActionsDictionary[@(button.tag)];
+  if (action) {
+    button.enabled = action.enabled;
+  }
 }
 
 // Updates the `textFieldStackHolder`'s border color when the view controller's
diff --git a/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator.mm b/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator.mm
index d243e45d..8a442dd 100644
--- a/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator.mm
+++ b/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator.mm
@@ -50,7 +50,16 @@
   }
 
   [consumer_ setProgressState:ProgressIndicatorStateSuccess];
-  [consumer_ setActions:@[]];
+
+  NSString* cancelTitle =
+      base::SysUTF16ToNSString(model_controller_->GetCancelButtonLabel());
+  AlertAction* disabledCancelAction =
+      [AlertAction actionWithTitle:cancelTitle
+                             style:UIAlertActionStyleCancel
+                           handler:nil];
+  disabledCancelAction.enabled = NO;
+  [consumer_ setActions:@[ @[ disabledCancelAction ] ]];
+
   consumer_.confirmationAccessibilityLabel = l10n_util::GetNSString(
       IDS_IOS_AUTOFILL_PROGRESS_DIALOG_CONFIRMATION_ACCESSIBILITY_ANNOUNCEMENT);
 
diff --git a/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator_unittest.mm b/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator_unittest.mm
index d0c1548..1f41840 100644
--- a/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator_unittest.mm
+++ b/ios/chrome/browser/autofill/ui_bundled/progress_dialog/autofill_progress_dialog_mediator_unittest.mm
@@ -19,6 +19,39 @@
 #import "third_party/ocmock/gtest_support.h"
 #import "ui/base/l10n/l10n_util.h"
 
+namespace {
+
+// Helper function to check if the passed value contains a disabled cancel
+// action.
+bool ValueHasDisabledCancelAction(id value) {
+  if (![value isKindOfClass:[NSArray class]]) {
+    return NO;
+  }
+
+  NSArray* actionsRowsOrActions = (NSArray*)value;
+
+  for (id rowOrActionObject in actionsRowsOrActions) {
+    if ([rowOrActionObject isKindOfClass:[AlertAction class]]) {
+      AlertAction* action = (AlertAction*)rowOrActionObject;
+      if (action.style == UIAlertActionStyleCancel && !action.enabled) {
+        return YES;
+      }
+    } else if ([rowOrActionObject isKindOfClass:[NSArray class]]) {
+      NSArray* actionsInRow = (NSArray*)rowOrActionObject;
+      for (id actionObjectInRow in actionsInRow) {
+        if ([actionObjectInRow isKindOfClass:[AlertAction class]]) {
+          AlertAction* action = (AlertAction*)actionObjectInRow;
+          if (action.style == UIAlertActionStyleCancel && !action.enabled) {
+            return YES;
+          }
+        }
+      }
+    }
+  }
+  return NO;
+}
+}  // namespace
+
 class AutofillProgressDialogMediatorTest : public PlatformTest {
  protected:
   AutofillProgressDialogMediatorTest() {
@@ -74,7 +107,10 @@
 
   // Expectations for the consumer when showing confirmation.
   OCMExpect([consumer_ setProgressState:ProgressIndicatorStateSuccess]);
-  OCMExpect([consumer_ setActions:@[]]);
+
+  OCMExpect([consumer_ setActions:[OCMArg checkWithBlock:^BOOL(id value) {
+                         return ValueHasDisabledCancelAction(value);
+                       }]]);
 
   mediator_->Dismiss(/*show_confirmation_before_closing=*/true,
                      /*is_canceled_by_user=*/false);
diff --git a/ios/chrome/browser/autofill/ui_bundled/save_profile_egtest.mm b/ios/chrome/browser/autofill/ui_bundled/save_profile_egtest.mm
index 7e380a4..2a537db4 100644
--- a/ios/chrome/browser/autofill/ui_bundled/save_profile_egtest.mm
+++ b/ios/chrome/browser/autofill/ui_bundled/save_profile_egtest.mm
@@ -59,6 +59,9 @@
 // Email value used by the tests.
 constexpr char kEmail[] = "foo1@gmail.com";
 
+// Histogram bucket representing renderer errors.
+constexpr int kRendererErrorHistogramBucket = 8;
+
 struct FullAddressFormPageParams {
   // True if the submission should be default prevented.
   bool default_prevented = false;
@@ -264,6 +267,16 @@
         kAutofillFormSubmissionEventsInCaptureMode);
   }
 
+  if ([self isRunningTest:@selector(testSubmissionErrorReporting_Enabled)]) {
+    config.features_enabled.push_back(kAutofillIsolatedWorldForJavascriptIos);
+    config.features_enabled.push_back(kAutofillReportFormSubmissionErrors);
+  }
+
+  if ([self isRunningTest:@selector(testSubmissionErrorReporting_Disabled)]) {
+    config.features_enabled.push_back(kAutofillIsolatedWorldForJavascriptIos);
+    config.features_disabled.push_back(kAutofillReportFormSubmissionErrors);
+  }
+
   return config;
 }
 
@@ -333,8 +346,7 @@
 }
 
 // Loads, fills, and submits the full address form.
-- (void)loadAndSubmitFullAddressFormWithParams:
-    (FullAddressFormPageParams)params {
+- (void)loadFullAddressFormWithParams:(FullAddressFormPageParams)params {
   // Start server.
   GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
 
@@ -365,7 +377,11 @@
 
   // Call the helper function embedded in the page content to fill the form.
   [ChromeEarlGrey evaluateJavaScriptForSideEffect:@"FillForm();"];
+}
 
+- (void)loadAndSubmitFullAddressFormWithParams:
+    (FullAddressFormPageParams)params {
+  [self loadFullAddressFormWithParams:params];
   // Submit the form via the dedicated <button>.
   [ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];
 }
@@ -912,7 +928,7 @@
           ^{
             NSError* error = [MetricsAppInterface
                 expectTotalCount:6
-                    forHistogram:@"Autofill.iOS.FormSubmission.Outcome"];
+                    forHistogram:@"Autofill.iOS.FormSubmission.OutcomeV2"];
             return error == nil;
           }),
       @"Timed out waiting for all form submission events.");
@@ -944,7 +960,93 @@
   // multiple submissions on the same form.
   chrome_test_util::GREYAssertErrorNil([MetricsAppInterface
       expectTotalCount:1
-          forHistogram:@"Autofill.iOS.FormSubmission.Outcome"]);
+          forHistogram:@"Autofill.iOS.FormSubmission.OutcomeV2"]);
+}
+
+// Tests that the submission errors that occur in the renderer are reported to
+// the browser.
+- (void)testSubmissionErrorReporting_Enabled {
+  // Inject a bug that will trigger error when handling the form submission in
+  // the renderer.
+  constexpr char kInjectedBug[] = R"(
+    // Swizzle autofillSubmissionData() with an erroring function.
+    gcrweb.gCrWeb.fill.autofillSubmissionData = function() {
+      throw new Error("Oh no, something bad happened!");
+    };
+    // This is to give a return value to make the thing handling the JS
+    // execution happy.
+    true
+  )";
+
+  // Load page without submitting the form.
+  [self loadFullAddressFormWithParams:{}];
+
+  // Inject the bug in the submission handler so it triggers an error that will
+  // be reported to the browser.
+  [ChromeEarlGrey
+      evaluateJavaScriptInIsolatedWorldForSideEffect:base::SysUTF8ToNSString(
+                                                         kInjectedBug)];
+
+  // Now that the submission handler is buggy, submit the form to trigger the
+  // error.
+  [ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];
+
+  // Verify that no infobar is displayed when there is a submission error.
+  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
+
+  // Verify that the submission error was reported and recorded.
+  GREYAssertTrue(
+      base::test::ios::WaitUntilConditionOrTimeout(
+          base::Milliseconds(200),
+          ^{
+            NSError* error = [MetricsAppInterface
+                expectUniqueSampleWithCount:1
+                                  forBucket:kRendererErrorHistogramBucket
+                               forHistogram:
+                                   @"Autofill.iOS.FormSubmission.OutcomeV2"];
+            return error == nil;
+          }),
+      @"Timed out waiting for the submission error uma record.");
+}
+
+// Tests that the submission errors that occur in the renderer are not reported
+// to the browser when the feature is disabled.
+- (void)testSubmissionErrorReporting_Disabled {
+  // Inject a bug that will trigger error when handling the form submission in
+  // the renderer.
+  constexpr char kInjectedBug[] = R"(
+    // Swizzle autofillSubmissionData() with an erroring function.
+    gcrweb.gCrWeb.fill.autofillSubmissionData = function() {
+      throw new Error("Oh no, something bad happened!");
+    };
+    // This is to give a return value to make the thing handling the JS
+    // execution happy.
+    true
+  )";
+
+  // Load page without submitting the form.
+  [self loadFullAddressFormWithParams:{}];
+
+  // Inject the bug in the submission handler so it triggers an error that will
+  // be reported to the browser.
+  [ChromeEarlGrey
+      evaluateJavaScriptInIsolatedWorldForSideEffect:base::SysUTF8ToNSString(
+                                                         kInjectedBug)];
+
+  // Now that the submission handler is buggy, submit the form to trigger the
+  // error.
+  [ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];
+
+  // Verify for some time that no infobar is displayed when there is a
+  // submission error.
+  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
+
+  // Verify that no submission error was not reported and recorded. At this
+  // point there should have been enough time to hypothetically handle the
+  // submit event if there was no error.
+  chrome_test_util::GREYAssertErrorNil([MetricsAppInterface
+      expectTotalCount:0
+          forHistogram:@"Autofill.iOS.FormSubmission.OutcomeV2"]);
 }
 
 // Tests that submission is detected hence the infobar is displayed when the
diff --git a/ios/chrome/test/earl_grey/chrome_earl_grey.h b/ios/chrome/test/earl_grey/chrome_earl_grey.h
index 0a1c549..1e6bff1 100644
--- a/ios/chrome/test/earl_grey/chrome_earl_grey.h
+++ b/ios/chrome/test/earl_grey/chrome_earl_grey.h
@@ -698,6 +698,11 @@
 // Fails if the execution causes an error.
 - (void)evaluateJavaScriptForSideEffect:(NSString*)javaScript;
 
+// Same as -evaluateJavaScriptForSideEffect but executes the javascript in the
+// isolated world instead of the page content world. This allows interacting
+// with the gcrweb objects that are injected there.
+- (void)evaluateJavaScriptInIsolatedWorldForSideEffect:(NSString*)javaScript;
+
 // Returns the user agent that should be used for the mobile version.
 - (NSString*)mobileUserAgentString;
 
diff --git a/ios/chrome/test/earl_grey/chrome_earl_grey.mm b/ios/chrome/test/earl_grey/chrome_earl_grey.mm
index a6eea0dc..e5f86bb 100644
--- a/ios/chrome/test/earl_grey/chrome_earl_grey.mm
+++ b/ios/chrome/test/earl_grey/chrome_earl_grey.mm
@@ -1335,6 +1335,13 @@
       result.success, @"An error was produced during the script's execution");
 }
 
+- (void)evaluateJavaScriptInIsolatedWorldForSideEffect:(NSString*)javaScript {
+  JavaScriptExecutionResult* result =
+      [ChromeEarlGreyAppInterface executeJavaScriptInIsolatedWorld:javaScript];
+  EG_TEST_HELPER_ASSERT_TRUE(
+      result.success, @"An error was produced during the script's execution");
+}
+
 - (NSString*)mobileUserAgentString {
   return [ChromeEarlGreyAppInterface mobileUserAgentString];
 }
diff --git a/ios/chrome/test/earl_grey/chrome_earl_grey_app_interface.h b/ios/chrome/test/earl_grey/chrome_earl_grey_app_interface.h
index 182d839..4dd9638 100644
--- a/ios/chrome/test/earl_grey/chrome_earl_grey_app_interface.h
+++ b/ios/chrome/test/earl_grey/chrome_earl_grey_app_interface.h
@@ -496,6 +496,12 @@
 // otherwise returns object representing execution result.
 + (JavaScriptExecutionResult*)executeJavaScript:(NSString*)javaScript;
 
+// Same as -executeJavaScript but executes the script in the isolated world
+// instead of the page content world. This allows interacting with the gcrweb
+// objects that are injected there.
++ (JavaScriptExecutionResult*)executeJavaScriptInIsolatedWorld:
+    (NSString*)javaScript;
+
 // Returns the user agent that should be used for the mobile version.
 + (NSString*)mobileUserAgentString;
 
diff --git a/ios/chrome/test/earl_grey/chrome_earl_grey_app_interface.mm b/ios/chrome/test/earl_grey/chrome_earl_grey_app_interface.mm
index 6949f21..b33ab156 100644
--- a/ios/chrome/test/earl_grey/chrome_earl_grey_app_interface.mm
+++ b/ios/chrome/test/earl_grey/chrome_earl_grey_app_interface.mm
@@ -91,6 +91,7 @@
 #import "ios/web/common/features.h"
 #import "ios/web/js_messaging/web_view_js_utils.h"
 #import "ios/web/public/browser_state_utils.h"
+#import "ios/web/public/js_messaging/content_world.h"
 #import "ios/web/public/js_messaging/web_frame.h"
 #import "ios/web/public/js_messaging/web_frames_manager.h"
 #import "ios/web/public/navigation/navigation_manager.h"
@@ -1081,12 +1082,27 @@
 #pragma mark - JavaScript Utilities (EG2)
 
 + (JavaScriptExecutionResult*)executeJavaScript:(NSString*)javaScript {
+  return [ChromeEarlGreyAppInterface
+      executeJavaScript:javaScript
+                inWorld:static_cast<int>(web::ContentWorld::kPageContentWorld)];
+}
+
++ (JavaScriptExecutionResult*)executeJavaScriptInIsolatedWorld:
+    (NSString*)javaScript {
+  return [ChromeEarlGreyAppInterface
+      executeJavaScript:javaScript
+                inWorld:static_cast<int>(web::ContentWorld::kIsolatedWorld)];
+}
+
++ (JavaScriptExecutionResult*)executeJavaScript:(NSString*)javaScript
+                                        inWorld:(int)world {
   __block web::WebFrame* main_frame = nullptr;
   bool completed =
       WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
-        main_frame = chrome_test_util::GetCurrentWebState()
-                         ->GetPageWorldWebFramesManager()
-                         ->GetMainWebFrame();
+        main_frame =
+            chrome_test_util::GetCurrentWebState()
+                ->GetWebFramesManager(static_cast<web::ContentWorld>(world))
+                ->GetMainWebFrame();
         return main_frame != nullptr;
       });
 
diff --git a/media/audio/audio_input_unittest.cc b/media/audio/audio_input_unittest.cc
index 12b27a4..42c2e33 100644
--- a/media/audio/audio_input_unittest.cc
+++ b/media/audio/audio_input_unittest.cc
@@ -277,7 +277,7 @@
 }
 
 // Test create, open and close of an AudioInputStream without recording audio.
-// TODO(crbug.com/40262701): This test is failing on ios-blink-dbg-fyi bot.
+// TODO(crbug.com/40262701): This test is failing on ios-blink-rel-fyi bot.
 #if BUILDFLAG(IS_IOS)
 #define MAYBE_OpenAndClose DISABLED_OpenAndClose
 #else
@@ -294,7 +294,7 @@
 }
 
 // Test create, open, stop and close of an AudioInputStream without recording.
-// TODO(crbug.com/40262701): This test is failing on ios-blink-dbg-fyi bot.
+// TODO(crbug.com/40262701): This test is failing on ios-blink-rel-fyi bot.
 #if BUILDFLAG(IS_IOS)
 #define MAYBE_OpenStopAndClose DISABLED_OpenStopAndClose
 #else
@@ -312,7 +312,7 @@
 
 // Test a normal recording sequence using an AudioInputStream.
 // Very simple test which starts capturing and verifies that recording starts.
-// TODO(crbug.com/40262701): This test is failing on ios-blink-dbg-fyi bot.
+// TODO(crbug.com/40262701): This test is failing on ios-blink-rel-fyi bot.
 #if BUILDFLAG(IS_IOS)
 #define MAYBE_Record DISABLED_Record
 #else
diff --git a/net/BUILD.gn b/net/BUILD.gn
index d2ecebc..d8673bc 100644
--- a/net/BUILD.gn
+++ b/net/BUILD.gn
@@ -299,6 +299,8 @@
     "base/sockaddr_storage.cc",
     "base/sockaddr_storage.h",
     "base/sys_addrinfo.h",
+    "base/task/task_runner.cc",
+    "base/task/task_runner.h",
     "base/trace_event_stub.h",
     "base/tracing.h",
     "base/transport_info.cc",
diff --git a/net/android/java/src/org/chromium/net/ProxyBroadcastReceiver.java b/net/android/java/src/org/chromium/net/ProxyBroadcastReceiver.java
index 8da4b54..777ea3d 100644
--- a/net/android/java/src/org/chromium/net/ProxyBroadcastReceiver.java
+++ b/net/android/java/src/org/chromium/net/ProxyBroadcastReceiver.java
@@ -12,6 +12,7 @@
 
 import androidx.annotation.RequiresApi;
 
+import org.chromium.base.TraceEvent;
 import org.chromium.build.annotations.NullMarked;
 
 @NullMarked
@@ -25,8 +26,10 @@
     @Override
     @RequiresApi(Build.VERSION_CODES.M)
     public void onReceive(Context context, final Intent intent) {
-        if (Proxy.PROXY_CHANGE_ACTION.equals(intent.getAction())) {
-            mListener.updateProxyConfigFromConnectivityManager(intent);
+        try (TraceEvent e = TraceEvent.scoped("ProxyBroadcastReceiver#onReceive")) {
+            if (Proxy.PROXY_CHANGE_ACTION.equals(intent.getAction())) {
+                mListener.updateProxyConfigFromConnectivityManager(intent);
+            }
         }
     }
 }
diff --git a/net/android/java/src/org/chromium/net/ProxyChangeListener.java b/net/android/java/src/org/chromium/net/ProxyChangeListener.java
index 833919e..980e9569 100644
--- a/net/android/java/src/org/chromium/net/ProxyChangeListener.java
+++ b/net/android/java/src/org/chromium/net/ProxyChangeListener.java
@@ -163,8 +163,10 @@
         @Override
         @UsedByReflection("WebView embedders call this to override proxy settings")
         public void onReceive(Context context, final Intent intent) {
-            if (Proxy.PROXY_CHANGE_ACTION.equals(intent.getAction())) {
-                runOnThread(() -> proxySettingsChanged(extractNewProxy(intent)));
+            try (TraceEvent e = TraceEvent.scoped("ProxyChangeListener.ProxyReceiver#onReceive")) {
+                if (Proxy.PROXY_CHANGE_ACTION.equals(intent.getAction())) {
+                    runOnThread(() -> proxySettingsChanged(extractNewProxy(intent)));
+                }
             }
         }
     }
@@ -176,119 +178,128 @@
     // use system properties).
     @SuppressWarnings({"PrivateApi", "ObsoleteSdkInt"})
     private static @Nullable ProxyConfig extractNewProxy(Intent intent) {
-        Bundle extras = intent.getExtras();
-        if (extras == null) {
-            return null;
-        }
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            return ProxyConfig.fromProxyInfo(
-                    (ProxyInfo) extras.get("android.intent.extra.PROXY_INFO"));
-        }
-
-        try {
-            final String getHostName = "getHost";
-            final String getPortName = "getPort";
-            final String getPacFileUrl = "getPacFileUrl";
-            final String getExclusionList = "getExclusionList";
-            final String className = "android.net.ProxyProperties";
-
-            Object props = extras.get("proxy");
-            if (props == null) {
+        try (TraceEvent e = TraceEvent.scoped("ProxyChangeListener#extractNewProxy")) {
+            Bundle extras = intent.getExtras();
+            if (extras == null) {
                 return null;
             }
-
-            Class<?> cls = Class.forName(className);
-            Method getHostMethod = cls.getDeclaredMethod(getHostName);
-            Method getPortMethod = cls.getDeclaredMethod(getPortName);
-            Method getExclusionListMethod = cls.getDeclaredMethod(getExclusionList);
-
-            String host = (String) getHostMethod.invoke(props);
-            int port = (Integer) getPortMethod.invoke(props);
-
-            String[] exclusionList;
-            String s = (String) getExclusionListMethod.invoke(props);
-            exclusionList = s.split(",");
-
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
-                Method getPacFileUrlMethod = cls.getDeclaredMethod(getPacFileUrl);
-                String pacFileUrl = (String) getPacFileUrlMethod.invoke(props);
-                if (!TextUtils.isEmpty(pacFileUrl)) {
-                    return new ProxyConfig(host, port, pacFileUrl, exclusionList);
-                }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                return ProxyConfig.fromProxyInfo(
+                        (ProxyInfo) extras.get("android.intent.extra.PROXY_INFO"));
             }
-            return new ProxyConfig(host, port, null, exclusionList);
-        } catch (ClassNotFoundException
-                | NoSuchMethodException
-                | IllegalAccessException
-                | InvocationTargetException
-                | NullPointerException ex) {
-            Log.e(TAG, "Using no proxy configuration due to exception:" + ex);
-            return null;
+
+            try {
+                final String getHostName = "getHost";
+                final String getPortName = "getPort";
+                final String getPacFileUrl = "getPacFileUrl";
+                final String getExclusionList = "getExclusionList";
+                final String className = "android.net.ProxyProperties";
+
+                Object props = extras.get("proxy");
+                if (props == null) {
+                    return null;
+                }
+
+                Class<?> cls = Class.forName(className);
+                Method getHostMethod = cls.getDeclaredMethod(getHostName);
+                Method getPortMethod = cls.getDeclaredMethod(getPortName);
+                Method getExclusionListMethod = cls.getDeclaredMethod(getExclusionList);
+
+                String host = (String) getHostMethod.invoke(props);
+                int port = (Integer) getPortMethod.invoke(props);
+
+                String[] exclusionList;
+                String s = (String) getExclusionListMethod.invoke(props);
+                exclusionList = s.split(",");
+
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+                    Method getPacFileUrlMethod = cls.getDeclaredMethod(getPacFileUrl);
+                    String pacFileUrl = (String) getPacFileUrlMethod.invoke(props);
+                    if (!TextUtils.isEmpty(pacFileUrl)) {
+                        return new ProxyConfig(host, port, pacFileUrl, exclusionList);
+                    }
+                }
+                return new ProxyConfig(host, port, null, exclusionList);
+            } catch (ClassNotFoundException
+                    | NoSuchMethodException
+                    | IllegalAccessException
+                    | InvocationTargetException
+                    | NullPointerException ex) {
+                Log.e(TAG, "Using no proxy configuration due to exception:" + ex);
+                return null;
+            }
         }
     }
 
     private void proxySettingsChanged(@Nullable ProxyConfig cfg) {
-        assertOnThread();
+        try (TraceEvent e = TraceEvent.scoped("ProxyChangeListener#proxySettingsChanged")) {
+            assertOnThread();
 
-        if (mDelegate != null) {
-            // proxySettingsChanged is called even if mNativePtr == 0, for testing purposes.
-            mDelegate.proxySettingsChanged();
-        }
-        if (mNativePtr == 0) {
-            return;
-        }
+            if (mDelegate != null) {
+                // proxySettingsChanged is called even if mNativePtr == 0, for testing purposes.
+                mDelegate.proxySettingsChanged();
+            }
+            if (mNativePtr == 0) {
+                return;
+            }
 
-        if (cfg != null) {
-            ProxyChangeListenerJni.get()
-                    .proxySettingsChangedTo(
-                            mNativePtr,
-                            ProxyChangeListener.this,
-                            cfg.mHost,
-                            cfg.mPort,
-                            cfg.mPacUrl,
-                            cfg.mExclusionList);
-        } else {
-            ProxyChangeListenerJni.get().proxySettingsChanged(mNativePtr, ProxyChangeListener.this);
+            if (cfg != null) {
+                ProxyChangeListenerJni.get()
+                        .proxySettingsChangedTo(
+                                mNativePtr,
+                                ProxyChangeListener.this,
+                                cfg.mHost,
+                                cfg.mPort,
+                                cfg.mPacUrl,
+                                cfg.mExclusionList);
+            } else {
+                ProxyChangeListenerJni.get()
+                        .proxySettingsChanged(mNativePtr, ProxyChangeListener.this);
+            }
         }
     }
 
     @RequiresApi(Build.VERSION_CODES.M)
     private @Nullable ProxyConfig getProxyConfig(Intent intent) {
-        ConnectivityManager connectivityManager =
-                (ConnectivityManager)
-                        ContextUtils.getApplicationContext()
-                                .getSystemService(Context.CONNECTIVITY_SERVICE);
-        ProxyConfig configFromConnectivityManager =
-                ProxyConfig.fromProxyInfo(connectivityManager.getDefaultProxy());
+        try (TraceEvent e = TraceEvent.scoped("ProxyChangeListener#getProxyConfig")) {
+            ConnectivityManager connectivityManager =
+                    (ConnectivityManager)
+                            ContextUtils.getApplicationContext()
+                                    .getSystemService(Context.CONNECTIVITY_SERVICE);
+            ProxyConfig configFromConnectivityManager =
+                    ProxyConfig.fromProxyInfo(connectivityManager.getDefaultProxy());
 
-        if (configFromConnectivityManager == null) {
-            return ProxyConfig.DIRECT;
-        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
-                && configFromConnectivityManager.mHost.equals("localhost")
-                && configFromConnectivityManager.mPort == -1) {
-            ProxyConfig configFromIntent = extractNewProxy(intent);
-            Log.i(
-                    TAG,
-                    "configFromConnectivityManager = %s, configFromIntent = %s",
-                    configFromConnectivityManager,
-                    configFromIntent);
+            if (configFromConnectivityManager == null) {
+                return ProxyConfig.DIRECT;
+            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
+                    && configFromConnectivityManager.mHost.equals("localhost")
+                    && configFromConnectivityManager.mPort == -1) {
+                ProxyConfig configFromIntent = extractNewProxy(intent);
+                Log.i(
+                        TAG,
+                        "configFromConnectivityManager = %s, configFromIntent = %s",
+                        configFromConnectivityManager,
+                        configFromIntent);
 
-            // There's a bug in Android Q+ PAC support. If ConnectivityManager returns localhost:-1
-            // then use the intent from the PROXY_CHANGE_ACTION broadcast to extract the
-            // ProxyConfig's host and port. See http://crbug.com/993538.
-            //
-            // -1 is never a reasonable port so just keep this workaround for future versions until
-            // we're sure it's fixed on the platform side.
-            if (configFromIntent == null) return null;
-            String correctHost = configFromIntent.mHost;
-            int correctPort = configFromIntent.mPort;
-            return new ProxyConfig(
-                    correctHost,
-                    correctPort,
-                    configFromConnectivityManager.mPacUrl,
-                    configFromConnectivityManager.mExclusionList);
+                // There's a bug in Android Q+ PAC support. If ConnectivityManager returns
+                // localhost:-1
+                // then use the intent from the PROXY_CHANGE_ACTION broadcast to extract the
+                // ProxyConfig's host and port. See http://crbug.com/993538.
+                //
+                // -1 is never a reasonable port so just keep this workaround for future versions
+                // until
+                // we're sure it's fixed on the platform side.
+                if (configFromIntent == null) return null;
+                String correctHost = configFromIntent.mHost;
+                int correctPort = configFromIntent.mPort;
+                return new ProxyConfig(
+                        correctHost,
+                        correctPort,
+                        configFromConnectivityManager.mPacUrl,
+                        configFromConnectivityManager.mExclusionList);
+            }
+            return configFromConnectivityManager;
         }
-        return configFromConnectivityManager;
     }
 
     @RequiresApi(Build.VERSION_CODES.M)
diff --git a/net/base/task/OWNERS b/net/base/task/OWNERS
new file mode 100644
index 0000000..0121bbf
--- /dev/null
+++ b/net/base/task/OWNERS
@@ -0,0 +1 @@
+hayato@chromium.org
diff --git a/net/base/task/README.md b/net/base/task/README.md
new file mode 100644
index 0000000..1eb0c1a
--- /dev/null
+++ b/net/base/task/README.md
@@ -0,0 +1,55 @@
+# //net/base/task
+
+This directory provides a mechanism for obtaining `base::SingleThreadTaskRunner`
+instances that are integrated with task scheduling and prioritization system of
+the embedder process (e.g network service).
+
+## Overview
+
+The primary API offered is:
+
+```cpp
+namespace net {
+
+const scoped_refptr<base::SingleThreadTaskRunner>& GetTaskRunner(
+    RequestPriority priority);
+
+}  // namespace net
+```
+
+This function allows code (typically running on the network thread or
+interacting closely with network operations) to post tasks with a specific
+`net::RequestPriority`.
+
+## Integration with Network Service Scheduler
+
+The `NetworkServiceScheduler` (located in `//services/network/scheduler/`) is
+responsible for setting up and managing the actual task queues on the network
+service thread, including a high-priority queue.
+
+During its initialization (specifically in `SetupNetTaskRunners()`), the
+`NetworkServiceScheduler` populates
+`net::internal::GetTaskRunnerGlobals().high_priority_task_runner` with the task
+runner associated with its own high-priority queue.
+
+This ensures that when `net::GetTaskRunner(net::HIGHEST)` is called, tasks
+posted to the returned runner are routed to the network service's designated
+high-priority processing queue.
+
+## Usage
+
+Components that need to schedule work on the network thread with specific
+network-related priorities should use `net::GetTaskRunner()`. This helps ensure
+that critical network tasks (like those with `net::HIGHEST` priority) are
+processed appropriately by the network service's scheduler.
+
+If `RequestPriority` is unavailable or you are unsure which `priority` should be
+used, continue to use `base::SingleThreadTaskRunner::GetCurrentDefault()` as
+usual. This is currently equivalent to calling `net::GetTaskRunner(priority)`
+for any `priority` except `net::HIGHEST`.
+
+Task execution order is not guaranteed if you post tasks to different queues. If
+you need posted tasks to be executed in order, use the same task runner.
+
+The mapping of `RequestPriority` to underlying `TaskQueue` is subject to change.
+Please do not write code that depends on this mapping.
diff --git a/net/base/task/task_runner.cc b/net/base/task/task_runner.cc
new file mode 100644
index 0000000..692bdf5
--- /dev/null
+++ b/net/base/task/task_runner.cc
@@ -0,0 +1,32 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/base/task/task_runner.h"
+
+#include "base/no_destructor.h"
+
+namespace net {
+
+const scoped_refptr<base::SingleThreadTaskRunner>& GetTaskRunner(
+    RequestPriority priority) {
+  if (priority == RequestPriority::HIGHEST &&
+      internal::GetTaskRunnerGlobals().high_priority_task_runner) {
+    return internal::GetTaskRunnerGlobals().high_priority_task_runner;
+  }
+  return base::SingleThreadTaskRunner::GetCurrentDefault();
+}
+
+namespace internal {
+
+TaskRunnerGlobals::TaskRunnerGlobals() = default;
+TaskRunnerGlobals::~TaskRunnerGlobals() = default;
+
+TaskRunnerGlobals& GetTaskRunnerGlobals() {
+  static base::NoDestructor<TaskRunnerGlobals> globals;
+  return *globals;
+}
+
+}  // namespace internal
+
+}  // namespace net
diff --git a/net/base/task/task_runner.h b/net/base/task/task_runner.h
new file mode 100644
index 0000000..849efea2
--- /dev/null
+++ b/net/base/task/task_runner.h
@@ -0,0 +1,47 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_BASE_TASK_TASK_RUNNER_H_
+#define NET_BASE_TASK_TASK_RUNNER_H_
+
+#include "base/memory/scoped_refptr.h"
+#include "base/task/single_thread_task_runner.h"
+#include "net/base/net_export.h"
+#include "net/base/request_priority.h"
+
+namespace net {
+
+// Retrieves a task runner suitable for the given `priority`.
+//
+// This function allows different parts of the //net stack to obtain task
+// runners that are integrated with the network service's scheduling mechanism
+// (or other embedder's scheduling). For `RequestPriority::HIGHEST`, this may
+// return a special high-priority task runner if one has been configured (e.g.,
+// by the NetworkServiceScheduler). For other priorities, or if no special
+// runner is configured, it typically returns the current thread's default task
+// runner.
+NET_EXPORT const scoped_refptr<base::SingleThreadTaskRunner>& GetTaskRunner(
+    RequestPriority priority);
+
+namespace internal {
+
+// A struct holding global task runner instances that can be set by an
+// embedder (like the network service scheduler). This allows `GetTaskRunner`
+// to return specialized runners.
+struct NET_EXPORT TaskRunnerGlobals {
+  TaskRunnerGlobals();
+  ~TaskRunnerGlobals();
+
+  // Task runner specifically for `net::RequestPriority::HIGHEST` tasks.
+  // This is set by the embedder (e.g., NetworkServiceScheduler).
+  scoped_refptr<base::SingleThreadTaskRunner> high_priority_task_runner;
+};
+
+NET_EXPORT TaskRunnerGlobals& GetTaskRunnerGlobals();
+
+}  // namespace internal
+
+}  // namespace net
+
+#endif  // NET_BASE_TASK_TASK_RUNNER_H_
diff --git a/net/dns/host_resolver_manager.cc b/net/dns/host_resolver_manager.cc
index 181b1ec..f1450c1 100644
--- a/net/dns/host_resolver_manager.cc
+++ b/net/dns/host_resolver_manager.cc
@@ -826,6 +826,14 @@
   *out_stale_info = std::nullopt;
 
   CreateTaskSequence(job_key, cache_usage, secure_dns_policy, out_tasks);
+  source_net_log.AddEvent(
+      NetLogEventType::HOST_RESOLVER_MANAGER_TASK_SEQUENCE_CREATED, [&] {
+        base::Value::List tasks_list;
+        for (TaskType task : *out_tasks) {
+          tasks_list.Append(static_cast<int>(task));
+        }
+        return base::Value::Dict().Set("tasks", std::move(tasks_list));
+      });
 
   if (!ip_address.IsValid()) {
     // Check that the caller supplied a valid hostname to resolve. For
diff --git a/net/log/net_log_event_type_list.h b/net/log/net_log_event_type_list.h
index bedb7aee..df1896c 100644
--- a/net/log/net_log_event_type_list.h
+++ b/net/log/net_log_event_type_list.h
@@ -73,6 +73,13 @@
 //   }
 EVENT_TYPE(HOST_RESOLVER_MANAGER_IPV6_REACHABILITY_CHECK)
 
+// This event is created when the host resolver creates tasks for a request.
+// It contains the following parameter:
+//  {
+//     "tasks": <TaskTypes of a request>,
+//  }
+EVENT_TYPE(HOST_RESOLVER_MANAGER_TASK_SEQUENCE_CREATED)
+
 // This event is logged when a request is handled by a cache entry.
 // It contains the following parameter:
 //   {
diff --git a/net/socket/fuzzed_socket_factory.cc b/net/socket/fuzzed_socket_factory.cc
index 6487518..bc671fa7 100644
--- a/net/socket/fuzzed_socket_factory.cc
+++ b/net/socket/fuzzed_socket_factory.cc
@@ -93,6 +93,9 @@
 
   // SSLClientSocket implementation:
   std::vector<uint8_t> GetECHRetryConfigs() override { NOTREACHED(); }
+  std::vector<std::vector<uint8_t>> GetServerTrustAnchorIDsForRetry() override {
+    NOTREACHED();
+  }
 
  private:
   NetLogWithSource net_log_;
diff --git a/net/socket/socket_test_util.cc b/net/socket/socket_test_util.cc
index 9532fe09..0d3a8a30 100644
--- a/net/socket/socket_test_util.cc
+++ b/net/socket/socket_test_util.cc
@@ -1582,6 +1582,11 @@
   return data_->ech_retry_configs;
 }
 
+std::vector<std::vector<uint8_t>>
+MockSSLClientSocket::GetServerTrustAnchorIDsForRetry() {
+  return data_->server_trust_anchor_ids_for_retry;
+}
+
 void MockSSLClientSocket::RunCallbackAsync(CompletionOnceCallback callback,
                                            int result) {
   base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
diff --git a/net/socket/socket_test_util.h b/net/socket/socket_test_util.h
index 4c63cd2..fa2e045 100644
--- a/net/socket/socket_test_util.h
+++ b/net/socket/socket_test_util.h
@@ -598,6 +598,9 @@
   // Result for GetECHRetryConfigs().
   std::vector<uint8_t> ech_retry_configs;
 
+  // Result for GetServerTrustAnchorIDsForRetry().
+  std::vector<std::vector<uint8_t>> server_trust_anchor_ids_for_retry;
+
   std::optional<NextProtoVector> next_protos_expected_in_ssl_config;
   std::optional<SSLConfig::ApplicationSettings> expected_application_settings;
 
@@ -1024,6 +1027,7 @@
 
   // SSLClientSocket implementation.
   std::vector<uint8_t> GetECHRetryConfigs() override;
+  std::vector<std::vector<uint8_t>> GetServerTrustAnchorIDsForRetry() override;
 
   // This MockSocket does not implement the manual async IO feature.
   void OnReadComplete(const MockRead& data) override;
diff --git a/net/socket/ssl_client_socket.h b/net/socket/ssl_client_socket.h
index 3672a5f1..5e90d82 100644
--- a/net/socket/ssl_client_socket.h
+++ b/net/socket/ssl_client_socket.h
@@ -59,6 +59,15 @@
   // connection can be retried with ECH disabled.
   virtual std::vector<uint8_t> GetECHRetryConfigs() = 0;
 
+  // Called in response to a connection error in Connect(), when the client
+  // advertised the TLS Trust Anchor IDs extension. If this method returns a
+  // non-empty set, it is the Trust Anchor IDs (in binary representation) that
+  // the server provided in the handshake. The connection can be retried with
+  // these new Trust Anchor IDs, overriding the Trust Anchor IDs that the server
+  // advertised in DNS.
+  virtual std::vector<std::vector<uint8_t>>
+  GetServerTrustAnchorIDsForRetry() = 0;
+
   // Log SSL key material to |logger|. Must be called before any
   // SSLClientSockets are created.
   //
diff --git a/net/socket/ssl_client_socket_impl.cc b/net/socket/ssl_client_socket_impl.cc
index cc75671..d30d3c8f 100644
--- a/net/socket/ssl_client_socket_impl.cc
+++ b/net/socket/ssl_client_socket_impl.cc
@@ -15,6 +15,7 @@
 #include <utility>
 
 #include "base/containers/span.h"
+#include "base/containers/to_vector.h"
 #include "base/feature_list.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
@@ -312,6 +313,23 @@
       std::vector<uint8_t>(retry_configs, retry_configs + retry_configs_len));
 }
 
+std::vector<std::vector<uint8_t>>
+SSLClientSocketImpl::GetServerTrustAnchorIDsForRetry() {
+  const uint8_t* available_trust_anchor_ids;
+  size_t available_trust_anchor_ids_len;
+  SSL_get0_peer_available_trust_anchors(ssl_.get(), &available_trust_anchor_ids,
+                                        &available_trust_anchor_ids_len);
+  // SAFETY:
+  // https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_get0_peer_available_trust_anchors
+  // says `available_trust_anchor_ids` and `available_trust_anchor_ids_len`
+  // define a buffer containing a list of Trust Anchor IDs in wire format
+  // (length-prefixed non-empty strings);
+  base::SpanReader<const uint8_t> reader(
+      UNSAFE_BUFFERS(base::span<const uint8_t>(
+          available_trust_anchor_ids, available_trust_anchor_ids_len)));
+  return ParseServerTrustAnchorIDs(&reader);
+}
+
 int SSLClientSocketImpl::ExportKeyingMaterial(
     std::string_view label,
     std::optional<base::span<const uint8_t>> context,
@@ -1470,6 +1488,25 @@
     DoWriteCallback(rv_write);
 }
 
+// static
+std::vector<std::vector<uint8_t>>
+SSLClientSocketImpl::ParseServerTrustAnchorIDs(
+    base::SpanReader<const uint8_t>* reader) {
+  std::vector<std::vector<uint8_t>> trust_anchor_ids;
+  while (reader->remaining() > 0) {
+    uint8_t len;
+    if (!reader->ReadU8BigEndian(len) || len < 1u) {
+      return {};
+    }
+    std::optional<base::span<const uint8_t>> bytes = reader->Read(len);
+    if (!bytes) {
+      return {};
+    }
+    trust_anchor_ids.emplace_back(base::ToVector(*bytes));
+  }
+  return trust_anchor_ids;
+}
+
 int SSLClientSocketImpl::ClientCertRequestCallback(SSL* ssl) {
   DCHECK(ssl == ssl_.get());
 
diff --git a/net/socket/ssl_client_socket_impl.h b/net/socket/ssl_client_socket_impl.h
index 27789ac..e8055bb 100644
--- a/net/socket/ssl_client_socket_impl.h
+++ b/net/socket/ssl_client_socket_impl.h
@@ -16,6 +16,7 @@
 
 #include "base/compiler_specific.h"
 #include "base/containers/lru_cache.h"
+#include "base/containers/span_reader.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
@@ -48,8 +49,9 @@
 class SSLKeyLogger;
 class X509Certificate;
 
-class SSLClientSocketImpl : public SSLClientSocket,
-                            public SocketBIOAdapter::Delegate {
+class NET_EXPORT_PRIVATE SSLClientSocketImpl
+    : public SSLClientSocket,
+      public SocketBIOAdapter::Delegate {
  public:
   // Takes ownership of |stream_socket|, which may already be connected.
   // The given hostname will be compared with the name(s) in the server's
@@ -73,6 +75,7 @@
 
   // SSLClientSocket implementation.
   std::vector<uint8_t> GetECHRetryConfigs() override;
+  std::vector<std::vector<uint8_t>> GetServerTrustAnchorIDsForRetry() override;
 
   // SSLSocket implementation.
   int ExportKeyingMaterial(std::string_view label,
@@ -122,6 +125,7 @@
   class SSLContext;
   friend class SSLClientSocket;
   friend class SSLContext;
+  FRIEND_TEST_ALL_PREFIXES(SSLClientSocketTest, ParseServerTrustAnchorIDs);
 
   int Init();
   void DoReadCallback(int result);
@@ -144,6 +148,9 @@
   // and, if complete, runs the respective callbacks.
   void RetryAllOperations();
 
+  static std::vector<std::vector<uint8_t>> ParseServerTrustAnchorIDs(
+      base::SpanReader<const uint8_t>* reader);
+
   // Callback from the SSL layer when a certificate needs to be verified. This
   // is called when establishing new (fresh) connections and when evaluating
   // whether an existing session can be resumed.
diff --git a/net/socket/ssl_client_socket_unittest.cc b/net/socket/ssl_client_socket_unittest.cc
index cf603e2..10ebd86 100644
--- a/net/socket/ssl_client_socket_unittest.cc
+++ b/net/socket/ssl_client_socket_unittest.cc
@@ -17,6 +17,7 @@
 #include <utility>
 
 #include "base/containers/span.h"
+#include "base/containers/span_reader.h"
 #include "base/files/file_util.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
@@ -67,6 +68,7 @@
 #include "net/socket/client_socket_handle.h"
 #include "net/socket/read_buffering_stream_socket.h"
 #include "net/socket/socket_test_util.h"
+#include "net/socket/ssl_client_socket_impl.h"
 #include "net/socket/ssl_server_socket.h"
 #include "net/socket/stream_socket.h"
 #include "net/socket/tcp_client_socket.h"
@@ -2770,6 +2772,11 @@
   EXPECT_TRUE(ran_callback);
 }
 
+// Tests that SSLClientSocket sends Trust Anchor IDs when configured via
+// SSLConfig (similar to ConnectWithTrustAnchorIDs, but more end-to-end as it
+// tests that sending a Trust Anchor ID influences the actual certificate that
+// the server serves), and properly retrieves the server's Trust Anchor IDs from
+// the handshake on error.
 TEST_P(SSLClientSocketVersionTest, ConnectToServerWithTrustAnchorIDs) {
   SSLServerConfig server_config;
   SSLConfig client_config;
@@ -2778,6 +2785,8 @@
   ASSERT_TRUE(StartEmbeddedTestServer(
       EmbeddedTestServer::CERT_OK_BY_INTERMEDIATE, server_config));
 
+  // If the client doesn't advertise any trust anchor IDs on the connection,
+  // then the server should provide a full chain (with the intermediate).
   int rv;
   ASSERT_TRUE(CreateAndConnectSSLClientSocket(client_config, &rv));
   EXPECT_THAT(rv, IsOk());
@@ -2785,11 +2794,73 @@
   ASSERT_TRUE(sock_->GetSSLInfo(&ssl_info));
   EXPECT_EQ(1u, ssl_info.unverified_cert->intermediate_buffers().size());
 
+  // If the client advertises trust anchor IDs that don't correspond to the
+  // server's intermediate, then the server should provide a full chain (with
+  // the intermediate).
+  client_config.trust_anchor_ids = {0x03, 0x01, 0x01, 0x01, 0x02, 0x03, 0x03};
+  ASSERT_TRUE(CreateAndConnectSSLClientSocket(client_config, &rv));
+  EXPECT_THAT(rv, IsOk());
+  EXPECT_TRUE(sock_->GetServerTrustAnchorIDsForRetry().empty());
+  ASSERT_TRUE(sock_->GetSSLInfo(&ssl_info));
+  EXPECT_EQ(1u, ssl_info.unverified_cert->intermediate_buffers().size());
+
+  // If the client advertises the trust anchor ID corresponding to the server's
+  // intermediate, then the server should omit the intermediate from the
+  // connection.
   client_config.trust_anchor_ids = {0x03, 0x01, 0x02, 0x03};
   ASSERT_TRUE(CreateAndConnectSSLClientSocket(client_config, &rv));
   EXPECT_THAT(rv, IsOk());
+  EXPECT_TRUE(sock_->GetServerTrustAnchorIDsForRetry().empty());
   ASSERT_TRUE(sock_->GetSSLInfo(&ssl_info));
   EXPECT_EQ(0u, ssl_info.unverified_cert->intermediate_buffers().size());
+
+  // If the client advertises multiple trust anchor IDs including the one
+  // corresponding to the server's intermediate, then the server should omit the
+  // intermediate from the connection.
+  client_config.trust_anchor_ids = {0x02, 0x01, 0x01, 0x03, 0x01, 0x02, 0x03};
+  ASSERT_TRUE(CreateAndConnectSSLClientSocket(client_config, &rv));
+  EXPECT_THAT(rv, IsOk());
+  EXPECT_TRUE(sock_->GetServerTrustAnchorIDsForRetry().empty());
+  ASSERT_TRUE(sock_->GetSSLInfo(&ssl_info));
+  EXPECT_EQ(0u, ssl_info.unverified_cert->intermediate_buffers().size());
+
+  // If the client advertises the trust anchor ID corresponding to the server's
+  // intermediate but gets an error, it should be able to access the trust
+  // anchor IDs that the server advertised in the handshake.
+  cert_verifier_->set_default_result(ERR_CERT_INVALID);
+  client_config.trust_anchor_ids = {0x03, 0x01, 0x02, 0x03};
+  ASSERT_TRUE(CreateAndConnectSSLClientSocket(client_config, &rv));
+  EXPECT_THAT(rv, IsError(ERR_CERT_INVALID));
+  ASSERT_TRUE(sock_->GetSSLInfo(&ssl_info));
+  EXPECT_EQ(0u, ssl_info.unverified_cert->intermediate_buffers().size());
+  EXPECT_EQ(sock_->GetServerTrustAnchorIDsForRetry(),
+            std::vector<std::vector<uint8_t>>({{0x01, 0x02, 0x03}}));
+}
+
+// Tests the method that parses the server's Trust Anchor IDs that it can
+// provide in the handshake.
+TEST_F(SSLClientSocketTest, ParseServerTrustAnchorIDs) {
+  struct TestCase {
+    const std::vector<uint8_t> server_trust_anchor_ids;
+    const std::vector<std::vector<uint8_t>> expected_parsed_trust_anchor_ids;
+  };
+  TestCase test_cases[] = {
+      // Two Trust Anchor IDs, correctly formed
+      {{0x03, 0x01, 0x02, 0x03, 0x02, 0x01, 0x01},
+       {{0x01, 0x02, 0x03}, {0x01, 0x01}}},
+      // Empty
+      {{}, {}},
+      // Malformed
+      {{0x02, 0x1}, {}},
+      {{0x00, 0x01, 0x02, 0x03}, {}},
+      {{0x00}, {}},
+  };
+
+  for (const auto& test : test_cases) {
+    base::SpanReader<const uint8_t> reader(test.server_trust_anchor_ids);
+    auto result = SSLClientSocketImpl::ParseServerTrustAnchorIDs(&reader);
+    EXPECT_EQ(result, test.expected_parsed_trust_anchor_ids);
+  }
 }
 
 // Tests that OCSP stapling is requested, as per Certificate Transparency (RFC
diff --git a/net/socket/ssl_connect_job.cc b/net/socket/ssl_connect_job.cc
index 938e42f..ab9b85f 100644
--- a/net/socket/ssl_connect_job.cc
+++ b/net/socket/ssl_connect_job.cc
@@ -249,11 +249,17 @@
   DCHECK(!TimerIsRunning());
 
   next_state_ = STATE_TRANSPORT_CONNECT_COMPLETE;
-  // If this is an ECH retry, connect to the same server as before.
+  // If this is an ECH or Trust Anchor IDs retry, connect to the same server as
+  // before.
   std::optional<TransportConnectJob::EndpointResultOverride>
       endpoint_result_override;
-  if (ech_retry_configs_) {
-    DCHECK(ssl_client_context()->config().ech_enabled);
+  if (ech_retry_configs_ || !trust_anchor_ids_for_retry_.empty()) {
+    if (ech_retry_configs_) {
+      DCHECK(ssl_client_context()->config().ech_enabled);
+    }
+    if (!trust_anchor_ids_for_retry_.empty()) {
+      DCHECK(base::FeatureList::IsEnabled(features::kTLSTrustAnchorIDs));
+    }
     DCHECK(endpoint_result_);
     endpoint_result_override.emplace(*endpoint_result_, dns_aliases_);
   }
@@ -380,12 +386,15 @@
   }
 
   if (base::FeatureList::IsEnabled(features::kTLSTrustAnchorIDs) &&
-      endpoint_result_ &&
-      !endpoint_result_->metadata.trust_anchor_ids.empty() &&
       !ssl_client_context()->config().trust_anchor_ids.empty()) {
-    ssl_config.trust_anchor_ids = SSLConfig::SelectTrustAnchorIDs(
-        endpoint_result_->metadata.trust_anchor_ids,
-        ssl_client_context()->config().trust_anchor_ids);
+    if (!trust_anchor_ids_for_retry_.empty()) {
+      ssl_config.trust_anchor_ids = trust_anchor_ids_for_retry_;
+    } else if (endpoint_result_ &&
+               !endpoint_result_->metadata.trust_anchor_ids.empty()) {
+      ssl_config.trust_anchor_ids = SSLConfig::SelectTrustAnchorIDs(
+          endpoint_result_->metadata.trust_anchor_ids,
+          ssl_client_context()->config().trust_anchor_ids);
+    }
   }
 
   net_log().AddEvent(NetLogEventType::SSL_CONNECT_JOB_SSL_CONNECT, [&] {
@@ -458,6 +467,36 @@
     return OK;
   }
 
+  // If we got a certificate error and the server advertised some Trust Anchor
+  // IDs in the handshake that we trust, then retry the connection, using the
+  // fresh Trust Anchor IDs from the server. We only want to retry once; if we
+  // already have |server_trust_anchor_ids_for_retry_| set at this point, it
+  // means we already retried, so we skip all of this and treat the connection
+  // error as usual.
+  //
+  // TODO(https://crbug.com/399937371): clarify and test the interactions of ECH
+  // retry and TAI retry.
+  if (IsCertificateError(result) && trust_anchor_ids_for_retry_.empty() &&
+      base::FeatureList::IsEnabled(features::kTLSTrustAnchorIDs)) {
+    std::vector<std::vector<uint8_t>> server_trust_anchor_ids =
+        ssl_socket_->GetServerTrustAnchorIDsForRetry();
+    // https://tlswg.org/tls-trust-anchor-ids/draft-ietf-tls-trust-anchor-ids.html#name-retry-mechanism:
+    // If the EncryptedExtensions had no trust_anchor extension, or no match was
+    // found, the client returns the error to the application.
+    if (!server_trust_anchor_ids.empty()) {
+      trust_anchor_ids_for_retry_ = SSLConfig::SelectTrustAnchorIDs(
+          server_trust_anchor_ids,
+          ssl_client_context()->config().trust_anchor_ids);
+      if (!trust_anchor_ids_for_retry_.empty()) {
+        ResetStateForRestart();
+        next_state_ = GetInitialState(params_->GetConnectionType());
+        return OK;
+      }
+    }
+  }
+
+  // TODO(crbug.com/422931824): pass in Trust Anchor IDs info and record
+  // TAI-specific metrics.
   SSLClientSocket::RecordSSLConnectResult(ssl_socket_.get(), result,
                                           is_ech_capable, ech_enabled,
                                           ech_retry_configs_, connect_timing_);
diff --git a/net/socket/ssl_connect_job.h b/net/socket/ssl_connect_job.h
index 9f91944..606b045 100644
--- a/net/socket/ssl_connect_job.h
+++ b/net/socket/ssl_connect_job.h
@@ -220,6 +220,13 @@
   // If not `std::nullopt`, the ECH retry configs to use in the ECH recovery
   // flow. `endpoint_result_` will then contain the endpoint to reconnect to.
   std::optional<std::vector<uint8_t>> ech_retry_configs_;
+
+  // If not empty, the intersection of the client's trusted TLS Trust Anchor IDs
+  // with those advertised by the server during the handshake, in wire format.
+  // This is the set of Trust Anchor IDs to advertise in the ClientHello when
+  // retrying the connection after receiving an error. When this is non-empty,
+  // `endpoint_result_` will contain the endpoint to reconnect to.
+  std::vector<uint8_t> trust_anchor_ids_for_retry_;
 };
 
 }  // namespace net
diff --git a/net/socket/ssl_connect_job_unittest.cc b/net/socket/ssl_connect_job_unittest.cc
index 342f3c98..95f07f6 100644
--- a/net/socket/ssl_connect_job_unittest.cc
+++ b/net/socket/ssl_connect_job_unittest.cc
@@ -1193,6 +1193,307 @@
   }
 }
 
+// Test that when `SSLConnectJob` sends Trust Anchor IDs, it retries on failure,
+// using the Trust Anchor IDs that the server provides in the handshake.
+TEST_F(SSLConnectJobTest, TrustAnchorIDsRetry) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(features::kTLSTrustAnchorIDs);
+
+  SSLContextConfig config;
+  config.trust_anchor_ids = {{0x01, 0x02, 0x03}, {0x02, 0x02}, {0x04, 0x04}};
+  ssl_config_service_->UpdateSSLConfigAndNotify(config);
+
+  HostResolverEndpointResult endpoint;
+  endpoint.metadata.trust_anchor_ids = {
+      {0x01, 0x02, 0x03}, {0x04, 0x04}, {0x05, 0x05, 0x05}};
+  endpoint.ip_endpoints = {IPEndPoint(ParseIP("1::"), 8441)};
+  host_resolver_.rules()->AddRule(
+      "host",
+      MockHostResolverBase::RuleResolver::RuleResult(std::vector{endpoint}));
+
+  StaticSocketDataProvider data1;
+  data1.set_expected_addresses(AddressList(endpoint.ip_endpoints));
+  data1.set_connect_data(MockConnect(SYNCHRONOUS, OK));
+  socket_factory_.AddSocketDataProvider(&data1);
+  // The first connection attempt will fail with a certificate error (simulating
+  // the server providing a certificate that the client does not trust, because,
+  // for example, the server's Trust Anchor IDs advertised in DNS were stale and
+  // it does not actually have a certificate for the trust anchor that the
+  // client selected).
+  SSLSocketDataProvider ssl_fail(ASYNC, ERR_CERT_AUTHORITY_INVALID);
+  ssl_fail.expected_trust_anchor_ids =
+      std::vector<uint8_t>({0x03, 0x01, 0x02, 0x03, 0x02, 0x04, 0x04});
+  // The server provides a different set of Trust Anchor IDs in the handshake
+  // than were present in the DNS record. This simulates the situation in which
+  // the server can't provide a certificate chaining to a trust anchor that the
+  // client signalled in the handshake, so it made its best guess, but it has
+  // another certificate available that the client does actually trust.
+  ssl_fail.server_trust_anchor_ids_for_retry =
+      std::vector<std::vector<uint8_t>>({{0x02, 0x02}, {0x05, 0x6}});
+  socket_factory_.AddSSLSocketDataProvider(&ssl_fail);
+  // The second connection attempt and handshake succeed.
+  StaticSocketDataProvider data2;
+  data2.set_expected_addresses(AddressList(endpoint.ip_endpoints));
+  data2.set_connect_data(MockConnect(SYNCHRONOUS, OK));
+  socket_factory_.AddSocketDataProvider(&data2);
+  SSLSocketDataProvider ssl_success(ASYNC, OK);
+  ssl_success.expected_trust_anchor_ids =
+      std::vector<uint8_t>({0x02, 0x02, 0x02});
+  socket_factory_.AddSSLSocketDataProvider(&ssl_success);
+
+  TestConnectJobDelegate test_delegate;
+  std::unique_ptr<ConnectJob> ssl_connect_job =
+      CreateConnectJob(&test_delegate, ProxyChain::Direct(), MEDIUM);
+  EXPECT_THAT(ssl_connect_job->Connect(), test::IsError(ERR_IO_PENDING));
+  EXPECT_THAT(test_delegate.WaitForResult(), test::IsOk());
+}
+
+// Test that when `SSLConnectJob` sends Trust Anchor IDs and the connection
+// fails, the client does not retry if the server does not provide Trust Anchor
+// IDs in the handshake.
+TEST_F(SSLConnectJobTest, NoRetryIfNoServerTrustAnchorIDs) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(features::kTLSTrustAnchorIDs);
+
+  SSLContextConfig config;
+  config.trust_anchor_ids = {{0x01, 0x02, 0x03}, {0x02, 0x02}, {0x04, 0x04}};
+  ssl_config_service_->UpdateSSLConfigAndNotify(config);
+
+  HostResolverEndpointResult endpoint;
+  endpoint.metadata.trust_anchor_ids = {
+      {0x01, 0x02, 0x03}, {0x04, 0x04}, {0x05, 0x05, 0x05}};
+  endpoint.ip_endpoints = {IPEndPoint(ParseIP("1::"), 8441)};
+  host_resolver_.rules()->AddRule(
+      "host",
+      MockHostResolverBase::RuleResolver::RuleResult(std::vector{endpoint}));
+
+  StaticSocketDataProvider data1;
+  data1.set_expected_addresses(AddressList(endpoint.ip_endpoints));
+  data1.set_connect_data(MockConnect(SYNCHRONOUS, OK));
+  socket_factory_.AddSocketDataProvider(&data1);
+  // The first connection attempt will fail with a certificate error (simulating
+  // the server providing a certificate that the client does not trust, because,
+  // for example, the server's Trust Anchor IDs advertised in DNS were stale and
+  // it does not actually have a certificate for the trust anchor that the
+  // client selected).
+  SSLSocketDataProvider ssl_fail(ASYNC, ERR_CERT_AUTHORITY_INVALID);
+  ssl_fail.expected_trust_anchor_ids =
+      std::vector<uint8_t>({0x03, 0x01, 0x02, 0x03, 0x02, 0x04, 0x04});
+  // The server does not provide any Trust Anchor IDs in the handshake, so there
+  // should be no retry.
+  socket_factory_.AddSSLSocketDataProvider(&ssl_fail);
+
+  TestConnectJobDelegate test_delegate(
+      TestConnectJobDelegate::SocketExpected::ALWAYS);
+  std::unique_ptr<ConnectJob> ssl_connect_job =
+      CreateConnectJob(&test_delegate, ProxyChain::Direct(), MEDIUM);
+  EXPECT_THAT(ssl_connect_job->Connect(), test::IsError(ERR_IO_PENDING));
+  EXPECT_THAT(test_delegate.WaitForResult(),
+              test::IsError(ERR_CERT_AUTHORITY_INVALID));
+}
+
+// Test that when `SSLConnectJob` sends Trust Anchor IDs and the connection
+// fails, the client does not retry if it does not trust any of the Trust Anchor
+// IDs that the server provides in the handshake.
+TEST_F(SSLConnectJobTest, NoRetryIfNoIntersectionWithServerTrustAnchorIDs) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(features::kTLSTrustAnchorIDs);
+
+  SSLContextConfig config;
+  config.trust_anchor_ids = {{0x01, 0x02, 0x03}, {0x02, 0x02}, {0x04, 0x04}};
+  ssl_config_service_->UpdateSSLConfigAndNotify(config);
+
+  HostResolverEndpointResult endpoint;
+  endpoint.metadata.trust_anchor_ids = {
+      {0x01, 0x02, 0x03}, {0x04, 0x04}, {0x05, 0x05, 0x05}};
+  endpoint.ip_endpoints = {IPEndPoint(ParseIP("1::"), 8441)};
+  host_resolver_.rules()->AddRule(
+      "host",
+      MockHostResolverBase::RuleResolver::RuleResult(std::vector{endpoint}));
+
+  StaticSocketDataProvider data1;
+  data1.set_expected_addresses(AddressList(endpoint.ip_endpoints));
+  data1.set_connect_data(MockConnect(SYNCHRONOUS, OK));
+  socket_factory_.AddSocketDataProvider(&data1);
+  // The first connection attempt will fail with a certificate error (simulating
+  // the server providing a certificate that the client does not trust, because,
+  // for example, the server's Trust Anchor IDs advertised in DNS were stale and
+  // it does not actually have a certificate for the trust anchor that the
+  // client selected).
+  SSLSocketDataProvider ssl_fail(ASYNC, ERR_CERT_AUTHORITY_INVALID);
+  ssl_fail.expected_trust_anchor_ids =
+      std::vector<uint8_t>({0x03, 0x01, 0x02, 0x03, 0x02, 0x04, 0x04});
+  // The server does not provide any Trust Anchor IDs in the handshake that the
+  // client trusts, so there should be no retry.
+  ssl_fail.server_trust_anchor_ids_for_retry =
+      std::vector<std::vector<uint8_t>>({{0x06, 0x06}, {0x07, 0x7}});
+  socket_factory_.AddSSLSocketDataProvider(&ssl_fail);
+
+  TestConnectJobDelegate test_delegate(
+      TestConnectJobDelegate::SocketExpected::ALWAYS);
+  std::unique_ptr<ConnectJob> ssl_connect_job =
+      CreateConnectJob(&test_delegate, ProxyChain::Direct(), MEDIUM);
+  EXPECT_THAT(ssl_connect_job->Connect(), test::IsError(ERR_IO_PENDING));
+  EXPECT_THAT(test_delegate.WaitForResult(),
+              test::IsError(ERR_CERT_AUTHORITY_INVALID));
+}
+
+// Test that when `SSLConnectJob` sends Trust Anchor IDs and the connection
+// fails, the client does not retry if the error is not certificate-related.
+TEST_F(SSLConnectJobTest, NoRetryIfNotCertificateError) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(features::kTLSTrustAnchorIDs);
+
+  SSLContextConfig config;
+  config.trust_anchor_ids = {{0x01, 0x02, 0x03}, {0x02, 0x02}, {0x04, 0x04}};
+  ssl_config_service_->UpdateSSLConfigAndNotify(config);
+
+  HostResolverEndpointResult endpoint;
+  endpoint.metadata.trust_anchor_ids = {
+      {0x01, 0x02, 0x03}, {0x04, 0x04}, {0x05, 0x05, 0x05}};
+  endpoint.ip_endpoints = {IPEndPoint(ParseIP("1::"), 8441)};
+  host_resolver_.rules()->AddRule(
+      "host",
+      MockHostResolverBase::RuleResolver::RuleResult(std::vector{endpoint}));
+
+  StaticSocketDataProvider data1;
+  data1.set_expected_addresses(AddressList(endpoint.ip_endpoints));
+  data1.set_connect_data(MockConnect(SYNCHRONOUS, OK));
+  socket_factory_.AddSocketDataProvider(&data1);
+  // The connection attempt will fail with a non-certificate error.
+  SSLSocketDataProvider ssl_fail(ASYNC, ERR_SSL_KEY_USAGE_INCOMPATIBLE);
+  ssl_fail.expected_trust_anchor_ids =
+      std::vector<uint8_t>({0x03, 0x01, 0x02, 0x03, 0x02, 0x04, 0x04});
+  ssl_fail.server_trust_anchor_ids_for_retry =
+      std::vector<std::vector<uint8_t>>({{0x02, 0x02}});
+  socket_factory_.AddSSLSocketDataProvider(&ssl_fail);
+  // There should be no retry because the error was not certificate-related.
+
+  TestConnectJobDelegate test_delegate;
+  std::unique_ptr<ConnectJob> ssl_connect_job =
+      CreateConnectJob(&test_delegate, ProxyChain::Direct(), MEDIUM);
+  EXPECT_THAT(ssl_connect_job->Connect(), test::IsError(ERR_IO_PENDING));
+  EXPECT_THAT(test_delegate.WaitForResult(),
+              test::IsError(ERR_SSL_KEY_USAGE_INCOMPATIBLE));
+}
+
+// Test that `SSLConnectJob` does not retry more than once even if the server
+// provides Trust Anchor IDs in each handshake attempt.
+TEST_F(SSLConnectJobTest, TrustAnchorIDsRetryOnlyOnce) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(features::kTLSTrustAnchorIDs);
+
+  SSLContextConfig config;
+  config.trust_anchor_ids = {{0x01, 0x02, 0x03}, {0x02, 0x02}, {0x04, 0x04}};
+  ssl_config_service_->UpdateSSLConfigAndNotify(config);
+
+  HostResolverEndpointResult endpoint;
+  endpoint.metadata.trust_anchor_ids = {
+      {0x01, 0x02, 0x03}, {0x04, 0x04}, {0x05, 0x05, 0x05}};
+  endpoint.ip_endpoints = {IPEndPoint(ParseIP("1::"), 8441)};
+  host_resolver_.rules()->AddRule(
+      "host",
+      MockHostResolverBase::RuleResolver::RuleResult(std::vector{endpoint}));
+
+  StaticSocketDataProvider data1;
+  data1.set_expected_addresses(AddressList(endpoint.ip_endpoints));
+  data1.set_connect_data(MockConnect(SYNCHRONOUS, OK));
+  socket_factory_.AddSocketDataProvider(&data1);
+  // The first connection attempt will fail with a certificate error (simulating
+  // the server providing a certificate that the client does not trust, because,
+  // for example, the server's Trust Anchor IDs advertised in DNS were stale and
+  // it does not actually have a certificate for the trust anchor that the
+  // client selected).
+  SSLSocketDataProvider ssl_fail(ASYNC, ERR_CERT_INVALID);
+  ssl_fail.expected_trust_anchor_ids =
+      std::vector<uint8_t>({0x03, 0x01, 0x02, 0x03, 0x02, 0x04, 0x04});
+  // The server provides a different set of Trust Anchor IDs in the handshake
+  // than were present in the DNS record, simulating e.g. stale data in DNS but
+  // a certificate available on the server that the client might be able to
+  // actually accept.
+  ssl_fail.server_trust_anchor_ids_for_retry =
+      std::vector<std::vector<uint8_t>>({{0x02, 0x02}, {0x05, 0x6}});
+  socket_factory_.AddSSLSocketDataProvider(&ssl_fail);
+  // The second connection attempt again fails with a (different) certificate
+  // error.
+  StaticSocketDataProvider data2;
+  data2.set_expected_addresses(AddressList(endpoint.ip_endpoints));
+  data2.set_connect_data(MockConnect(SYNCHRONOUS, OK));
+  socket_factory_.AddSocketDataProvider(&data2);
+  SSLSocketDataProvider ssl_fail2(ASYNC, ERR_CERT_AUTHORITY_INVALID);
+  ssl_fail2.expected_trust_anchor_ids =
+      std::vector<uint8_t>({0x02, 0x02, 0x02});
+  ssl_fail2.server_trust_anchor_ids_for_retry =
+      std::vector<std::vector<uint8_t>>({{0x04, 0x04}, {0x05, 0x6}});
+  socket_factory_.AddSSLSocketDataProvider(&ssl_fail2);
+  // There should be no third attempt.
+
+  TestConnectJobDelegate test_delegate(
+      TestConnectJobDelegate::SocketExpected::ALWAYS);
+  std::unique_ptr<ConnectJob> ssl_connect_job =
+      CreateConnectJob(&test_delegate, ProxyChain::Direct(), MEDIUM);
+  EXPECT_THAT(ssl_connect_job->Connect(), test::IsError(ERR_IO_PENDING));
+  EXPECT_THAT(test_delegate.WaitForResult(),
+              test::IsError(ERR_CERT_AUTHORITY_INVALID));
+}
+
+// Tests that when `SSLConnectJob` retries due to an error after sending Trust
+// Anchor IDs, it reuses the same endpoint on the retry.
+TEST_F(SSLConnectJobTest, TrustAnchorIDsRetryUsesSameEndpoint) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(features::kTLSTrustAnchorIDs);
+
+  SSLContextConfig config;
+  config.trust_anchor_ids = {{0x01, 0x02, 0x03}, {0x02, 0x02}, {0x04, 0x04}};
+  ssl_config_service_->UpdateSSLConfigAndNotify(config);
+
+  // Configure two HTTPS RR routes, to test the retry uses the correct one.
+  HostResolverEndpointResult endpoint1, endpoint2;
+  endpoint1.ip_endpoints = {IPEndPoint(ParseIP("1::"), 8441)};
+  endpoint1.metadata.trust_anchor_ids = {
+      {0x01, 0x02, 0x03}, {0x04, 0x04}, {0x05, 0x05, 0x05}};
+  endpoint2.ip_endpoints = {IPEndPoint(ParseIP("2::"), 8442)};
+  endpoint2.metadata.trust_anchor_ids = {{0x04, 0x04}};
+  host_resolver_.rules()->AddRule(
+      "host", MockHostResolverBase::RuleResolver::RuleResult(
+                  std::vector{endpoint1, endpoint2}));
+
+  // The first connection attempt will be to `endpoint1`, which will fail.
+  StaticSocketDataProvider data1;
+  data1.set_expected_addresses(AddressList(endpoint1.ip_endpoints));
+  data1.set_connect_data(MockConnect(SYNCHRONOUS, ERR_CONNECTION_REFUSED));
+  socket_factory_.AddSocketDataProvider(&data1);
+  // The second connection attempt will be to `endpoint2`, which will succeed.
+  StaticSocketDataProvider data2;
+  data2.set_expected_addresses(AddressList(endpoint2.ip_endpoints));
+  data2.set_connect_data(MockConnect(SYNCHRONOUS, OK));
+  socket_factory_.AddSocketDataProvider(&data2);
+  // The handshake will then fail, but then provide up-to-date Trust Anchor IDs.
+  SSLSocketDataProvider ssl2(ASYNC, ERR_CERT_AUTHORITY_INVALID);
+  ssl2.expected_trust_anchor_ids = std::vector<uint8_t>({0x02, 0x04, 0x04});
+  ssl2.server_trust_anchor_ids_for_retry = {{0x01, 0x02, 0x03}};
+  socket_factory_.AddSSLSocketDataProvider(&ssl2);
+  // The third connection attempt should skip `endpoint1` and retry with only
+  // `endpoint2`.
+  StaticSocketDataProvider data3;
+  data3.set_expected_addresses(AddressList(endpoint2.ip_endpoints));
+  data3.set_connect_data(MockConnect(SYNCHRONOUS, OK));
+  socket_factory_.AddSocketDataProvider(&data3);
+  // The handshake should use the Trust Anchor IDs that the server provided in
+  // the handshake.
+  SSLSocketDataProvider ssl3(ASYNC, OK);
+  ssl3.expected_trust_anchor_ids =
+      std::vector<uint8_t>({0x03, 0x01, 0x02, 0x03});
+  socket_factory_.AddSSLSocketDataProvider(&ssl3);
+
+  // The connection should ultimately succeed.
+  TestConnectJobDelegate test_delegate;
+  std::unique_ptr<ConnectJob> ssl_connect_job =
+      CreateConnectJob(&test_delegate, ProxyChain::Direct(), MEDIUM);
+  EXPECT_THAT(ssl_connect_job->Connect(), test::IsError(ERR_IO_PENDING));
+  EXPECT_THAT(test_delegate.WaitForResult(), test::IsOk());
+}
+
 // Test that `SSLConnectJob` passes the ECHConfigList from DNS to
 // `SSLClientSocket`.
 TEST_F(SSLConnectJobTest, EncryptedClientHello) {
diff --git a/services/network/BUILD.gn b/services/network/BUILD.gn
index 2df8fd7..7afc5774 100644
--- a/services/network/BUILD.gn
+++ b/services/network/BUILD.gn
@@ -147,6 +147,12 @@
     "restricted_cookie_manager.h",
     "restricted_udp_socket.cc",
     "restricted_udp_socket.h",
+    "scheduler/network_service_scheduler.cc",
+    "scheduler/network_service_scheduler.h",
+    "scheduler/network_service_task_priority.cc",
+    "scheduler/network_service_task_priority.h",
+    "scheduler/network_service_task_queues.cc",
+    "scheduler/network_service_task_queues.h",
     "sec_header_helpers.cc",
     "sec_header_helpers.h",
     "session_cleanup_cookie_store.cc",
@@ -478,6 +484,8 @@
     "resource_scheduler/resource_scheduler_params_manager_unittest.cc",
     "resource_scheduler/resource_scheduler_unittest.cc",
     "restricted_cookie_manager_unittest.cc",
+    "scheduler/network_service_scheduler_unittest.cc",
+    "scheduler/network_service_task_queues_unittest.cc",
     "sec_header_helpers_unittest.cc",
     "session_cleanup_cookie_store_unittest.cc",
     "shared_dictionary/shared_dictionary_data_pipe_writer_unittest.cc",
diff --git a/services/network/network_service.cc b/services/network/network_service.cc
index 4cb2af3..515023b 100644
--- a/services/network/network_service.cc
+++ b/services/network/network_service.cc
@@ -96,6 +96,7 @@
 #include "services/network/public/mojom/network_service_test.mojom.h"
 #include "services/network/public/mojom/system_dns_resolution.mojom-forward.h"
 #include "services/network/restricted_cookie_manager.h"
+#include "services/network/scheduler/network_service_scheduler.h"
 #include "services/network/tpcd/metadata/manager.h"
 #include "services/network/url_loader.h"
 
@@ -380,6 +381,11 @@
   DCHECK(!g_network_service);
   g_network_service = this;
 
+  if (base::FeatureList::IsEnabled(features::kNetworkServiceScheduler)) {
+    scheduler_ = std::make_unique<NetworkServiceScheduler>();
+    scheduler_->SetUpNetTaskRunners();
+  }
+
   ContentDecodingInterceptor::SetIsNetworkServiceRunningInTheCurrentProcess(
       true, {});
 
diff --git a/services/network/network_service.h b/services/network/network_service.h
index ad40223..62a5fc6 100644
--- a/services/network/network_service.h
+++ b/services/network/network_service.h
@@ -89,6 +89,7 @@
 class NetLogProxySink;
 class NetworkContext;
 class NetworkService;
+class NetworkServiceScheduler;
 class SCTAuditingCache;
 
 class COMPONENT_EXPORT(NETWORK_SERVICE) NetworkService
@@ -558,6 +559,9 @@
 
   std::unique_ptr<network::tpcd::metadata::Manager> tpcd_metadata_manager_;
 
+  // An experimental task scheduler for NetworkService.
+  std::unique_ptr<NetworkServiceScheduler> scheduler_;
+
   bool exclusive_cookie_database_locking_ = true;
   base::WeakPtrFactory<NetworkService> weak_factory_{this};
 };
diff --git a/services/network/public/cpp/features.cc b/services/network/public/cpp/features.cc
index 742bb5aa..3bb7707f 100644
--- a/services/network/public/cpp/features.cc
+++ b/services/network/public/cpp/features.cc
@@ -617,4 +617,8 @@
                    /*name=*/"max_size",
                    1'000'000);
 
+BASE_FEATURE(kNetworkServiceScheduler,
+             "NetworkServiceScheduler",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 }  // namespace network::features
diff --git a/services/network/public/cpp/features.h b/services/network/public/cpp/features.h
index 1a9ec56..a92060e 100644
--- a/services/network/public/cpp/features.h
+++ b/services/network/public/cpp/features.h
@@ -340,6 +340,11 @@
 COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES)
 BASE_DECLARE_FEATURE_PARAM(size_t, kSharedDictionaryCacheMaxSizeBytes);
 
+// When enabled, Network Service Scheduler is enabled on the Network
+// Service's IO Thread.
+COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES)
+BASE_DECLARE_FEATURE(kNetworkServiceScheduler);
+
 }  // namespace network::features
 
 #endif  // SERVICES_NETWORK_PUBLIC_CPP_FEATURES_H_
diff --git a/services/network/scheduler/OWNERS b/services/network/scheduler/OWNERS
new file mode 100644
index 0000000..0121bbf
--- /dev/null
+++ b/services/network/scheduler/OWNERS
@@ -0,0 +1 @@
+hayato@chromium.org
diff --git a/services/network/scheduler/README.md b/services/network/scheduler/README.md
new file mode 100644
index 0000000..85dfd16a
--- /dev/null
+++ b/services/network/scheduler/README.md
@@ -0,0 +1,33 @@
+# Network Service Scheduler
+
+This directory contains the implementation for an experimental task scheduler on
+Chromium's Network Service.
+
+The scheduler introduces a `base::sequence_manager::SequenceManager` configured
+with multiple prioritized task queues. The primary goal is to allow
+high-priority network tasks (e.g., those critical for navigation) to execute
+with precedence over lower-priority tasks, aiming to improve user-perceived
+performance metrics like FCP and LCP.
+
+## Design
+
+For a detailed explanation of the design, motivations, and implementation plan,
+please refer to the design document:
+[go/task-scheduler-in-net](http://go/task-scheduler-in-net) (Google internal)
+
+## Overview
+
+The core mechanism involves:
+
+1. Initializing a `SequenceManager` on the Network Service Thread.
+2. Creating at least two `TaskQueue`s: one for high-priority tasks and one for
+   default-priority tasks.
+3. Exposing `TaskRunner`s associated with these queues.
+4. Modifying relevant `PostTask` call sites in `//net` and `//services/network`
+   to route tasks to the appropriate `TaskRunner` based on the task's conceptual
+   priority (often derived from `net::RequestPriority`).
+
+## Status
+
+This feature is currently experimental and under development. Its impact is
+being evaluated via diagnostic metrics and Finch experiments.
diff --git a/services/network/scheduler/network_service_scheduler.cc b/services/network/scheduler/network_service_scheduler.cc
new file mode 100644
index 0000000..6eabcac
--- /dev/null
+++ b/services/network/scheduler/network_service_scheduler.cc
@@ -0,0 +1,86 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/network/scheduler/network_service_scheduler.h"
+
+#include "base/task/sequence_manager/sequence_manager.h"
+#include "base/task/sequence_manager/task_queue.h"
+#include "net/base/task/task_runner.h"
+#include "services/network/scheduler/network_service_task_priority.h"
+
+namespace network {
+
+// The NetworkServiceScheduler's lifetime is coupled with the Network Service
+// itself. Therefore, explicit cleanup of resources like
+// `net::internal::TaskRunnerGlobals` (for non-test scenarios) is not required
+// here as they are managed elsewhere or their lifetime is tied to the process.
+//
+// For testing scenarios using `SetUpNetTaskRunnersForTesting`, the original
+// high-priority task runner is restored.
+NetworkServiceScheduler::~NetworkServiceScheduler() {
+  if (original_high_priority_task_runner_for_testing_.has_value()) {
+    net::internal::GetTaskRunnerGlobals().high_priority_task_runner =
+        *original_high_priority_task_runner_for_testing_;
+  }
+}
+
+NetworkServiceScheduler::NetworkServiceScheduler()
+    : sequence_manager_(
+          base::sequence_manager::CreateSequenceManagerOnCurrentThread(
+              base::sequence_manager::SequenceManager::Settings::Builder()
+                  .SetCanRunTasksByBatches(true)
+                  .SetPrioritySettings(
+                      internal::CreateNetworkServiceTaskPrioritySettings())
+                  .SetShouldSampleCPUTime(true)
+                  .Build())),
+      task_queues_(sequence_manager_.get()) {
+  CHECK_EQ(static_cast<size_t>(sequence_manager_->GetPriorityCount()),
+           static_cast<size_t>(
+               internal::NetworkServiceTaskPriority::kPriorityCount));
+
+  // Set a handler to be called upon completion of each task.
+  task_queues_.SetOnTaskCompletedHandler(base::BindRepeating(
+      &NetworkServiceScheduler::OnTaskCompleted, base::Unretained(this)));
+
+  // Enable crash keys for the sequence manager to help debug scheduler related
+  // crashes.
+  sequence_manager_->EnableCrashKeys("network_service_scheduler_async_stack");
+
+  // Set the default task runner for the current thread.
+  sequence_manager_->SetDefaultTaskRunner(task_queues_.GetDefaultTaskRunner());
+}
+
+void NetworkServiceScheduler::OnTaskCompleted(
+    const base::sequence_manager::Task& task,
+    base::sequence_manager::TaskQueue::TaskTiming* task_timing,
+    base::LazyNow* lazy_now) {
+  // Records the end time of the task.
+  task_timing->RecordTaskEnd(lazy_now);
+
+  // Records CPU usage for the completed task.
+  //
+  // Note: Thread time is already subsampled in sequence manager by a factor of
+  // `kTaskSamplingRateForRecordingCPUTime`.
+  task_timing->RecordUmaOnCpuMetrics("NetworkService.Scheduler.IOThread");
+}
+
+void NetworkServiceScheduler::SetUpNetTaskRunners() {
+  net::internal::TaskRunnerGlobals& globals =
+      net::internal::GetTaskRunnerGlobals();
+  globals.high_priority_task_runner = GetTaskRunner(QueueType::kHighPriority);
+}
+
+void NetworkServiceScheduler::SetUpNetTaskRunnersForTesting() {
+  CHECK(!original_high_priority_task_runner_for_testing_.has_value());
+  original_high_priority_task_runner_for_testing_ =
+      net::internal::GetTaskRunnerGlobals().high_priority_task_runner;
+  SetUpNetTaskRunners();
+}
+
+const scoped_refptr<base::SingleThreadTaskRunner>&
+NetworkServiceScheduler::GetTaskRunner(QueueType type) const {
+  return task_queues_.GetTaskRunner(type);
+}
+
+}  // namespace network
diff --git a/services/network/scheduler/network_service_scheduler.h b/services/network/scheduler/network_service_scheduler.h
new file mode 100644
index 0000000..0cfa73e2
--- /dev/null
+++ b/services/network/scheduler/network_service_scheduler.h
@@ -0,0 +1,82 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_NETWORK_SCHEDULER_NETWORK_SERVICE_SCHEDULER_H_
+#define SERVICES_NETWORK_SCHEDULER_NETWORK_SERVICE_SCHEDULER_H_
+
+#include <memory>
+#include <optional>
+
+#include "base/component_export.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/task/sequence_manager/task_queue.h"
+#include "services/network/scheduler/network_service_task_queues.h"
+
+namespace base {
+class LazyNow;
+namespace sequence_manager {
+class SequenceManager;
+struct Task;
+}  // namespace sequence_manager
+}  // namespace base
+
+namespace network {
+
+// Manages the task scheduling for the network service. It sets up a
+// `base::sequence_manager::SequenceManager` on the current thread with specific
+// priorities defined by NetworkServiceTaskPriority` and manages
+// `NetworkServiceTaskQueues` (e.g., default, high priority).
+//
+// This scheduler is responsible for:
+// - Creating and configuring the sequence manager for the network thread.
+// - Providing task runners for different priority levels.
+// - Integrating with `net::GetTaskRunner` by setting up the task runners in
+//   `net::internal::TaskRunnerGlobals`.
+// - Optionally handling task completion notifications for metrics.
+class COMPONENT_EXPORT(NETWORK_SERVICE) NetworkServiceScheduler {
+ public:
+  NetworkServiceScheduler();
+
+  NetworkServiceScheduler(const NetworkServiceScheduler&) = delete;
+  NetworkServiceScheduler& operator=(const NetworkServiceScheduler&) = delete;
+
+  ~NetworkServiceScheduler();
+
+  using QueueType = NetworkServiceTaskQueues::QueueType;
+
+  // Returns the task runner for the specified `QueueType`.
+  const scoped_refptr<base::SingleThreadTaskRunner>& GetTaskRunner(
+      QueueType type) const;
+
+  // Sets up the global task runners in `net::internal::TaskRunnerGlobals` so
+  // that `net::GetTaskRunner(net::RequestPriority)` returns the appropriate
+  // task runner managed by this scheduler.
+  void SetUpNetTaskRunners();
+
+  // Similar to `SetUpNetTaskRunners`, but specifically for testing.
+  // It saves the current global high-priority task runner and restores it
+  // when this `NetworkServiceScheduler` instance is destructed.
+  void SetUpNetTaskRunnersForTesting();
+
+ private:
+  // Callback for when a task completes on one of the managed queues.
+  // Used for recording metrics.
+  void OnTaskCompleted(
+      const base::sequence_manager::Task& task,
+      base::sequence_manager::TaskQueue::TaskTiming* task_timing,
+      base::LazyNow* lazy_now);
+
+  std::unique_ptr<base::sequence_manager::SequenceManager> sequence_manager_;
+  NetworkServiceTaskQueues task_queues_;
+
+  // Stores the original global high-priority task runner when
+  // `SetUpNetTaskRunnersForTesting()` is called, so it can be restored on
+  // destruction.
+  std::optional<scoped_refptr<base::SingleThreadTaskRunner>>
+      original_high_priority_task_runner_for_testing_;
+};
+
+}  // namespace network
+
+#endif  // SERVICES_NETWORK_SCHEDULER_NETWORK_SERVICE_SCHEDULER_H_
diff --git a/services/network/scheduler/network_service_scheduler_unittest.cc b/services/network/scheduler/network_service_scheduler_unittest.cc
new file mode 100644
index 0000000..43f0a94
--- /dev/null
+++ b/services/network/scheduler/network_service_scheduler_unittest.cc
@@ -0,0 +1,165 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/network/scheduler/network_service_scheduler.h"
+
+#include "base/run_loop.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/test/mock_callback.h"
+#include "base/test/task_environment.h"
+#include "net/base/task/task_runner.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace network {
+namespace {
+
+using QueueType = NetworkServiceTaskQueues::QueueType;
+
+using StrictMockTask =
+    testing::StrictMock<base::MockCallback<base::RepeatingCallback<void()>>>;
+
+class NetworkServiceSchedulerTest : public testing::Test {
+ protected:
+  base::test::TaskEnvironment task_environment;
+};
+
+// Tests that tasks posted to the default task runner provided by the scheduler
+// are executed in order. Also verifies that the scheduler sets the current
+// thread's default task runner.
+TEST_F(NetworkServiceSchedulerTest, DefaultQueuePostsTasksInOrder) {
+  NetworkServiceScheduler scheduler;
+
+  scoped_refptr<base::SingleThreadTaskRunner> tq =
+      scheduler.GetTaskRunner(QueueType::kDefault);
+  EXPECT_EQ(tq.get(), base::SingleThreadTaskRunner::GetCurrentDefault());
+
+  StrictMockTask task_1;
+  StrictMockTask task_2;
+
+  testing::InSequence s;
+  EXPECT_CALL(task_1, Run);
+  EXPECT_CALL(task_2, Run);
+
+  base::RunLoop run_loop;
+  tq->PostTask(FROM_HERE, task_1.Get());
+  tq->PostTask(FROM_HERE, task_2.Get());
+  tq->PostTask(FROM_HERE, run_loop.QuitClosure());
+  run_loop.Run();
+}
+
+// Tests that tasks posted to different priority queues (default and high
+// priority) are executed according to their priority, with high priority tasks
+// running before default priority tasks.
+TEST_F(NetworkServiceSchedulerTest, MultipleQueuesHighPriorityTaskRunsFirst) {
+  NetworkServiceScheduler scheduler;
+
+  scoped_refptr<base::SingleThreadTaskRunner> tq1 =
+      scheduler.GetTaskRunner(QueueType::kDefault);
+  scoped_refptr<base::SingleThreadTaskRunner> tq2 =
+      scheduler.GetTaskRunner(QueueType::kHighPriority);
+
+  StrictMockTask task_1;
+  StrictMockTask task_2;
+
+  testing::InSequence s;
+  // High priority task runs at first.
+  EXPECT_CALL(task_2, Run);
+  // Default priority task runs after a high priority task runs.
+  EXPECT_CALL(task_1, Run);
+
+  base::RunLoop run_loop;
+  tq1->PostTask(FROM_HERE, task_1.Get());
+  tq2->PostTask(FROM_HERE, task_2.Get());
+  tq1->PostTask(FROM_HERE, run_loop.QuitClosure());
+  run_loop.Run();
+}
+
+// Tests that tasks posted to the current thread's default task runner *before*
+// the NetworkServiceScheduler is instantiated are still executed
+// correctly once the scheduler is up and running.
+TEST_F(NetworkServiceSchedulerTest,
+       PendingTasksRunAfterSchedulerInitialization) {
+  StrictMockTask task_1;
+  StrictMockTask task_2;
+
+  testing::InSequence s;
+  EXPECT_CALL(task_1, Run);
+  EXPECT_CALL(task_2, Run);
+
+  base::RunLoop run_loop;
+
+  // Post a task before NetworkServiceScheduler is instantiated.
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
+                                                              task_1.Get());
+  NetworkServiceScheduler scheduler;
+  scoped_refptr<base::SingleThreadTaskRunner> tq =
+      scheduler.GetTaskRunner(QueueType::kDefault);
+  tq->PostTask(FROM_HERE, task_2.Get());
+  tq->PostTask(FROM_HERE, run_loop.QuitClosure());
+  run_loop.Run();
+}
+
+// Tests a combination of pending tasks (posted before scheduler instantiation)
+// and tasks posted to different priority queues after the scheduler is
+// instantiated. Verifies that all tasks run in the correct order based on
+// posting time and priority.
+TEST_F(NetworkServiceSchedulerTest, PendingAndPostedTasksRunInCorrectOrder) {
+  StrictMockTask task_1;
+  StrictMockTask task_2;
+  StrictMockTask task_3;
+  StrictMockTask task_4;
+
+  testing::InSequence s;
+  // Prior tasks run at first.
+  EXPECT_CALL(task_1, Run);
+  EXPECT_CALL(task_2, Run);
+  // High priority task runs.
+  EXPECT_CALL(task_4, Run);
+  // Default priority task runs after a high priority task runs.
+  EXPECT_CALL(task_3, Run);
+
+  base::RunLoop run_loop;
+
+  // Post tasks before NetworkServiceScheduler is instantiated.
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
+                                                              task_1.Get());
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
+                                                              task_2.Get());
+
+  NetworkServiceScheduler scheduler;
+  scoped_refptr<base::SingleThreadTaskRunner> tq_default =
+      scheduler.GetTaskRunner(QueueType::kDefault);
+  scoped_refptr<base::SingleThreadTaskRunner> tq_high =
+      scheduler.GetTaskRunner(QueueType::kHighPriority);
+  tq_default->PostTask(FROM_HERE, task_3.Get());
+  tq_high->PostTask(FROM_HERE, task_4.Get());
+  tq_default->PostTask(FROM_HERE, run_loop.QuitClosure());
+  run_loop.Run();
+}
+
+// Tests the `SetUpNetTaskRunners`.
+// Verifies that after calling `SetupNetTaskRunners`, `net::GetTaskRunner`
+// returns the appropriate task runners managed by the scheduler.
+TEST_F(NetworkServiceSchedulerTest,
+       SetUpNetTaskRunnersIntegratesWithNetGetTaskRunner) {
+  NetworkServiceScheduler scheduler;
+  scheduler.SetUpNetTaskRunnersForTesting();
+
+  auto scheduler_high_priority_runner =
+      scheduler.GetTaskRunner(QueueType::kHighPriority);
+  auto net_highest_priority_runner = net::GetTaskRunner(net::HIGHEST);
+
+  EXPECT_EQ(net_highest_priority_runner.get(),
+            scheduler_high_priority_runner.get());
+
+  auto scheduler_default_runner = scheduler.GetTaskRunner(QueueType::kDefault);
+  auto net_medium_priority_runner = net::GetTaskRunner(net::MEDIUM);
+  EXPECT_EQ(net_medium_priority_runner.get(), scheduler_default_runner.get());
+  EXPECT_EQ(net_medium_priority_runner.get(),
+            base::SingleThreadTaskRunner::GetCurrentDefault().get());
+}
+
+}  // namespace
+}  // namespace network
diff --git a/services/network/scheduler/network_service_task_priority.cc b/services/network/scheduler/network_service_task_priority.cc
new file mode 100644
index 0000000..5c513d7
--- /dev/null
+++ b/services/network/scheduler/network_service_task_priority.cc
@@ -0,0 +1,47 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/network/scheduler/network_service_task_priority.h"
+
+#include "base/notreached.h"
+#include "base/tracing/protos/chrome_track_event.pbzero.h"
+
+namespace network::internal {
+
+namespace {
+
+using ProtoPriority = perfetto::protos::pbzero::SequenceManagerTask::Priority;
+
+ProtoPriority ToProtoPriority(NetworkServiceTaskPriority priority) {
+  switch (priority) {
+    case NetworkServiceTaskPriority::kHighPriority:
+      return ProtoPriority::HIGHEST_PRIORITY;
+    case NetworkServiceTaskPriority::kNormalPriority:
+      return ProtoPriority::NORMAL_PRIORITY;
+    case NetworkServiceTaskPriority::kPriorityCount:
+      NOTREACHED();
+  }
+  NOTREACHED();
+}
+
+ProtoPriority TaskPriorityToProto(
+    base::sequence_manager::TaskQueue::QueuePriority priority) {
+  CHECK_LT(static_cast<size_t>(priority),
+           static_cast<size_t>(NetworkServiceTaskPriority::kPriorityCount));
+  return ToProtoPriority(static_cast<NetworkServiceTaskPriority>(priority));
+}
+
+}  // namespace
+
+base::sequence_manager::SequenceManager::PrioritySettings
+CreateNetworkServiceTaskPrioritySettings() {
+  using base::sequence_manager::TaskQueue;
+  base::sequence_manager::SequenceManager::PrioritySettings settings(
+      NetworkServiceTaskPriority::kPriorityCount,
+      NetworkServiceTaskPriority::kNormalPriority);
+  settings.SetProtoPriorityConverter(&TaskPriorityToProto);
+  return settings;
+}
+
+}  // namespace network::internal
diff --git a/services/network/scheduler/network_service_task_priority.h b/services/network/scheduler/network_service_task_priority.h
new file mode 100644
index 0000000..9b795161
--- /dev/null
+++ b/services/network/scheduler/network_service_task_priority.h
@@ -0,0 +1,33 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_NETWORK_SCHEDULER_NETWORK_SERVICE_TASK_PRIORITY_H_
+#define SERVICES_NETWORK_SCHEDULER_NETWORK_SERVICE_TASK_PRIORITY_H_
+
+#include "base/component_export.h"
+#include "base/task/sequence_manager/sequence_manager.h"
+#include "base/task/sequence_manager/task_queue.h"
+
+namespace network::internal {
+
+// Defines the set of task priorities for the Network Service. These priorities
+// are used by the `SequenceManager` to schedule tasks.
+enum class NetworkServiceTaskPriority : base::sequence_manager::TaskQueue::
+    QueuePriority {
+      // Priorities are in descending order.
+      kHighPriority = 0,
+      kNormalPriority = 1,
+      kDefaultPriority = kNormalPriority,
+      // Must be the last entry.
+      kPriorityCount = 2,
+    };
+
+// Creates and returns the priority settings for the Network Service's
+// `SequenceManager`.
+COMPONENT_EXPORT(NETWORK_SERVICE)
+base::sequence_manager::SequenceManager::PrioritySettings
+CreateNetworkServiceTaskPrioritySettings();
+
+}  // namespace network::internal
+#endif  // SERVICES_NETWORK_SCHEDULER_NETWORK_SERVICE_TASK_PRIORITY_H_
diff --git a/services/network/scheduler/network_service_task_queues.cc b/services/network/scheduler/network_service_task_queues.cc
new file mode 100644
index 0000000..aec6388
--- /dev/null
+++ b/services/network/scheduler/network_service_task_queues.cc
@@ -0,0 +1,69 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/network/scheduler/network_service_task_queues.h"
+
+#include "base/notreached.h"
+#include "base/task/sequence_manager/sequence_manager.h"
+#include "services/network/scheduler/network_service_task_priority.h"
+
+namespace network {
+namespace {
+
+using NetworkServiceTaskPriority =
+    ::network::internal::NetworkServiceTaskPriority;
+using QueueName = ::perfetto::protos::pbzero::SequenceManagerTask::QueueName;
+
+QueueName GetTaskQueueName(NetworkServiceTaskQueues::QueueType queue_type) {
+  switch (queue_type) {
+    case NetworkServiceTaskQueues::QueueType::kDefault:
+      return QueueName::NETWORK_SERVICE_THREAD_DEFAULT_TQ;
+    case NetworkServiceTaskQueues::QueueType::kHighPriority:
+      return QueueName::NETWORK_SERVICE_THREAD_HIGH_TQ;
+    default:
+      NOTREACHED();
+  }
+}
+
+}  // namespace
+
+NetworkServiceTaskQueues::NetworkServiceTaskQueues(
+    base::sequence_manager::SequenceManager* sequence_manager) {
+  CreateTaskQueues(sequence_manager);
+  CreateNetworkServiceTaskRunners();
+}
+
+NetworkServiceTaskQueues::~NetworkServiceTaskQueues() = default;
+
+void NetworkServiceTaskQueues::CreateTaskQueues(
+    base::sequence_manager::SequenceManager* sequence_manager) {
+  for (size_t i = 0; i < task_queues_.size(); ++i) {
+    task_queues_[i] = sequence_manager->CreateTaskQueue(
+        base::sequence_manager::TaskQueue::Spec(
+            GetTaskQueueName(static_cast<QueueType>(i))));
+  }
+
+  // Default queue
+  GetTaskQueue(QueueType::kDefault)
+      ->SetQueuePriority(NetworkServiceTaskPriority::kDefaultPriority);
+
+  // High Priority queue
+  GetTaskQueue(QueueType::kHighPriority)
+      ->SetQueuePriority(NetworkServiceTaskPriority::kHighPriority);
+}
+
+void NetworkServiceTaskQueues::CreateNetworkServiceTaskRunners() {
+  for (size_t i = 0; i < task_queues_.size(); ++i) {
+    task_runners_[i] = task_queues_[i]->task_runner();
+  }
+}
+
+void NetworkServiceTaskQueues::SetOnTaskCompletedHandler(
+    base::sequence_manager::TaskQueue::OnTaskCompletedHandler handler) {
+  for (auto& queue : task_queues_) {
+    queue->SetOnTaskCompletedHandler(handler);
+  }
+}
+
+}  // namespace network
diff --git a/services/network/scheduler/network_service_task_queues.h b/services/network/scheduler/network_service_task_queues.h
new file mode 100644
index 0000000..db349cde
--- /dev/null
+++ b/services/network/scheduler/network_service_task_queues.h
@@ -0,0 +1,103 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_NETWORK_SCHEDULER_NETWORK_SERVICE_TASK_QUEUES_H_
+#define SERVICES_NETWORK_SCHEDULER_NETWORK_SERVICE_TASK_QUEUES_H_
+
+#include <array>
+
+#include "base/component_export.h"
+#include "base/functional/callback_helpers.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/task/sequence_manager/task_queue.h"
+#include "base/task/single_thread_task_runner.h"
+
+namespace base::sequence_manager {
+class SequenceManager;
+}  // namespace base::sequence_manager
+
+namespace network {
+
+// Task queues for the network service thread.
+//
+// Instances must be created and destroyed on the same thread as the
+// underlying SequenceManager and instances are not allowed to outlive this
+// SequenceManager. All methods of this class must be called from the
+// associated thread unless noted otherwise.
+//
+// This class creates and manages a set of `base::sequence_manager::TaskQueue`s
+// with different priorities for the network service thread. It provides
+// `base::SingleThreadTaskRunner`s for each of these queues.
+class COMPONENT_EXPORT(NETWORK_SERVICE) NetworkServiceTaskQueues {
+ public:
+  // Defines the types of task queues available.
+  enum class QueueType {
+    kDefault,
+    kHighPriority,
+    kMaxValue = kHighPriority,
+  };
+
+  // Creates task queues and task runners using the provided `sequence_manager`.
+  // The `sequence_manager` must outlive this `NetworkServiceTaskQueues`
+  // instance.
+  explicit NetworkServiceTaskQueues(
+      base::sequence_manager::SequenceManager* sequence_manager);
+
+  // Destroys all managed task queues.
+  ~NetworkServiceTaskQueues();
+
+  // Returns the underlying `TaskQueue` for the default priority.
+  base::sequence_manager::TaskQueue* GetDefaultTaskQueue() const {
+    return GetTaskQueue(QueueType::kDefault);
+  }
+
+  // Returns the task runner that should be returned by
+  // SingleThreadTaskRunner::GetCurrentDefault().
+  // This is typically the task runner for the `QueueType::kDefault`.
+  const scoped_refptr<base::SingleThreadTaskRunner>& GetDefaultTaskRunner()
+      const {
+    return GetTaskRunner(QueueType::kDefault);
+  }
+
+  // Returns the task runner for the specified `QueueType`.
+  const scoped_refptr<base::SingleThreadTaskRunner>& GetTaskRunner(
+      QueueType type) const {
+    return task_runners_[static_cast<size_t>(type)];
+  }
+
+  // Sets a handler to be called when a task is completed on any of the
+  // managed task queues.
+  void SetOnTaskCompletedHandler(
+      base::sequence_manager::TaskQueue::OnTaskCompletedHandler handler);
+
+ private:
+  static constexpr size_t kNumQueueTypes =
+      static_cast<size_t>(QueueType::kMaxValue) + 1;
+
+  // Helper to get the underlying `TaskQueue` for a given `QueueType`.
+  base::sequence_manager::TaskQueue* GetTaskQueue(QueueType type) const {
+    return task_queues_[static_cast<size_t>(type)].get();
+  }
+
+  void CreateTaskQueues(
+      base::sequence_manager::SequenceManager* sequence_manager);
+
+  void CreateNetworkServiceTaskRunners();
+
+  // Array of handles to the underlying task queues.
+  // The index corresponds to the integer value of `QueueType`.
+  std::array<base::sequence_manager::TaskQueue::Handle, kNumQueueTypes>
+      task_queues_;
+
+  // Array of task runners, one for each `TaskQueue` in `task_queues_`. There is
+  // a 1:1 correspondence: `task_runners_[i]` is the runner for
+  // `task_queues_[i]`.
+  std::array<scoped_refptr<base::SingleThreadTaskRunner>, kNumQueueTypes>
+      task_runners_;
+};
+
+}  // namespace network
+
+#endif  // SERVICES_NETWORK_SCHEDULER_NETWORK_SERVICE_TASK_QUEUES_H_
diff --git a/services/network/scheduler/network_service_task_queues_unittest.cc b/services/network/scheduler/network_service_task_queues_unittest.cc
new file mode 100644
index 0000000..7f97a42
--- /dev/null
+++ b/services/network/scheduler/network_service_task_queues_unittest.cc
@@ -0,0 +1,92 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/network/scheduler/network_service_task_queues.h"
+
+#include "base/message_loop/message_pump.h"
+#include "base/message_loop/message_pump_type.h"
+#include "base/run_loop.h"
+#include "base/task/sequence_manager/sequence_manager.h"
+#include "base/test/bind.h"
+#include "base/test/mock_callback.h"
+#include "services/network/scheduler/network_service_task_priority.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace network {
+namespace {
+
+using StrictMockTask =
+    testing::StrictMock<base::MockCallback<base::RepeatingCallback<void()>>>;
+
+using QueueType = NetworkServiceTaskQueues::QueueType;
+
+// Test fixture for NetworkServiceTaskQueues. Sets up a SequenceManager and
+// NetworkServiceTaskQueues instance for each test.
+class NetworkServiceTaskQueuesTest : public testing::Test {
+ protected:
+  NetworkServiceTaskQueuesTest()
+      : sequence_manager_(
+            base::sequence_manager::
+                CreateSequenceManagerOnCurrentThreadWithPump(
+                    base::MessagePump::Create(base::MessagePumpType::DEFAULT),
+                    base::sequence_manager::SequenceManager::Settings::Builder()
+                        .SetPrioritySettings(
+                            internal::
+                                CreateNetworkServiceTaskPrioritySettings())
+                        .Build())),
+        queues_(sequence_manager_.get()) {
+    sequence_manager_->SetDefaultTaskRunner(queues_.GetDefaultTaskRunner());
+  }
+
+  std::unique_ptr<base::sequence_manager::SequenceManager> sequence_manager_;
+  NetworkServiceTaskQueues queues_;
+};
+
+// Tests that tasks posted to the default task runner are executed in order.
+TEST_F(NetworkServiceTaskQueuesTest, SimplePosting) {
+  scoped_refptr<base::SingleThreadTaskRunner> tq =
+      queues_.GetDefaultTaskRunner();
+
+  StrictMockTask task_1;
+  StrictMockTask task_2;
+
+  testing::InSequence s;
+  EXPECT_CALL(task_1, Run);
+  EXPECT_CALL(task_2, Run);
+
+  base::RunLoop run_loop;
+  tq->PostTask(FROM_HERE, task_1.Get());
+  tq->PostTask(FROM_HERE, task_2.Get());
+  tq->PostTask(FROM_HERE, run_loop.QuitClosure());
+  run_loop.Run();
+}
+
+// Tests that tasks posted to different priority queues are executed according
+// to their priority (high priority first, then default).
+TEST_F(NetworkServiceTaskQueuesTest, PostingToMultipleQueues) {
+  scoped_refptr<base::SingleThreadTaskRunner> tq1 =
+      queues_.GetDefaultTaskRunner();
+  scoped_refptr<base::SingleThreadTaskRunner> tq2 =
+      queues_.GetTaskRunner(QueueType::kHighPriority);
+
+  StrictMockTask task_1;
+  StrictMockTask task_2;
+
+  testing::InSequence s;
+  EXPECT_CALL(task_2, Run);
+  EXPECT_CALL(task_1, Run);
+
+  base::RunLoop run_loop;
+
+  tq1->PostTask(FROM_HERE, task_1.Get());
+  tq2->PostTask(FROM_HERE, task_2.Get());
+
+  tq1->PostTask(FROM_HERE, run_loop.QuitClosure());
+
+  run_loop.Run();
+}
+
+}  // namespace
+}  // namespace network
diff --git a/services/passage_embeddings/BUILD.gn b/services/passage_embeddings/BUILD.gn
index 6d0dd27..478c34770 100644
--- a/services/passage_embeddings/BUILD.gn
+++ b/services/passage_embeddings/BUILD.gn
@@ -10,6 +10,8 @@
     "passage_embeddings_service.cc",
     "passage_embeddings_service.h",
   ]
+  public_deps = [ "//mojo/public/cpp/bindings" ]
+  deps = [ "//services/passage_embeddings/public/mojom" ]
 
   if (build_with_tflite_lib) {
     sources += [
@@ -20,12 +22,6 @@
       "passage_embeddings_op_resolver.cc",
       "passage_embeddings_op_resolver.h",
     ]
-  }
-
-  public_deps = [ "//mojo/public/cpp/bindings" ]
-  deps = [ "//services/passage_embeddings/public/mojom" ]
-
-  if (build_with_tflite_lib) {
     public_deps += [
       "//third_party/sentencepiece:sentencepiece",
       "//third_party/sentencepiece:sentencepiece_proto",
@@ -34,14 +30,10 @@
       "//third_party/tflite_support",
       "//third_party/tflite_support:tflite_support_proto",
     ]
-
-    deps += [ "//services/on_device_model:ml_internal_buildflags" ]
-  }
-
-  if (build_with_tflite_lib) {
     deps += [
       "//components/optimization_guide:machine_learning_tflite_buildflags",
-      "//components/optimization_guide/core:machine_learning",
+      "//components/optimization_guide/core/inference:op_resolver",
+      "//services/on_device_model:ml_internal_buildflags",
     ]
   }
 
@@ -52,7 +44,6 @@
 
 source_set("tests") {
   testonly = true
-
   sources = [ "passage_embeddings_service_unittest.cc" ]
   deps = [
     ":passage_embeddings",
diff --git a/services/webnn/ort/context_impl_ort.cc b/services/webnn/ort/context_impl_ort.cc
index 07f4cba..056131a 100644
--- a/services/webnn/ort/context_impl_ort.cc
+++ b/services/webnn/ort/context_impl_ort.cc
@@ -171,13 +171,13 @@
        /*scatter_nd_updates=*/{},
        /*sigmoid_input=*/{DataTypeConstraint::kFloat16To32, kMaxRank},
        /*slice_input=*/{},
-       /*softmax_input=*/{},
+       /*softmax_input=*/{DataTypeConstraint::kFloat16To32, kMaxRank},
        /*softplus_input=*/{},
        /*softsign_input=*/{DataTypeConstraint::kFloat16To32, kMaxRank},
        /*split_input=*/{},
        /*tanh_input=*/{DataTypeConstraint::kFloat16To32, kMaxRank},
        /*tile_input=*/{},
-       /*transpose_input=*/{},
+       /*transpose_input=*/{SupportedDataTypes::All(), kMaxRank},
        /*triangular_input=*/{},
        /*where_condition=*/{},
        /*where_value=*/{}});
diff --git a/services/webnn/ort/graph_builder_ort.cc b/services/webnn/ort/graph_builder_ort.cc
index 00daf053..0e64ef4 100644
--- a/services/webnn/ort/graph_builder_ort.cc
+++ b/services/webnn/ort/graph_builder_ort.cc
@@ -53,8 +53,10 @@
 constexpr base::cstring_view kOpTypeHardSwish = "HardSwish";
 constexpr base::cstring_view kOpTypeRelu = "Relu";
 constexpr base::cstring_view kOpTypeSigmoid = "Sigmoid";
+constexpr base::cstring_view kOpTypeSoftmax = "Softmax";
 constexpr base::cstring_view kOpTypeSoftsign = "Softsign";
 constexpr base::cstring_view kOpTypeTanh = "Tanh";
+constexpr base::cstring_view kOpTypeTranspose = "Transpose";
 
 // Pooling operations
 constexpr base::cstring_view kOpTypeAveragePool2d = "AveragePool";
@@ -588,6 +590,44 @@
   model_editor_.AddNode(op_type, node, inputs, outputs, attributes);
 }
 
+void GraphBuilderOrt::AddSoftmaxOperation(const mojom::Softmax& softmax) {
+  const std::string node = GenerateOperationName(softmax.label);
+  const std::string input = GetOperandNameById(softmax.input_operand_id);
+  const std::string output = GetOperandNameById(softmax.output_operand_id);
+
+  CHECK(context_properties_.data_type_limits.softmax_input.Supports(
+      GetOperand(softmax.input_operand_id).descriptor));
+
+  std::array<const char*, 1> inputs = {input.c_str()};
+  std::array<const char*, 1> outputs = {output.c_str()};
+
+  constexpr base::cstring_view kAttrAxis = "axis";
+  std::array<ScopedOrtOpAttr, 1> attributes = {model_editor_.CreateAttribute(
+      kAttrAxis, static_cast<int64_t>(softmax.axis))};
+
+  model_editor_.AddNode(kOpTypeSoftmax, node, inputs, outputs, attributes);
+}
+
+void GraphBuilderOrt::AddTransposeOperation(const mojom::Transpose& transpose) {
+  const std::string node = GenerateOperationName(transpose.label);
+  const std::string input = GetOperandNameById(transpose.input_operand_id);
+  const std::string output = GetOperandNameById(transpose.output_operand_id);
+
+  CHECK(context_properties_.data_type_limits.transpose_input.Supports(
+      GetOperand(transpose.input_operand_id).descriptor));
+
+  std::array<const char*, 1> inputs = {input.c_str()};
+  std::array<const char*, 1> outputs = {output.c_str()};
+
+  constexpr base::cstring_view kAttrPerm = "perm";
+  std::vector<int64_t> perm_value(transpose.permutation.begin(),
+                                  transpose.permutation.end());
+  std::array<ScopedOrtOpAttr, 1> attributes = {
+      model_editor_.CreateAttribute(kAttrPerm, perm_value)};
+
+  model_editor_.AddNode(kOpTypeTranspose, node, inputs, outputs, attributes);
+}
+
 [[nodiscard]] base::expected<std::unique_ptr<ModelEditor::ModelInfo>,
                              mojom::ErrorPtr>
 GraphBuilderOrt::BuildModel() {
@@ -650,6 +690,10 @@
         AddUnaryOperation(*operation->get_sigmoid(), kOpTypeSigmoid);
         break;
       }
+      case mojom::Operation::Tag::kSoftmax: {
+        AddSoftmaxOperation(*operation->get_softmax());
+        break;
+      }
       case mojom::Operation::Tag::kSoftsign: {
         CHECK(data_type_limits.softsign_input.Supports(
             GetOperand(operation->get_softsign()->input_operand_id)
@@ -663,6 +707,10 @@
         AddUnaryOperation(*operation->get_tanh(), kOpTypeTanh);
         break;
       }
+      case mojom::Operation::Tag::kTranspose: {
+        AddTransposeOperation(*operation->get_transpose());
+        break;
+      }
       case mojom::Operation::Tag::kArgMinMax:
       case mojom::Operation::Tag::kBatchNormalization:
       case mojom::Operation::Tag::kConcat:
@@ -694,11 +742,9 @@
       case mojom::Operation::Tag::kScatterElements:
       case mojom::Operation::Tag::kScatterNd:
       case mojom::Operation::Tag::kSlice:
-      case mojom::Operation::Tag::kSoftmax:
       case mojom::Operation::Tag::kSoftplus:
       case mojom::Operation::Tag::kSplit:
       case mojom::Operation::Tag::kTile:
-      case mojom::Operation::Tag::kTranspose:
       case mojom::Operation::Tag::kTriangular:
       case mojom::Operation::Tag::kWhere:
         NOTREACHED() << "[WebNN] Unsupported operation.";
diff --git a/services/webnn/ort/graph_builder_ort.h b/services/webnn/ort/graph_builder_ort.h
index b06bcd2..bffb46c 100644
--- a/services/webnn/ort/graph_builder_ort.h
+++ b/services/webnn/ort/graph_builder_ort.h
@@ -122,6 +122,8 @@
       const mojom::ElementWiseUnary& element_wise_unary);
   void AddGemmOperation(const mojom::Gemm& gemm);
   void AddPool2dOperation(const mojom::Pool2d& pool2d);
+  void AddSoftmaxOperation(const mojom::Softmax& softmax);
+  void AddTransposeOperation(const mojom::Transpose& transpose);
 
   [[nodiscard]] base::expected<std::unique_ptr<ModelEditor::ModelInfo>,
                                mojom::ErrorPtr>
diff --git a/services/webnn/webnn_context_impl.cc b/services/webnn/webnn_context_impl.cc
index e8935f3..1193451 100644
--- a/services/webnn/webnn_context_impl.cc
+++ b/services/webnn/webnn_context_impl.cc
@@ -268,6 +268,12 @@
       .IntersectWith(SupportedRanks::Exactly(4));
   backend_context_properties.data_type_limits.conv_transpose2d_bias.ranks
       .IntersectWith(SupportedRanks::Exactly(1));
+  backend_context_properties.data_type_limits.logical_and_input.data_types
+      .RetainAll(DataTypeConstraint::kUint8);
+  backend_context_properties.data_type_limits.logical_or_input.data_types
+      .RetainAll(DataTypeConstraint::kUint8);
+  backend_context_properties.data_type_limits.logical_xor_input.data_types
+      .RetainAll(DataTypeConstraint::kUint8);
   backend_context_properties.data_type_limits.logical_not_input.data_types
       .RetainAll(DataTypeConstraint::kUint8);
   backend_context_properties.data_type_limits.logical_output.RetainAll(
diff --git a/testing/buildbot/filters/ios.gpu_unittests.filter b/testing/buildbot/filters/ios.gpu_unittests.filter
index 734b65ff..0767d978 100644
--- a/testing/buildbot/filters/ios.gpu_unittests.filter
+++ b/testing/buildbot/filters/ios.gpu_unittests.filter
@@ -11,7 +11,7 @@
 -Service/FeatureInfoTest.InitializeWithANGLE_BGRA8/0
 -Service/FeatureInfoTest.InitializeWithANGLE_BGRA8/1
 
-# TODO(crbug.com/379525871): These tests are failing on the ios-blink-dbg-fyi
+# TODO(crbug.com/379525871): These tests are failing on the ios-blink-rel-fyi
 # bot.
 -ShaderTranslatorTest.GetAttributes
 -ShaderTranslatorTest.GetAttributes
@@ -28,14 +28,14 @@
 -ShaderTranslatorTest.ValidVertexShader
 -ShaderTranslatorTest.ValidVertexShader
 
-# TODO(crbug.com/379525870): These tests are failing on the ios-blink-dbg-fyi
+# TODO(crbug.com/379525870): These tests are failing on the ios-blink-rel-fyi
 # bot.
 -OpenGLESContexts/ShaderTranslatorOutputVersionTest.HasCorrectOutputGLSLVersion/0
 -OpenGLESContexts/ShaderTranslatorOutputVersionTest.HasCorrectOutputGLSLVersion/1
 -OpenGLESContexts/ShaderTranslatorOutputVersionTest.HasCorrectOutputGLSLVersion/2
 -OpenGLESContexts/ShaderTranslatorOutputVersionTest.HasCorrectOutputGLSLVersion/3
 
-# TODO(crbug.com/380308799): These tests are failing on the ios-blink-dbg-fyi
+# TODO(crbug.com/380308799): These tests are failing on the ios-blink-rel-fyi
 # bot.
 -ES3ShaderTranslatorTest.GetInterfaceBlocks
 -ES3ShaderTranslatorTest.InvalidInterfaceBlocks
diff --git a/testing/buildbot/filters/ios.ui_base_unittests.filter b/testing/buildbot/filters/ios.ui_base_unittests.filter
index 4a67fe7..d3c0695 100644
--- a/testing/buildbot/filters/ios.ui_base_unittests.filter
+++ b/testing/buildbot/filters/ios.ui_base_unittests.filter
@@ -4,7 +4,7 @@
 -ClipboardTest/PlatformClipboardTest.BitmapWriteAndPngRead_N32_Premul_2x7
 
 # TODO(crbug.com/40276835): A timeout happens on these tests since
-# https://ci.chromium.org/ui/p/chromium/builders/ci/ios-blink-dbg-fyi/25367/overview
+# https://ci.chromium.org/ui/p/chromium/builders/ci/ios-blink-rel-fyi/25367/overview
 -ClipboardTest/PlatformClipboardTest.HTMLTest
 -ClipboardTest/PlatformClipboardTest.MultiFormatTest
 -ClipboardTest/PlatformClipboardTest.TrickyHTMLTest
diff --git a/testing/buildbot/filters/ios.viz_unittests.filter b/testing/buildbot/filters/ios.viz_unittests.filter
index b9853497..330ba20 100644
--- a/testing/buildbot/filters/ios.viz_unittests.filter
+++ b/testing/buildbot/filters/ios.viz_unittests.filter
@@ -201,7 +201,7 @@
 -SurfaceSynchronizationTestMayAlwaysAckOnActivation.UnAckedSurfaceArrivesBeforeNewActivationDependencies/DoNotAckOnSurfaceActivationWhenInteractive
 -SurfaceSynchronizationTestMayAlwaysAckOnActivation.UnAckedSurfaceArrivesBeforeNewActivationDependencies/DoNotAckOnSurfaceActivationWhenInteractive-
 
-# TODO(crbug.com/383593269): These tests are failing on the ios-blink-dbg-fyi
+# TODO(crbug.com/383593269): These tests are failing on the ios-blink-rel-fyi
 # bot.
 -GPURendererPixelTest.TrilinearFiltering/SkiaGraphiteDawn
 -GPURendererPixelTest.TrilinearFiltering/SkiaGraphiteMetal
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index d1e69074..d521566 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -19677,24 +19677,6 @@
             ]
         }
     ],
-    "ReadAnythingDocsIntegrationRollout": [
-        {
-            "platforms": [
-                "chromeos",
-                "linux",
-                "mac",
-                "windows"
-            ],
-            "experiments": [
-                {
-                    "name": "Enabled",
-                    "enable_features": [
-                        "ReadAnythingDocsIntegration"
-                    ]
-                }
-            ]
-        }
-    ],
     "ReadAnythingIPHRollout": [
         {
             "platforms": [
diff --git a/third_party/androidx/build.gradle b/third_party/androidx/build.gradle
index a938cb3..53e47b1 100644
--- a/third_party/androidx/build.gradle
+++ b/third_party/androidx/build.gradle
@@ -307,7 +307,7 @@
     google()
     maven {
         // This URL is generated by the fetch_all_androidx.py script.
-        url 'https://androidx.dev/snapshots/builds/13642928/artifacts/repository'
+        url 'https://androidx.dev/snapshots/builds/13645243/artifacts/repository'
     }
     mavenCentral()
 }
diff --git a/third_party/angle b/third_party/angle
index c53c908..79ec8b3 160000
--- a/third_party/angle
+++ b/third_party/angle
@@ -1 +1 @@
-Subproject commit c53c908c41a10167fabdf4f49c98b9d610d43216
+Subproject commit 79ec8b3400ceeafc3e69b9bec29fa39a0e1a9a16
diff --git a/third_party/blink/renderer/core/layout/length_utils.cc b/third_party/blink/renderer/core/layout/length_utils.cc
index bdc70a8b..8387183 100644
--- a/third_party/blink/renderer/core/layout/length_utils.cc
+++ b/third_party/blink/renderer/core/layout/length_utils.cc
@@ -1086,12 +1086,21 @@
       replaced_block = space.AvailableSize().block_size;
       DCHECK_GE(*replaced_block, 0);
     } else {
+      const Length& non_stretch_length =
+          RuntimeEnabledFeatures::LayoutNewReplacedLogicEnabled()
+              ? Length::FitContent()
+              : Length::Auto();
       const Length& auto_block_length = space.IsBlockAutoBehaviorStretch()
                                             ? Length::FillAvailable()
-                                            : Length::Auto();
-      const LayoutUnit block_size = ResolveMainBlockLength(
-          space, style, border_padding, block_length, &auto_block_length,
-          /* intrinsic_size */ kIndefiniteSize);
+                                            : non_stretch_length;
+      const LayoutUnit block_size =
+          RuntimeEnabledFeatures::LayoutNewReplacedLogicEnabled()
+              ? ResolveMainBlockLength(space, style, border_padding,
+                                       block_length, &auto_block_length,
+                                       BlockSizeFunc)
+              : ResolveMainBlockLength(space, style, border_padding,
+                                       block_length, &auto_block_length,
+                                       /* intrinsic_size */ kIndefiniteSize);
       if (block_size != kIndefiniteSize) {
         DCHECK_GE(block_size, LayoutUnit());
         replaced_block = block_min_max_sizes.ClampSizeToMinAndMax(block_size);
@@ -1138,14 +1147,32 @@
   MinMaxSizes inline_min_max_sizes;
   std::optional<LayoutUnit> replaced_inline;
   if (mode == ReplacedSizeMode::kIgnoreInlineLengths) {
-    // Don't resolve any inline lengths or constraints.
-    inline_min_max_sizes = {LayoutUnit(), LayoutUnit::Max()};
+    // Just use the transferred sizes.
+    inline_min_max_sizes =
+        RuntimeEnabledFeatures::LayoutNewReplacedLogicEnabled()
+            ? transferred_min_max_sizes
+            : MinMaxSizes{LayoutUnit(), LayoutUnit::Max()};
   } else {
     inline_min_max_sizes = {
         ResolveMinInlineLength(space, style, border_padding, MinMaxSizesFunc,
                                style.LogicalMinWidth()),
         ResolveMaxInlineLength(space, style, border_padding, MinMaxSizesFunc,
                                style.LogicalMaxWidth())};
+
+    // Transfer the block min/max sizes if applicable.
+    if (RuntimeEnabledFeatures::LayoutNewReplacedLogicEnabled() &&
+        style.LogicalWidth().HasAuto() &&
+        space.InlineAutoBehavior() != AutoSizeBehavior::kStretchExplicit) {
+      // https://drafts.csswg.org/css-sizing-4/#aspect-ratio-size-transfers
+      inline_min_max_sizes.min_size =
+          std::max(inline_min_max_sizes.min_size,
+                   std::min(transferred_min_max_sizes.min_size,
+                            inline_min_max_sizes.max_size));
+      inline_min_max_sizes.max_size = std::min(
+          inline_min_max_sizes.max_size, transferred_min_max_sizes.max_size);
+    }
+
+    // Ensure the max-size encompasses the min-size.
     inline_min_max_sizes.max_size =
         std::max(inline_min_max_sizes.min_size, inline_min_max_sizes.max_size);
 
@@ -1153,9 +1180,13 @@
       replaced_inline = space.AvailableSize().inline_size;
       DCHECK_GE(*replaced_inline, 0);
     } else {
+      const Length& non_stretch_length =
+          RuntimeEnabledFeatures::LayoutNewReplacedLogicEnabled()
+              ? Length::FitContent()
+              : Length::Auto();
       const Length& auto_length = space.IsInlineAutoBehaviorStretch()
                                       ? Length::FillAvailable()
-                                      : Length::Auto();
+                                      : non_stretch_length;
       const LayoutUnit inline_size =
           ResolveMainInlineLength(space, style, border_padding, MinMaxSizesFunc,
                                   inline_length, &auto_length);
@@ -1191,15 +1222,16 @@
           Length::FillAvailable(), /* auto_length */ nullptr,
           /* override_available_size */ kIndefiniteSize);
     }
+    if (RuntimeEnabledFeatures::LayoutNewReplacedLogicEnabled()) {
+      return size;
+    }
     return transferred_min_max_sizes.ClampSizeToMinAndMax(size);
   };
 
   // We have *only* an aspect-ratio with no sizes (natural or otherwise), we
   // default to stretching.
   if (!natural_size && !replaced_inline && !replaced_block) {
-    replaced_inline = StretchFit();
-    replaced_inline =
-        inline_min_max_sizes.ClampSizeToMinAndMax(*replaced_inline);
+    replaced_inline = inline_min_max_sizes.ClampSizeToMinAndMax(StretchFit());
   }
 
   // We only know one size, the other gets computed via the aspect-ratio (if
@@ -1227,9 +1259,14 @@
     return LogicalSize(*replaced_inline, *replaced_block);
   }
 
-  // Both lengths are unknown, start with the natural-size.
-  DCHECK(!replaced_inline);
-  DCHECK(!replaced_block);
+  // Both sizes are unknown.
+  if (RuntimeEnabledFeatures::LayoutNewReplacedLogicEnabled()) {
+    return {
+        inline_min_max_sizes.ClampSizeToMinAndMax(natural_size->inline_size),
+        block_min_max_sizes.ClampSizeToMinAndMax(natural_size->block_size)};
+  }
+
+  // Start with the natural-size.
   replaced_inline = natural_size->inline_size;
   replaced_block = natural_size->block_size;
 
diff --git a/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.cc b/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.cc
index fcb0236..d976c68c 100644
--- a/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.cc
+++ b/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.cc
@@ -3,6 +3,8 @@
 // found in the LICENSE file.
 #include "third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.h"
 
+#include <cstddef>
+
 #include "base/feature_list.h"
 #include "services/metrics/public/cpp/ukm_builders.h"
 #include "services/metrics/public/cpp/ukm_recorder.h"
@@ -23,6 +25,7 @@
 #include "third_party/blink/renderer/core/style/style_fetched_image.h"
 #include "third_party/blink/renderer/core/timing/dom_window_performance.h"
 #include "third_party/blink/renderer/core/timing/performance_entry.h"
+#include "third_party/blink/renderer/core/timing/soft_navigation_context.h"
 #include "third_party/blink/renderer/core/timing/soft_navigation_heuristics.h"
 #include "third_party/blink/renderer/platform/heap/thread_state.h"
 #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
@@ -62,18 +65,6 @@
   return visual_size;
 }
 
-void RecordPotentialSoftNavigationPaint(LocalFrameView* frame_view,
-                                        gfx::RectF rect,
-                                        Node* node) {
-  LocalFrame& frame = frame_view->GetFrame();
-  if (LocalDOMWindow* window = frame.DomWindow()) {
-    if (SoftNavigationHeuristics* heuristics =
-            window->GetSoftNavigationHeuristics()) {
-      heuristics->RecordPaint(&frame, rect, node);
-    }
-  }
-}
-
 }  // namespace
 
 double ImageRecord::EntropyForLCP() const {
@@ -90,6 +81,7 @@
 
 void ImageRecord::Trace(Visitor* visitor) const {
   visitor->Trace(media_timing);
+  visitor->Trace(soft_navigation_context_);
 }
 
 ImagePaintTimingDetector::ImagePaintTimingDetector(LocalFrameView* frame_view)
@@ -210,14 +202,15 @@
   added_entry_in_latest_frame_ = false;
   auto callback = WTF::BindOnce(
       [](ImagePaintTimingDetector* self, unsigned int frame_index,
-         const base::TimeTicks& presentation_timestamp,
+         bool is_recording_lcp, const base::TimeTicks& presentation_timestamp,
          const DOMPaintTimingInfo& paint_timing_info) {
         if (self) {
           self->records_manager_.AssignPaintTimeToRegisteredQueuedRecords(
-              presentation_timestamp, paint_timing_info, frame_index);
+              presentation_timestamp, paint_timing_info, frame_index,
+              is_recording_lcp);
         }
       },
-      WrapWeakPersistent(this), frame_index_);
+      WrapWeakPersistent(this), frame_index_, IsRecordingLargestImagePaint());
   last_registered_frame_index_ = frame_index_++;
 
   // This is for unit-testing purposes only. Some of these tests check for UKMs
@@ -252,7 +245,8 @@
 void ImageRecordsManager::AssignPaintTimeToRegisteredQueuedRecords(
     const base::TimeTicks& presentation_timestamp,
     const DOMPaintTimingInfo& paint_timing_info,
-    unsigned last_queued_frame_index) {
+    unsigned last_queued_frame_index,
+    bool is_recording_lcp) {
   while (!images_queued_for_paint_time_.empty()) {
     ImageRecord* record = images_queued_for_paint_time_.front();
     // Skip any null records at the start of the queue
@@ -301,8 +295,9 @@
       record->paint_timing_info = paint_timing_info;
     }
     // Update largest if necessary.
-    if (!largest_painted_image_ ||
-        largest_painted_image_->recorded_size < record->recorded_size) {
+    if (is_recording_lcp &&
+        (!largest_painted_image_ ||
+         largest_painted_image_->recorded_size < record->recorded_size)) {
       largest_painted_image_ = std::move(it->value);
     }
     // Remove from pending.
@@ -349,11 +344,20 @@
     if (depth == 1 && IgnorePaintTimingScope::IsDocumentElementInvisible() &&
         media_timing.IsSufficientContentLoadedForPaint()) {
       records_manager_.MaybeUpdateLargestIgnoredImage(
-          record_id, rect_size, image_border, mapped_visual_rect);
+          record_id, rect_size, image_border, mapped_visual_rect,
+          IsRecordingLargestImagePaint());
     }
     return false;
   }
 
+  SoftNavigationContext* context = nullptr;
+  if (LocalDOMWindow* window = frame_view_->GetFrame().DomWindow()) {
+    if (SoftNavigationHeuristics* heuristics =
+            window->GetSoftNavigationHeuristics()) {
+      context = heuristics->MaybeGetSoftNavigationContextForTiming(node);
+    }
+  }
+
   // RecordImage is called whenever an image is painted, which may happen many
   // times for the same record.  The very first paint for this record, we have
   // to create and initialize things, and all subsequent paints we just do a
@@ -367,13 +371,8 @@
                      ? media_timing.ContentSizeForEntropy() * 8.0 / rect_size
                      : 0.0;
     record = records_manager_.RecordFirstPaintAndMaybeCreateImageRecord(
-        !IsRecordingLargestImagePaint(), record_id, rect_size, image_border,
-        mapped_visual_rect, bpp);
-
-    // We record soft paint, even if we didn't create a new LCP Record, because
-    // we only create records for new largest content, but we want to record
-    // all paints.
-    RecordPotentialSoftNavigationPaint(frame_view_, mapped_visual_rect, node);
+        IsRecordingLargestImagePaint(), record_id, rect_size, image_border,
+        mapped_visual_rect, bpp, context);
   }
 
   // Note: Even if IsRecordedImage() returns `true`, or if we are calling a new
@@ -385,6 +384,14 @@
     return false;
   }
 
+  // Check if context changed from the last time we painted this media.
+  if (record->soft_navigation_context_ != context) {
+    record->soft_navigation_context_ = context;
+    // TODO(crbug.com/424437484): Find a mechanism to re-report this media, if
+    // it has already been loaded, because it won't report again otherwise.
+    // record->loaded = false;
+  }
+
   // If this frame is the first painted frame for animated content, mark it and
   // call `QueueToMeasurePaintTime` (eventually) to measure it.
   // This mechanism works a bit differently for images and video.
@@ -410,6 +417,10 @@
           object, mapped_visual_rect,
           media_timing.IsSufficientContentLoadedForPaint(), media_timing.Url());
     }
+    CHECK(context == record->soft_navigation_context_);
+    if (context) {
+      context->AddPaintedArea(record);
+    }
     return true;
   }
   return false;
@@ -470,7 +481,8 @@
 
 void ImagePaintTimingDetector::ReportLargestIgnoredImage() {
   added_entry_in_latest_frame_ = true;
-  records_manager_.ReportLargestIgnoredImage(frame_index_);
+  records_manager_.ReportLargestIgnoredImage(frame_index_,
+                                             IsRecordingLargestImagePaint());
 }
 
 ImageRecordsManager::ImageRecordsManager(LocalFrameView* frame_view)
@@ -533,7 +545,8 @@
 }
 
 void ImageRecordsManager::ReportLargestIgnoredImage(
-    unsigned current_frame_index) {
+    unsigned current_frame_index,
+    bool is_recording_lcp) {
   if (!largest_ignored_image_)
     return;
   Node* node = DOMNodeIds::NodeForId(largest_ignored_image_->node_id);
@@ -552,7 +565,7 @@
   ImageRecord* record = largest_ignored_image_.Get();
   CHECK(record);
   recorded_images_.insert(record->hash);
-  AddPendingImage(record);
+  AddPendingImage(record, is_recording_lcp);
   OnImageLoadedInternal(record, current_frame_index);
 }
 
@@ -566,23 +579,27 @@
     const MediaRecordId& record_id,
     const uint64_t& visual_size,
     const gfx::Rect& frame_visual_rect,
-    const gfx::RectF& root_visual_rect) {
-  if (visual_size && (!largest_ignored_image_ ||
-                      visual_size > largest_ignored_image_->recorded_size)) {
+    const gfx::RectF& root_visual_rect,
+    bool is_recording_lcp) {
+  if (visual_size && is_recording_lcp &&
+      (!largest_ignored_image_ ||
+       visual_size > largest_ignored_image_->recorded_size)) {
     largest_ignored_image_ = CreateImageRecord(
         *record_id.GetLayoutObject(), record_id.GetMediaTiming(), visual_size,
-        frame_visual_rect, root_visual_rect, record_id.GetHash());
+        frame_visual_rect, root_visual_rect, record_id.GetHash(),
+        /*soft_navigation_context=*/nullptr);
     largest_ignored_image_->load_time = base::TimeTicks::Now();
   }
 }
 
 ImageRecord* ImageRecordsManager::RecordFirstPaintAndMaybeCreateImageRecord(
-    bool record_media_only_and_skip_measuring_all_image_records,
+    bool is_recording_lcp,
     const MediaRecordId& record_id,
     const uint64_t& visual_size,
     const gfx::Rect& frame_visual_rect,
     const gfx::RectF& root_visual_rect,
-    double bpp) {
+    double bpp,
+    SoftNavigationContext* soft_navigation_context) {
   // Don't process the image yet if it is invisible, as it may later become
   // visible, and potentially eligible to be an LCP candidate.
   if (visual_size == 0u) {
@@ -590,28 +607,38 @@
   }
   recorded_images_.insert(record_id.GetHash());
 
-  if (record_media_only_and_skip_measuring_all_image_records) {
+  // If we are recording LCP, take the timing unless the correct LCP is already
+  // larger.
+  bool timing_needed_for_lcp =
+      is_recording_lcp &&
+      !(largest_painted_image_ &&
+        largest_painted_image_->recorded_size > visual_size);
+  // If we have a context involved in this node creation, we need to do record
+  // keeping.
+  // Node: Once the soft nav entry is emitted, we might be able to switch to
+  // largest-area-only recording.
+  bool timing_needed_for_soft_nav = soft_navigation_context != nullptr;
+
+  if (!timing_needed_for_lcp && !timing_needed_for_soft_nav) {
     return nullptr;
   }
 
-  // If this cannot become an LCP candidate, no need to do anything else.
-  if (largest_painted_image_ &&
-      largest_painted_image_->recorded_size > visual_size) {
-    return nullptr;
-  }
   if (bpp < kMinimumEntropyForLCP) {
     return nullptr;
   }
 
   ImageRecord* record = CreateImageRecord(
       *record_id.GetLayoutObject(), record_id.GetMediaTiming(), visual_size,
-      frame_visual_rect, root_visual_rect, record_id.GetHash());
-  AddPendingImage(record);
+      frame_visual_rect, root_visual_rect, record_id.GetHash(),
+      soft_navigation_context);
+  AddPendingImage(record, is_recording_lcp);
   return record;
 }
-void ImageRecordsManager::AddPendingImage(ImageRecord* record) {
-  if (!largest_pending_image_ ||
-      (largest_pending_image_->recorded_size < record->recorded_size)) {
+void ImageRecordsManager::AddPendingImage(ImageRecord* record,
+                                          bool is_recording_lcp) {
+  if (is_recording_lcp &&
+      (!largest_pending_image_ ||
+       (largest_pending_image_->recorded_size < record->recorded_size))) {
     largest_pending_image_ = record;
   }
   pending_images_.insert(record->hash, record);
@@ -623,29 +650,20 @@
     const uint64_t& visual_size,
     const gfx::Rect& frame_visual_rect,
     const gfx::RectF& root_visual_rect,
-    MediaRecordIdHash hash) {
+    MediaRecordIdHash hash,
+    SoftNavigationContext* soft_navigaton_context) {
   DCHECK_GT(visual_size, 0u);
   Node* node = object.GetNode();
   DOMNodeId node_id = node->GetDomNodeId();
   return MakeGarbageCollected<ImageRecord>(node_id, media_timing, visual_size,
                                            frame_visual_rect, root_visual_rect,
-                                           hash);
+                                           hash, soft_navigaton_context);
 }
 
 void ImageRecordsManager::ClearImagesQueuedForPaintTime() {
   images_queued_for_paint_time_.clear();
 }
 
-void ImageRecordsManager::Clear() {
-  largest_painted_image_ = nullptr;
-  largest_pending_image_ = nullptr;
-  images_queued_for_paint_time_.clear();
-  recorded_images_.clear();
-  pending_images_.clear();
-  image_finished_times_.clear();
-  largest_ignored_image_ = nullptr;
-}
-
 void ImageRecordsManager::Trace(Visitor* visitor) const {
   visitor->Trace(frame_view_);
   visitor->Trace(largest_painted_image_);
diff --git a/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.h b/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.h
index 8c398a1..a1c473f8 100644
--- a/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.h
+++ b/third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.h
@@ -27,6 +27,7 @@
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_deque.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_hash_set.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+#include "third_party/blink/renderer/platform/heap/member.h"
 #include "third_party/blink/renderer/platform/loader/fetch/media_timing.h"
 #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
 #include "third_party/blink/renderer/platform/wtf/hash_set.h"
@@ -42,23 +43,27 @@
 class PaintTimingCallbackManager;
 class StyleImage;
 struct DOMPaintTimingInfo;
+class SoftNavigationContext;
 
 static constexpr double kMinimumEntropyForLCP = 0.05;
 
 // TODO(crbug/960502): we should limit the access of these properties.
 // TODO(yoav): Rename all mentions of "image" to "media"
-class ImageRecord : public GarbageCollected<ImageRecord> {
+class CORE_EXPORT ImageRecord : public GarbageCollected<ImageRecord> {
  public:
   ImageRecord(DOMNodeId new_node_id,
               const MediaTiming* new_media_timing,
               uint64_t new_recorded_size,
               const gfx::Rect& frame_visual_rect,
               const gfx::RectF& root_visual_rect,
-              MediaRecordIdHash hash)
+              MediaRecordIdHash hash,
+              SoftNavigationContext* soft_navigation_context)
       : node_id(new_node_id),
         media_timing(new_media_timing),
         hash(hash),
-        recorded_size(new_recorded_size) {
+        root_visual_rect(root_visual_rect),
+        recorded_size(new_recorded_size),
+        soft_navigation_context_(soft_navigation_context) {
     if (PaintTimingVisualizer::IsTracingEnabled()) {
       lcp_rect_info_ = std::make_unique<LCPRectInfo>(
           frame_visual_rect, gfx::ToRoundedRect(root_visual_rect));
@@ -83,6 +88,7 @@
   MediaRecordIdHash hash;
   // Mind that |recorded_size| has to be assigned before any size comparisons
   // (to determine largest image) are performed.
+  gfx::RectF root_visual_rect;
   uint64_t recorded_size = 0;
   unsigned frame_index = 0;
   // The time of the first paint after fully loaded. 0 means not painted yet.
@@ -100,6 +106,7 @@
   // Images that come from origin-dirty styles should have some limitations on
   // what they report.
   bool origin_clean = true;
+  WeakMember<SoftNavigationContext> soft_navigation_context_;
 };
 
 // |ImageRecordsManager| is the manager of all of the images that Largest
@@ -140,12 +147,13 @@
   // Always adds media record to `recorded_images_`, and might create a new
   // ImageRecord to add to `pending_images_`.
   ImageRecord* RecordFirstPaintAndMaybeCreateImageRecord(
-      bool record_media_only_and_skip_measuring_all_image_records,
+      bool is_recording_lcp,
       const MediaRecordId& record_id,
       const uint64_t& visual_size,
       const gfx::Rect& frame_visual_rect,
       const gfx::RectF& root_visual_rect,
-      double bpp);
+      double bpp,
+      SoftNavigationContext* soft_navigation_context);
   bool IsRecordedImage(MediaRecordIdHash record_id_hash) const {
     return recorded_images_.Contains(record_id_hash);
   }
@@ -177,24 +185,28 @@
   void MaybeUpdateLargestIgnoredImage(const MediaRecordId&,
                                       const uint64_t& visual_size,
                                       const gfx::Rect& frame_visual_rect,
-                                      const gfx::RectF& root_visual_rect);
-  void ReportLargestIgnoredImage(unsigned current_frame_index);
+                                      const gfx::RectF& root_visual_rect,
+                                      bool is_recording_lcp);
+  void ReportLargestIgnoredImage(unsigned current_frame_index,
+                                 bool is_recording_lcp);
 
   void AssignPaintTimeToRegisteredQueuedRecords(
       const base::TimeTicks&,
       const DOMPaintTimingInfo&,
-      unsigned last_queued_frame_index);
+      unsigned last_queued_frame_index,
+      bool is_recording_lcp);
 
-  void AddPendingImage(ImageRecord* record);
+  void AddPendingImage(ImageRecord* record, bool is_recording_lcp);
   void ClearImagesQueuedForPaintTime();
-  void Clear();
 
-  ImageRecord* CreateImageRecord(const LayoutObject& object,
-                                 const MediaTiming* media_timing,
-                                 const uint64_t& visual_size,
-                                 const gfx::Rect& frame_visual_rect,
-                                 const gfx::RectF& root_visual_rect,
-                                 MediaRecordIdHash hash);
+  ImageRecord* CreateImageRecord(
+      const LayoutObject& object,
+      const MediaTiming* media_timing,
+      const uint64_t& visual_size,
+      const gfx::Rect& frame_visual_rect,
+      const gfx::RectF& root_visual_rect,
+      MediaRecordIdHash hash,
+      SoftNavigationContext* soft_navigation_context);
   inline void QueueToMeasurePaintTime(ImageRecord* record,
                                       unsigned current_frame_index) {
     CHECK(record);
@@ -313,11 +325,6 @@
   void StopRecordingLargestImagePaint() {
     recording_largest_image_paint_ = false;
   }
-  void RestartRecordingLargestImagePaint() {
-    recording_largest_image_paint_ = true;
-    records_manager_.Clear();
-  }
-
   void Trace(Visitor*) const;
 
  private:
diff --git a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.cc b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.cc
index 24d4ff67..f001b31 100644
--- a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.cc
+++ b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.cc
@@ -122,13 +122,13 @@
   const AtomicString& image_id =
       image_element ? image_element->GetIdAttribute() : AtomicString();
 
+  // TODO(crbug.com/424433918): Create a new entry type, specific to soft-navs.
   window_performance_->OnLargestContentfulPaintUpdated(
       expose_paint_time_to_api
           ? std::make_optional(largest_image->paint_timing_info)
           : std::nullopt,
       /*paint_size=*/largest_image->recorded_size,
       /*load_time=*/largest_image->load_time,
-
       /*id=*/image_id, /*url=*/image_url, /*element=*/image_element,
       is_triggered_by_soft_navigation);
 
@@ -164,6 +164,8 @@
       text_node->IsInShadowTree() ? nullptr : DynamicTo<Element>(text_node);
   const AtomicString& text_id =
       text_element ? text_element->GetIdAttribute() : AtomicString();
+
+  // TODO(crbug.com/424433918): Create a new entry type, specific to soft-navs.
   // Always use paint time as start time for text LCP candidate.
   window_performance_->OnLargestContentfulPaintUpdated(
       largest_text.paint_timing_info,
diff --git a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h
index 5cf5bea..74588668 100644
--- a/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h
+++ b/third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h
@@ -12,6 +12,9 @@
 
 namespace blink {
 
+class TextRecord;
+class ImageRecord;
+
 // LargestContentfulPaintCalculator is responsible for tracking the largest
 // image paint and the largest text paint and notifying WindowPerformance
 // whenever a new LatestLargestContentfulPaint entry should be dispatched.
diff --git a/third_party/blink/renderer/core/paint/timing/paint_timing.cc b/third_party/blink/renderer/core/paint/timing/paint_timing.cc
index 5c3e7f6..50cfaecc 100644
--- a/third_party/blink/renderer/core/paint/timing/paint_timing.cc
+++ b/third_party/blink/renderer/core/paint/timing/paint_timing.cc
@@ -33,6 +33,7 @@
 #include "third_party/blink/renderer/core/timing/dom_window_performance.h"
 #include "third_party/blink/renderer/core/timing/performance_entry.h"
 #include "third_party/blink/renderer/core/timing/performance_timing_for_reporting.h"
+#include "third_party/blink/renderer/core/timing/soft_navigation_heuristics.h"
 #include "third_party/blink/renderer/core/timing/window_performance.h"
 #include "third_party/blink/renderer/platform/graphics/paint/ignore_paint_timing_scope.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
@@ -61,7 +62,6 @@
 
 struct PendingPaintTimingRecord {
   HashSet<PaintEvent> paint_events;
-  bool is_soft_navigation = false;
   base::TimeTicks rendering_update_end_time;
 };
 
@@ -247,6 +247,8 @@
 
 void PaintTiming::MarkPaintTimingInternal() {
   PaintTimingDetector* detector = &GetFrame()->View()->GetPaintTimingDetector();
+  SoftNavigationHeuristics* soft_navigation_heuristics =
+      GetFrame()->DomWindow()->GetSoftNavigationHeuristics();
 
   // 3. Let paintedImages be a new ordered set...
   auto add_painted_images_element_timing_entries =
@@ -264,7 +266,6 @@
   // 7. Let reportedPaints be the document’s set of previously reported paints.
   PendingPaintTimingRecord paint_timing_record{
       .paint_events = pending_paint_events_,
-      .is_soft_navigation = first_paints_reset_,
       .rendering_update_end_time = last_rendering_update_end_time_};
   pending_paint_events_.clear();
 
@@ -298,6 +299,7 @@
              OptionalPaintTimingCallback painted_images_callback,
              OptionalPaintTimingCallback painted_text_callback,
              PaintTimingDetector* paint_timing_detector,
+             SoftNavigationHeuristics* soft_navigation_heuristics,
              const base::TimeTicks& raw_presentation_timestamp,
              const DOMPaintTimingInfo& paint_timing_info) {
             if (!performance) {
@@ -308,8 +310,7 @@
             // then: Report paint timing given document,
             // "first-paint", and paintTimingInfo.
             if (record.paint_events.Contains(PaintEvent::kFirstPaint)) {
-              performance->AddFirstPaintTiming(paint_timing_info,
-                                               record.is_soft_navigation);
+              performance->AddFirstPaintTiming(paint_timing_info);
             }
 
             // 10.2. If document should report first contentful paint,
@@ -317,8 +318,7 @@
             // "first-contentful-paint", and paintTimingInfo.
             if (record.paint_events.Contains(
                     PaintEvent::kFirstContentfulPaint)) {
-              performance->AddFirstContentfulPaintTiming(
-                  paint_timing_info, record.is_soft_navigation);
+              performance->AddFirstContentfulPaintTiming(paint_timing_info);
             }
 
             // 10.3. Report largest contentful paint given document,
@@ -346,6 +346,10 @@
               paint_timing_detector->UpdateLcpCandidate();
             }
 
+            if (soft_navigation_heuristics && may_have_lcp) {
+              soft_navigation_heuristics->UpdateSoftLcpCandidate();
+            }
+
             // 10.5 If frameTimingInfo is not null, then queue a long
             // animation frame entry given document, frameTimingInfo, and
             // paintTimingInfo.
@@ -358,7 +362,8 @@
           paint_timing_record, WrapPersistent(frame_timing_info),
           std::move(add_image_lcp_entries),
           std::move(add_painted_images_element_timing_entries),
-          std::move(add_painted_text_entries), WrapWeakPersistent(detector));
+          std::move(add_painted_text_entries), WrapWeakPersistent(detector),
+          WrapWeakPersistent(soft_navigation_heuristics));
 
   // 11. If the user-agent does not support implementation-defined presentation
   // times, call flushPaintTimings and return.
@@ -472,12 +477,10 @@
 
   relevant_paint_details.first_paint_ = stamp;
 
-  if (!first_paints_reset_) {
     LocalFrame* frame = GetFrame();
     if (frame && frame->GetDocument()) {
       frame->GetDocument()->MarkFirstPaint();
     }
-  }
 
   pending_paint_events_.insert(PaintEvent::kFirstPaint);
 }
@@ -492,23 +495,18 @@
   relevant_paint_details.first_contentful_paint_ = stamp;
 
   // This only happens in hard navigations.
-  if (!first_paints_reset_) {
-    LocalFrame* frame = GetFrame();
-    if (!frame) {
-      return;
-    }
-    frame->View()->OnFirstContentfulPaint();
+  LocalFrame* frame = GetFrame();
+  if (!frame) {
+    return;
+  }
+  frame->View()->OnFirstContentfulPaint();
 
-    if (frame->IsMainFrame() && frame->GetFrameScheduler()) {
-      frame->GetFrameScheduler()->OnFirstContentfulPaintInMainFrame();
-    }
+  if (frame->IsMainFrame() && frame->GetFrameScheduler()) {
+    frame->GetFrameScheduler()->OnFirstContentfulPaintInMainFrame();
   }
   SetFirstPaint(stamp);
   Mark(PaintEvent::kFirstContentfulPaint);
-
-  if (!first_paints_reset_ || soft_navigation_detected_) {
-    NotifyPaintTimingChanged();
-  }
+  NotifyPaintTimingChanged();
 }
 
 void PaintTiming::Mark(PaintEvent event) {
@@ -598,18 +596,7 @@
 
 void PaintTiming::SetFirstPaintPresentation(
     const PaintTimingInfo& paint_timing_info) {
-  if (soft_navigation_fp_reported_) {
-    return;
-  }
-  if (first_paints_reset_ && !soft_navigation_detected_) {
-    // We're expecting a soft navigation paint, but soft navigation wasn't yet
-    // detected. Avoid reporting it for now, and it'll be reported once soft
-    // navigation is detected.
-    soft_navigation_pending_first_paint_timing_info_ = paint_timing_info;
-    return;
-  }
   PaintDetails& relevant_paint_details = GetRelevantPaintDetails();
-  soft_navigation_pending_first_paint_timing_info_ = std::nullopt;
   DCHECK(relevant_paint_details.first_paint_presentation_.is_null());
   relevant_paint_details.first_paint_presentation_ =
       paint_timing_info.presentation_time;
@@ -621,51 +608,24 @@
       relevant_paint_details.first_paint_presentation_.since_origin()
           .InSecondsF());
   NotifyPaintTimingChanged();
-  if (first_paints_reset_) {
-    soft_navigation_fp_reported_ = true;
-  }
 }
 
 void PaintTiming::SetFirstContentfulPaintPresentation(
     const PaintTimingInfo& paint_timing_info) {
-  if (soft_navigation_fcp_reported_) {
-    return;
-  }
-  if (first_paints_reset_ && !soft_navigation_detected_) {
-    // We're expecting a soft navigation paint, but soft navigation wasn't yet
-    // detected. Avoid reporting it for now, and it'll be reported once soft
-    // navigation is detected.
-    soft_navigation_pending_first_contentful_paint_timing_info_ =
-        paint_timing_info;
-    return;
-  }
   PaintDetails& relevant_paint_details = GetRelevantPaintDetails();
-  soft_navigation_pending_first_contentful_paint_timing_info_ = std::nullopt;
   DCHECK(relevant_paint_details.first_contentful_paint_presentation_.is_null());
   TRACE_EVENT_INSTANT_WITH_TIMESTAMP0(
       "benchmark,loading", "GlobalFirstContentfulPaint",
       TRACE_EVENT_SCOPE_GLOBAL, paint_timing_info.presentation_time);
   relevant_paint_details.first_contentful_paint_presentation_ =
       paint_timing_info.presentation_time;
-  bool is_soft_navigation_fcp = false;
-  if (first_contentful_paint_presentation_ignoring_soft_navigations_
-          .is_null()) {
-    first_contentful_paint_presentation_ignoring_soft_navigations_ =
-        paint_timing_info.presentation_time;
-  } else {
-    is_soft_navigation_fcp = true;
-  }
+  CHECK(first_contentful_paint_presentation_.is_null());
+  first_contentful_paint_presentation_ = paint_timing_info.presentation_time;
   probe::PaintTiming(
       GetSupplementable(), "firstContentfulPaint",
       relevant_paint_details.first_contentful_paint_presentation_.since_origin()
           .InSecondsF());
   WindowPerformance* performance = GetPerformanceInstance(GetFrame());
-  // For soft navigations, we just want to report a performance entry, but not
-  // trigger any of the other FCP observers.
-  if (is_soft_navigation_fcp) {
-    soft_navigation_fcp_reported_ = true;
-    return;
-  }
   if (GetFrame()) {
     GetFrame()->OnFirstContentfulPaint();
     GetFrame()->Loader().Progress().DidFirstContentfulPaint();
@@ -765,16 +725,4 @@
                                                                      index));
 }
 
-void PaintTiming::SoftNavigationDetected() {
-  soft_navigation_detected_ = true;
-  if (soft_navigation_pending_first_paint_timing_info_.has_value()) {
-    SetFirstPaintPresentation(
-        *soft_navigation_pending_first_paint_timing_info_);
-  }
-  if (soft_navigation_pending_first_contentful_paint_timing_info_.has_value()) {
-    SetFirstContentfulPaintPresentation(
-        *soft_navigation_pending_first_contentful_paint_timing_info_);
-  }
-}
-
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/paint/timing/paint_timing.h b/third_party/blink/renderer/core/paint/timing/paint_timing.h
index c9428c1..46127e7 100644
--- a/third_party/blink/renderer/core/paint/timing/paint_timing.h
+++ b/third_party/blink/renderer/core/paint/timing/paint_timing.h
@@ -123,7 +123,7 @@
   // instance, the first time that text or image content was painted after the
   // user landed on the page.
   base::TimeTicks FirstContentfulPaintIgnoringSoftNavigations() const {
-    return first_contentful_paint_presentation_ignoring_soft_navigations_;
+    return first_contentful_paint_presentation_;
   }
 
   base::TimeTicks FirstContentfulPaintRenderedButNotPresentedAsMonotonicTime()
@@ -131,14 +131,6 @@
     return paint_details_.first_contentful_paint_;
   }
 
-  void ResetFirstPaintAndFCP() {
-    soft_navigation_pending_paint_details_ = PaintDetails();
-    first_paints_reset_ = true;
-    soft_navigation_detected_ = false;
-    soft_navigation_fp_reported_ = false;
-    soft_navigation_fcp_reported_ = false;
-  }
-
   // FirstImagePaint returns the first time that image content was painted.
   base::TimeTicks FirstImagePaint() const {
     return paint_details_.first_image_paint_presentation_;
@@ -186,8 +178,6 @@
 
   void OnRestoredFromBackForwardCache();
 
-  void SoftNavigationDetected();
-
   void MarkPaintTiming();
 
   void Trace(Visitor*) const override;
@@ -254,33 +244,20 @@
     base::TimeTicks first_contentful_paint_presentation_;
   };
 
-  PaintDetails& GetRelevantPaintDetails() {
-    return first_paints_reset_ ? soft_navigation_pending_paint_details_
-                               : paint_details_;
-  }
+  PaintDetails& GetRelevantPaintDetails() { return paint_details_; }
 
   DOMPaintTimingInfo ToDOMPaintTimingInfo(const PaintTimingInfo&) const;
 
   PaintDetails paint_details_;
-  PaintDetails soft_navigation_pending_paint_details_;
-  std::optional<PaintTimingInfo>
-      soft_navigation_pending_first_paint_timing_info_;
-  std::optional<PaintTimingInfo>
-      soft_navigation_pending_first_contentful_paint_timing_info_;
   // First paint timestamp that doesn't update after soft navigations, and only
   // used for UKM reporting.
   base::TimeTicks first_paint_presentation_for_ukm_;
   // FCP timestamp that does not update after soft navigations.
-  base::TimeTicks
-      first_contentful_paint_presentation_ignoring_soft_navigations_;
+  base::TimeTicks first_contentful_paint_presentation_;
   base::TimeTicks first_meaningful_paint_presentation_;
   base::TimeTicks first_meaningful_paint_candidate_;
   base::TimeTicks first_eligible_to_paint_;
   base::TimeTicks last_rendering_update_end_time_;
-  bool first_paints_reset_ = false;
-  bool soft_navigation_detected_ = false;
-  bool soft_navigation_fp_reported_ = false;
-  bool soft_navigation_fcp_reported_ = false;
 
   base::TimeTicks lcp_mouse_over_dispatch_time_;
 
diff --git a/third_party/blink/renderer/core/paint/timing/paint_timing_detector.cc b/third_party/blink/renderer/core/paint/timing/paint_timing_detector.cc
index b2124090..3c00718 100644
--- a/third_party/blink/renderer/core/paint/timing/paint_timing_detector.cc
+++ b/third_party/blink/renderer/core/paint/timing/paint_timing_detector.cc
@@ -245,7 +245,7 @@
 void PaintTimingDetector::OnInputOrScroll() {
   // If we have already stopped and we're no longer recording the largest image
   // paint, then abort.
-  if (!image_paint_timing_detector_->IsRecordingLargestImagePaint()) {
+  if (!record_lcp_to_metrics_) {
     return;
   }
 
@@ -295,50 +295,6 @@
          image_paint_timing_detector_;
 }
 
-void PaintTimingDetector::RestartRecordingLCP() {
-  text_paint_timing_detector_->RestartRecordingLargestTextPaint();
-  image_paint_timing_detector_->RestartRecordingLargestImagePaint();
-  lcp_was_restarted_ = true;
-  soft_navigation_was_detected_ = false;
-  GetLargestContentfulPaintCalculator()->ResetMetricsLcp();
-}
-
-void PaintTimingDetector::SoftNavigationDetected(LocalDOMWindow* window) {
-  soft_navigation_was_detected_ = true;
-  auto* lcp_calculator = GetLargestContentfulPaintCalculator();
-  // If the window is detached (no calculator) or we haven't yet got any
-  // presentation times for neither a text record nor an image one, bail. The
-  // web exposed entry will get updated when the presentation times callback
-  // will be called.
-  if (!lcp_calculator || (!potential_soft_navigation_text_record_ &&
-                          !potential_soft_navigation_image_record_)) {
-    return;
-  }
-  if (!lcp_was_restarted_ ||
-      RuntimeEnabledFeatures::SoftNavigationHeuristicsEnabled(window)) {
-    lcp_calculator->UpdateWebExposedLargestContentfulPaintIfNeeded(
-        potential_soft_navigation_text_record_,
-        potential_soft_navigation_image_record_,
-        /*is_triggered_by_soft_navigation=*/lcp_was_restarted_);
-  }
-
-  // Report the soft navigation LCP to metrics.
-  CHECK(record_soft_navigation_lcp_for_metrics_);
-  soft_navigation_lcp_details_for_metrics_ =
-      largest_contentful_paint_calculator_->LatestLcpDetails();
-  DidChangePerformanceTiming();
-}
-
-void PaintTimingDetector::RestartRecordingLCPToUkm() {
-  text_paint_timing_detector_->RestartRecordingLargestTextPaint();
-  image_paint_timing_detector_->RestartRecordingLargestImagePaint();
-  record_soft_navigation_lcp_for_metrics_ = true;
-  // Reset the lcp candidate and the soft navigation LCP for reporting to UKM
-  // when a new soft navigation happens. When this resetting happens, the
-  // previous lcp details should already be updated.
-  soft_navigation_lcp_details_for_metrics_ = LargestContentfulPaintDetails();
-}
-
 LargestContentfulPaintCalculator*
 PaintTimingDetector::GetLargestContentfulPaintCalculator() {
   if (largest_contentful_paint_calculator_) {
@@ -359,24 +315,13 @@
 void PaintTimingDetector::UpdateMetricsLcp() {
   // The DidChangePerformanceTiming method which triggers the reporting of
   // metrics LCP would not be called when we are not recording metrics LCP.
-  if (!record_lcp_to_metrics_ && !record_soft_navigation_lcp_for_metrics_) {
+  if (!record_lcp_to_metrics_) {
     return;
   }
 
-  if (record_lcp_to_metrics_) {
-    auto latest_lcp_details =
-        GetLargestContentfulPaintCalculator()->LatestLcpDetails();
-    lcp_details_for_metrics_ = latest_lcp_details;
-  }
-
-  // If we're waiting on a softnav and it wasn't detected yet, keep on waiting
-  // and don't update.
-  if (record_soft_navigation_lcp_for_metrics_ &&
-      soft_navigation_was_detected_) {
-    auto latest_lcp_details =
-        GetLargestContentfulPaintCalculator()->LatestLcpDetails();
-    soft_navigation_lcp_details_for_metrics_ = latest_lcp_details;
-  }
+  auto latest_lcp_details =
+      GetLargestContentfulPaintCalculator()->LatestLcpDetails();
+  lcp_details_for_metrics_ = latest_lcp_details;
 
   DidChangePerformanceTiming();
 }
@@ -467,25 +412,11 @@
   if (image_update_result.second || text_update_result.second) {
     UpdateMetricsLcp();
   }
-  // If we stopped and then restarted LCP measurement (to support soft
-  // navigations), and didn't yet detect a soft navigation, put aside the
-  // records as potential soft navigation LCP ones, and don't update the web
-  // exposed entries just yet. We'll do that once we actually detect the soft
-  // navigation.
-  if (lcp_was_restarted_ && !soft_navigation_was_detected_) {
-    potential_soft_navigation_text_record_ = text_update_result.first;
-    potential_soft_navigation_image_record_ = image_update_result.first;
-    return;
-  }
-  potential_soft_navigation_text_record_ = nullptr;
-  potential_soft_navigation_image_record_ = nullptr;
 
-  // If we're still recording the initial LCP, or if LCP was explicitly
-  // restarted for soft navigations, fire the web exposed entry.
-  if (record_lcp_to_metrics_ || lcp_was_restarted_) {
+  if (record_lcp_to_metrics_) {
     lcp_calculator->UpdateWebExposedLargestContentfulPaintIfNeeded(
         text_update_result.first, image_update_result.first,
-        /*is_triggered_by_soft_navigation=*/lcp_was_restarted_);
+        /*is_triggered_by_soft_navigation=*/false);
   }
 }
 
@@ -558,8 +489,6 @@
   visitor->Trace(image_paint_timing_detector_);
   visitor->Trace(frame_view_);
   visitor->Trace(largest_contentful_paint_calculator_);
-  visitor->Trace(potential_soft_navigation_image_record_);
-  visitor->Trace(potential_soft_navigation_text_record_);
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/paint/timing/paint_timing_detector.h b/third_party/blink/renderer/core/paint/timing/paint_timing_detector.h
index 34493b3a..56f52b0 100644
--- a/third_party/blink/renderer/core/paint/timing/paint_timing_detector.h
+++ b/third_party/blink/renderer/core/paint/timing/paint_timing_detector.h
@@ -23,7 +23,6 @@
 
 class Image;
 class ImagePaintTimingDetector;
-class ImageRecord;
 class ImageResourceContent;
 class LargestContentfulPaintCalculator;
 class LayoutObject;
@@ -31,7 +30,6 @@
 class PropertyTreeStateOrAlias;
 class MediaTiming;
 class TextPaintTimingDetector;
-class TextRecord;
 class StyleImage;
 
 // PaintTimingDetector receives signals regarding text and image paints and
@@ -100,22 +98,12 @@
     DCHECK(image_paint_timing_detector_);
     return *image_paint_timing_detector_;
   }
-  void RestartRecordingLCP();
-  void SoftNavigationDetected(LocalDOMWindow*);
-  bool IsSoftNavigationDetected() const {
-    return soft_navigation_was_detected_;
-  }
-  bool WasLCPRestarted() const { return lcp_was_restarted_; }
-
-  void RestartRecordingLCPToUkm();
-
   LargestContentfulPaintCalculator* GetLargestContentfulPaintCalculator();
 
   const LargestContentfulPaintDetails& LargestContentfulPaintDetailsForMetrics()
       const {
     return lcp_details_for_metrics_;
   }
-
   const LargestContentfulPaintDetails&
   SoftNavigationLargestContentfulPaintDetailsForMetrics() const {
     return soft_navigation_lcp_details_for_metrics_;
@@ -171,17 +159,6 @@
   // Ensures LCP stops being reported as a hard navigation metric once we start
   // reporting soft navigation ones.
   bool record_lcp_to_metrics_ = true;
-  // LCP was restarted, due to a potential soft navigation.
-  bool lcp_was_restarted_ = false;
-  // The soft navigation was detected, so the LCP entries can be updated.
-  bool soft_navigation_was_detected_ = false;
-  // Records of entries discovered after LCP was restarted but before a soft
-  // navigation was detected.
-  Member<TextRecord> potential_soft_navigation_text_record_;
-  Member<ImageRecord> potential_soft_navigation_image_record_;
-
-  // This flag indicates if LCP is being reported to UKM.
-  bool record_soft_navigation_lcp_for_metrics_ = false;
 };
 
 // Largest Text Paint and Text Element Timing aggregate text nodes by these
diff --git a/third_party/blink/renderer/core/paint/timing/text_paint_timing_detector.cc b/third_party/blink/renderer/core/paint/timing/text_paint_timing_detector.cc
index 066b0fe..c27f2b7 100644
--- a/third_party/blink/renderer/core/paint/timing/text_paint_timing_detector.cc
+++ b/third_party/blink/renderer/core/paint/timing/text_paint_timing_detector.cc
@@ -14,6 +14,7 @@
 #include "third_party/blink/renderer/core/paint/timing/paint_timing.h"
 #include "third_party/blink/renderer/core/paint/timing/paint_timing_callback_manager.h"
 #include "third_party/blink/renderer/core/paint/timing/paint_timing_detector.h"
+#include "third_party/blink/renderer/core/timing/soft_navigation_context.h"
 #include "third_party/blink/renderer/core/timing/soft_navigation_heuristics.h"
 #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
 #include "third_party/blink/renderer/platform/instrumentation/tracing/traced_value.h"
@@ -23,6 +24,7 @@
 
 void TextRecord::Trace(Visitor* visitor) const {
   visitor->Trace(node_);
+  visitor->Trace(soft_navigation_context_);
 }
 
 TextPaintTimingDetector::TextPaintTimingDetector(
@@ -112,26 +114,52 @@
 }
 
 bool TextPaintTimingDetector::ShouldWalkObject(
-    const LayoutBoxModelObject& object) const {
-  // TODO(crbug.com/933479): Use LayoutObject::GeneratingNode() to include
-  // anonymous objects' rect.
-  Node* node = object.GetNode();
+    const LayoutBoxModelObject& aggregator) {
+  Node* node = aggregator.GetNode();
   if (!node)
     return false;
-  // If we have finished recording Largest Text Paint and the element is a
-  // shadow element or has no elementtiming attribute, then we should not record
-  // its text.
-  if (!IsRecordingLargestTextPaint() &&
-      !TextElementTiming::NeededForTiming(*node)) {
-    return false;
+
+  // Do not walk the object if it has already been recorded, unless it has
+  // specifically been marked for "re-walking".
+  if (recorded_set_.Contains(&aggregator)) {
+    // TODO(crbug.com/40220033): rewalkable_set_ should be empty most of the
+    // time, until we ship the feature for custom fonts.
+    // HashSet::Contains() appears to hash key even when container is empty.
+    return !rewalkable_set_.empty() && rewalkable_set_.Contains(&aggregator);
   }
 
-  if (rewalkable_set_.Contains(&object))
+  // Check if we know for certain that we need to measure this node, first.
+  if (IsRecordingLargestTextPaint() ||
+      TextElementTiming::NeededForTiming(*node)) {
     return true;
+  }
 
-  // This metric defines the size of a text block by its first size, so we
-  // should not walk the object if it has been recorded.
-  return !recorded_set_.Contains(&object);
+  // If we haven't seen this node before, an we aren't recording LCP nor is this
+  // node needed for element timing, the only remaining reason to measure text
+  // timing is for soft navs paints.  We leave this check for last, just because
+  // it might be more expensive.
+  // TODO(crbug.com/423670827): If we cache this value during pre-paint, then we
+  // might not need to worry about it.
+  if (LocalDOMWindow* window = frame_view_->GetFrame().DomWindow()) {
+    if (SoftNavigationHeuristics* heuristics =
+            window->GetSoftNavigationHeuristics();
+        heuristics && heuristics->MaybeGetSoftNavigationContextForTiming(
+                          aggregator.GetNode())) {
+      return true;
+    }
+  }
+
+  // If we've decided not to visit this node for any reason, then let's add it
+  // to the set of recorded nodes, even without measuring its paint, so we never
+  // bother to check it again.
+  // TODO(crbug.com/423670827): Part of the motivation for doing this is so we
+  // don't try to look up context more than once per node.  But then this
+  // content becomes un-recorded for any future observers, and that isn't always
+  // correct (i.e. late application of elementtiming or an Interaction which
+  // toggles content within the node, i.e. adding textContent for the first time
+  // to a previously empty node.)
+  recorded_set_.insert(&aggregator);
+  return false;
 }
 
 void TextPaintTimingDetector::RecordAggregatedText(
@@ -179,16 +207,22 @@
     }
   }
 
-  LocalFrame& frame = frame_view_->GetFrame();
-  if (LocalDOMWindow* window = frame.DomWindow()) {
+  SoftNavigationContext* context = nullptr;
+  if (LocalDOMWindow* window = frame_view_->GetFrame().DomWindow()) {
     if (SoftNavigationHeuristics* heuristics =
             window->GetSoftNavigationHeuristics()) {
-      heuristics->RecordPaint(&frame, mapped_visual_rect, aggregator.GetNode());
+      context = heuristics->MaybeGetSoftNavigationContextForTiming(
+          aggregator.GetNode());
     }
   }
+
   recorded_set_.insert(&aggregator);
-  MaybeRecordTextRecord(aggregator, aggregated_size, property_tree_state,
-                        aggregated_visual_rect, mapped_visual_rect);
+  TextRecord* record = MaybeRecordTextRecord(
+      aggregator, aggregated_size, property_tree_state, aggregated_visual_rect,
+      mapped_visual_rect, context);
+  if (context && record) {
+    context->AddPaintedArea(record);
+  }
   if (std::optional<PaintTimingVisualizer>& visualizer =
           frame_view_->GetPaintTimingDetector().Visualizer()) {
     visualizer->DumpTextDebuggingRect(aggregator, mapped_visual_rect);
@@ -199,12 +233,6 @@
   recording_largest_text_paint_ = false;
 }
 
-void TextPaintTimingDetector::RestartRecordingLargestTextPaint() {
-  recording_largest_text_paint_ = true;
-  texts_queued_for_paint_time_.clear();
-  ltp_manager_->Clear();
-}
-
 void TextPaintTimingDetector::ReportLargestIgnoredText() {
   if (!ltp_manager_)
     return;
@@ -254,7 +282,8 @@
     // queued for paint, we'll set the appropriate |frame_index_|.
     largest_ignored_text_ = MakeGarbageCollected<TextRecord>(
         *object.GetNode(), size, gfx::RectF(), frame_visual_rect,
-        root_visual_rect, 0u, false /* is_needed_for_timing */);
+        root_visual_rect, 0u, /*is_needed_for_timing=*/false,
+        /*soft_navigation_context=*/nullptr);
   }
 }
 
@@ -277,6 +306,7 @@
     }
   }
 
+  bool is_needed_for_lcp = IsRecordingLargestTextPaint();
   bool can_report_timing =
       text_element_timing_ ? text_element_timing_->CanReportElements() : false;
   HeapVector<Member<const LayoutObject>> keys_to_be_removed;
@@ -290,7 +320,7 @@
       text_element_timing_->OnTextObjectPainted(*record, paint_timing_info);
     }
 
-    if (ltp_manager_ && (record->recorded_size > 0u)) {
+    if (is_needed_for_lcp && ltp_manager_ && (record->recorded_size > 0u)) {
       ltp_manager_->MaybeUpdateLargestText(record);
     }
     keys_to_be_removed.push_back(key);
@@ -303,16 +333,19 @@
     const uint64_t& visual_size,
     const PropertyTreeStateOrAlias& property_tree_state,
     const gfx::Rect& frame_visual_rect,
-    const gfx::RectF& root_visual_rect) {
+    const gfx::RectF& root_visual_rect,
+    SoftNavigationContext* context) {
   Node* node = object.GetNode();
   DCHECK(node);
 
   bool is_needed_for_lcp = IsRecordingLargestTextPaint() && visual_size > 0u;
   bool is_needed_for_element_timing = TextElementTiming::NeededForTiming(*node);
+  bool is_needed_for_soft_navs = context != nullptr;
 
-  // If the node is not required by LCP and not required by ElementTiming, we
-  // can bail out early.
-  if (!is_needed_for_lcp && !is_needed_for_element_timing) {
+  // If the node is not required by LCP and not required by ElementTiming,
+  // we can bail out early.
+  if (!is_needed_for_lcp && !is_needed_for_element_timing &&
+      !is_needed_for_soft_navs) {
     return nullptr;
   }
 
@@ -320,14 +353,14 @@
   if (visual_size == 0u) {
     record = MakeGarbageCollected<TextRecord>(
         *node, visual_size, gfx::RectF(), gfx::Rect(), gfx::RectF(),
-        frame_index_, is_needed_for_element_timing);
+        frame_index_, is_needed_for_element_timing, context);
   } else {
     record = MakeGarbageCollected<TextRecord>(
         *node, visual_size,
         TextElementTiming::ComputeIntersectionRect(
             object, frame_visual_rect, property_tree_state, frame_view_),
         frame_visual_rect, root_visual_rect, frame_index_,
-        is_needed_for_element_timing);
+        is_needed_for_element_timing, context);
   }
   QueueToMeasurePaintTime(object, record);
   return record;
diff --git a/third_party/blink/renderer/core/paint/timing/text_paint_timing_detector.h b/third_party/blink/renderer/core/paint/timing/text_paint_timing_detector.h
index 4b86df1..58e90cad 100644
--- a/third_party/blink/renderer/core/paint/timing/text_paint_timing_detector.h
+++ b/third_party/blink/renderer/core/paint/timing/text_paint_timing_detector.h
@@ -23,8 +23,9 @@
 class TextElementTiming;
 class TracedValue;
 struct DOMPaintTimingInfo;
+class SoftNavigationContext;
 
-class TextRecord final : public GarbageCollected<TextRecord> {
+class CORE_EXPORT TextRecord final : public GarbageCollected<TextRecord> {
  public:
   TextRecord(Node& node,
              uint64_t new_recorded_size,
@@ -32,12 +33,15 @@
              const gfx::Rect& frame_visual_rect,
              const gfx::RectF& root_visual_rect,
              uint32_t frame_index,
-             bool is_needed_for_timing)
+             bool is_needed_for_timing,
+             SoftNavigationContext* soft_navigation_context)
       : node_(&node),
         recorded_size(new_recorded_size),
         frame_index_(frame_index),
         element_timing_rect_(element_timing_rect),
-        is_needed_for_timing_(is_needed_for_timing) {
+        root_visual_rect_(root_visual_rect),
+        is_needed_for_timing_(is_needed_for_timing),
+        soft_navigation_context_(soft_navigation_context) {
     if (PaintTimingVisualizer::IsTracingEnabled()) {
       lcp_rect_info_ = std::make_unique<LCPRectInfo>(
           frame_visual_rect, gfx::ToRoundedRect(root_visual_rect));
@@ -52,11 +56,13 @@
   uint64_t recorded_size = 0;
   uint32_t frame_index_ = 0;
   gfx::RectF element_timing_rect_;
+  gfx::RectF root_visual_rect_;
   std::unique_ptr<LCPRectInfo> lcp_rect_info_;
   // The time of the first paint after fully loaded.
   base::TimeTicks paint_time = base::TimeTicks();
   DOMPaintTimingInfo paint_timing_info;
   bool is_needed_for_timing_ = false;
+  WeakMember<SoftNavigationContext> soft_navigation_context_;
 };
 
 class CORE_EXPORT LargestTextPaintManager final
@@ -86,12 +92,6 @@
     return std::move(largest_ignored_text_);
   }
 
-  void Clear() {
-    count_candidates_ = 0;
-    largest_text_.Clear();
-    largest_ignored_text_.Clear();
-  }
-
   void Trace(Visitor*) const;
 
  private:
@@ -135,7 +135,7 @@
   TextPaintTimingDetector(const TextPaintTimingDetector&) = delete;
   TextPaintTimingDetector& operator=(const TextPaintTimingDetector&) = delete;
 
-  bool ShouldWalkObject(const LayoutBoxModelObject&) const;
+  bool ShouldWalkObject(const LayoutBoxModelObject&);
   void RecordAggregatedText(const LayoutBoxModelObject& aggregator,
                             const gfx::Rect& aggregated_visual_rect,
                             const PropertyTreeStateOrAlias&);
@@ -144,7 +144,6 @@
   TakePaintTimingCallback();
   void LayoutObjectWillBeDestroyed(const LayoutObject&);
   void StopRecordingLargestTextPaint();
-  void RestartRecordingLargestTextPaint();
   void ResetCallbackManager(PaintTimingCallbackManager* manager) {
     callback_manager_ = manager;
   }
@@ -169,7 +168,8 @@
       const uint64_t& visual_size,
       const PropertyTreeStateOrAlias& property_tree_state,
       const gfx::Rect& frame_visual_rect,
-      const gfx::RectF& root_visual_rect);
+      const gfx::RectF& root_visual_rect,
+      SoftNavigationContext* context);
   inline void QueueToMeasurePaintTime(const LayoutObject& object,
                                       TextRecord* record) {
     texts_queued_for_paint_time_.insert(&object, record);
diff --git a/third_party/blink/renderer/core/style/computed_style.cc b/third_party/blink/renderer/core/style/computed_style.cc
index e767e30..79e9b66 100644
--- a/third_party/blink/renderer/core/style/computed_style.cc
+++ b/third_party/blink/renderer/core/style/computed_style.cc
@@ -1942,7 +1942,7 @@
     return text;
   }
   UChar character = text[0];
-  UChar32 transformed_char = ItalicMathVariant(text[0]);
+  UChar32 transformed_char = unicode::ItalicMathVariant(text[0]);
   if (transformed_char == static_cast<UChar32>(character)) {
     return text;
   }
diff --git a/third_party/blink/renderer/core/timing/performance.cc b/third_party/blink/renderer/core/timing/performance.cc
index ee864f3..bfbc508 100644
--- a/third_party/blink/renderer/core/timing/performance.cc
+++ b/third_party/blink/renderer/core/timing/performance.cc
@@ -1283,11 +1283,10 @@
 }
 
 void Performance::AddPaintTiming(PerformancePaintTiming::PaintType type,
-                                 const DOMPaintTimingInfo& paint_timing_info,
-                                 bool is_triggered_by_soft_navigation) {
+                                 const DOMPaintTimingInfo& paint_timing_info) {
   PerformancePaintTiming* entry = MakeGarbageCollected<PerformancePaintTiming>(
-      type, paint_timing_info, DynamicTo<LocalDOMWindow>(GetExecutionContext()),
-      is_triggered_by_soft_navigation);
+      type, paint_timing_info,
+      DynamicTo<LocalDOMWindow>(GetExecutionContext()));
   DCHECK((type == PerformancePaintTiming::PaintType::kFirstPaint) ||
          (type == PerformancePaintTiming::PaintType::kFirstContentfulPaint));
 
diff --git a/third_party/blink/renderer/core/timing/performance.h b/third_party/blink/renderer/core/timing/performance.h
index 178f977..f5a3a526 100644
--- a/third_party/blink/renderer/core/timing/performance.h
+++ b/third_party/blink/renderer/core/timing/performance.h
@@ -386,8 +386,7 @@
   virtual void BuildJSONValue(V8ObjectBuilder&) const;
 
   void AddPaintTiming(PerformancePaintTiming::PaintType,
-                      const DOMPaintTimingInfo& paint_timing_info,
-                      bool is_triggered_by_soft_navigation);
+                      const DOMPaintTimingInfo& paint_timing_info);
 
   PerformanceEntryVector resource_timing_buffer_;
   // The secondary RT buffer, used to store incoming entries after the main
diff --git a/third_party/blink/renderer/core/timing/performance_paint_timing.cc b/third_party/blink/renderer/core/timing/performance_paint_timing.cc
index 73364a7..ecb9a3c3 100644
--- a/third_party/blink/renderer/core/timing/performance_paint_timing.cc
+++ b/third_party/blink/renderer/core/timing/performance_paint_timing.cc
@@ -36,8 +36,7 @@
 PerformancePaintTiming::PerformancePaintTiming(
     PaintType type,
     const DOMPaintTimingInfo& paint_timing_info,
-    DOMWindow* source,
-    bool is_triggered_by_soft_navigation)
+    DOMWindow* source)
     : PerformanceEntry(
           FromPaintTypeToString(type),
           // https://w3c.github.io/paint-timing/#report-paint-timing
@@ -45,8 +44,7 @@
           // given paintTimingInfo.
           paint_timing_info.presentation_time,
           paint_timing_info.presentation_time,
-          source,
-          is_triggered_by_soft_navigation) {
+          source) {
   SetPaintTimingInfo(paint_timing_info);
 }
 
diff --git a/third_party/blink/renderer/core/timing/performance_paint_timing.h b/third_party/blink/renderer/core/timing/performance_paint_timing.h
index 8b64aff..a5d0ae0 100644
--- a/third_party/blink/renderer/core/timing/performance_paint_timing.h
+++ b/third_party/blink/renderer/core/timing/performance_paint_timing.h
@@ -19,8 +19,7 @@
 
   PerformancePaintTiming(PaintType,
                          const DOMPaintTimingInfo& paint_timing_info,
-                         DOMWindow* source,
-                         bool is_triggered_by_soft_navigation);
+                         DOMWindow* source);
   ~PerformancePaintTiming() override;
 
   const AtomicString& entryType() const override;
diff --git a/third_party/blink/renderer/core/timing/soft_navigation_context.cc b/third_party/blink/renderer/core/timing/soft_navigation_context.cc
index a234388c..bb9023e5 100644
--- a/third_party/blink/renderer/core/timing/soft_navigation_context.cc
+++ b/third_party/blink/renderer/core/timing/soft_navigation_context.cc
@@ -8,14 +8,21 @@
 #include "third_party/blink/renderer/core/dom/container_node.h"
 #include "third_party/blink/renderer/core/dom/node.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
+#include "third_party/blink/renderer/core/paint/timing/image_paint_timing_detector.h"
+#include "third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h"
+#include "third_party/blink/renderer/core/paint/timing/text_paint_timing_detector.h"
+#include "third_party/blink/renderer/core/timing/dom_window_performance.h"
 
 namespace blink {
 
 uint64_t SoftNavigationContext::last_context_id_ = 0;
 
 SoftNavigationContext::SoftNavigationContext(
+    LocalDOMWindow& window,
     features::SoftNavigationHeuristicsMode mode)
-    : paint_attribution_mode_(mode) {}
+    : paint_attribution_mode_(mode),
+      lcp_calculator_(MakeGarbageCollected<LargestContentfulPaintCalculator>(
+          DOMWindowPerformance::performance(window))) {}
 
 void SoftNavigationContext::AddModifiedNode(Node* node) {
   node->SetIsModifiedBySoftNavigation();
@@ -38,9 +45,64 @@
   }
 }
 
-bool SoftNavigationContext::AddPaintedArea(Node* node,
-                                           const gfx::RectF& rect,
-                                           bool is_newest_context) {
+bool SoftNavigationContext::IsNeededForTiming(Node* node) {
+  if (!node) {
+    return false;
+  }
+  for (Node* current_node = node; current_node;
+       current_node = current_node->parentNode()) {
+    if (current_node == known_not_related_parent_) {
+      return false;
+    }
+    // If the current_node is known modified, it is a container root.
+    if (modified_nodes_.Contains(current_node)) {
+      return true;
+    }
+    // For now, do not "tree walk" when in basic mode.
+    if (paint_attribution_mode_ ==
+        features::SoftNavigationHeuristicsMode::kBasic) {
+      break;
+    }
+  }
+  // This node was not part of a container root for this context.
+  // Let's cache this node's parent node, so if any of this node's siblings
+  // paint next, we can finish this check quicker for them.
+  if (Node* parent = node->parentNode()) {
+    known_not_related_parent_ = parent;
+  }
+  return false;
+}
+
+bool SoftNavigationContext::AddPaintedArea(TextRecord* text_record) {
+  Node* node = text_record->node_;
+  const gfx::RectF& rect = text_record->root_visual_rect_;
+  bool is_attributable = AddPaintedAreaInternal(node, rect);
+  if (is_attributable) {
+    if (!largest_text_ ||
+        largest_text_->recorded_size < text_record->recorded_size) {
+      largest_text_ = text_record;
+    }
+  }
+  return is_attributable;
+}
+
+bool SoftNavigationContext::AddPaintedArea(ImageRecord* image_record) {
+  Node* node = Node::FromDomNodeId(image_record->node_id);
+  const gfx::RectF& rect = image_record->root_visual_rect;
+  bool is_attributable = AddPaintedAreaInternal(node, rect);
+  if (is_attributable) {
+    if (!largest_image_ ||
+        largest_image_->recorded_size < image_record->recorded_size) {
+      largest_image_ = image_record;
+    }
+  }
+  return is_attributable;
+}
+
+bool SoftNavigationContext::AddPaintedAreaInternal(Node* node,
+                                                   const gfx::RectF& rect) {
+  DCHECK(IsNeededForTiming(node));
+
   uint64_t painted_area = rect.size().GetArea();
 
   if (already_painted_modified_nodes_.Contains(node)) {
@@ -50,53 +112,18 @@
     return false;
   }
 
-  // Iterate up the dom tree:
-  for (Node* current_node = node; current_node;
-       current_node = current_node->parentNode()) {
-    if (current_node == known_not_related_parent_) {
-      TRACE_EVENT_INSTANT(
-          TRACE_DISABLED_BY_DEFAULT("loading"),
-          "SoftNavigationContext::AddPaintedAreaWithEarlyExitTreeWalk",
-          "context", this);
-      break;
-    }
-    // If the current_node is known modified, it is a container root, and this
-    // paint counts.
-    if (modified_nodes_.Contains(current_node)) {
-      already_painted_modified_nodes_.insert(node);
-      // If this is the first paint this animation frame, trace it.
-      if (painted_area_ == painted_area_last_animation_frame_) {
-        // TODO(crbug.com/353218760): Add support for reporting every single
-        // paint.
-        TRACE_EVENT_INSTANT(
-            TRACE_DISABLED_BY_DEFAULT("loading"),
-            "SoftNavigationContext::FirstAttributablePaintInAnimationFrame",
-            "context", this);
-      }
-      painted_area_ += painted_area;
-      return true;
-    }
-
-    // For now, do not "tree walk" when in basic mode.
-    if (paint_attribution_mode_ ==
-        features::SoftNavigationHeuristicsMode::kBasic) {
-      break;
-    }
+  already_painted_modified_nodes_.insert(node);
+  // If this is the first paint this animation frame, trace it.
+  if (painted_area_ == painted_area_last_animation_frame_) {
+    // TODO(crbug.com/353218760): Add support for reporting every single
+    // paint.
+    TRACE_EVENT_INSTANT(
+        TRACE_DISABLED_BY_DEFAULT("loading"),
+        "SoftNavigationContext::FirstAttributablePaintInAnimationFrame",
+        "context", this);
   }
-
-  // This node was not part of a container root for this context.
-  // Let's cache this node's parent node, so if any of this node's siblings
-  // paint next, we can finish this check quicker for them.
-  known_not_related_parent_ = node->parentNode();
-
-  if (is_newest_context) {
-    // We want to know how much paint the page is doing that isn't attributed.
-    // We only want to do this for a single (most recent) context, in order to
-    // never double count painted areas.
-    unattributed_area_ += painted_area;
-  }
-
-  return false;
+  painted_area_ += painted_area;
+  return true;
 }
 
 bool SoftNavigationContext::SatisfiesSoftNavNonPaintCriteria() const {
@@ -140,6 +167,39 @@
   return new_painted_area > 0;
 }
 
+// TODO(crbug.com/419386429): This gets called after each new presentation time
+// update, but this might have a range of deficiencies:
+//
+// 1. Candidate records might get replaced between paint and presentation.
+//
+// `largest_text_` and `largest_image_` are updated in `AddPaintedArea` from
+// Paint stage of rendering. But `UpdateSoftLcpCandidate` is called after we
+// receive frame presentation time feedback (via `PaintTimingMixin`). It is
+// possible that we replace the current largest* paint record with a "pending"
+// candidate, but unrelated to the presentation feedback of this
+// `UpdateSoftLcpCandidate`. We should only report fully recorded paint records.
+// One option is to manage a largest pending/painted recortd (like LCP
+// calculator), or, just skip this next step if the candidates aren't done.
+//
+// 2. We might not be ready to Emit LCP candidates yet, and we might not get
+// another chance later.
+//
+// Right now we will skip emitting LCP candidates until after soft-navigation
+// entry and NavigationID are incremented.  But, this might happen after a few
+// frames/paints.  Potentially unlikely given the low paint area requirement
+// right now, but increasingly likely as we bump that up.
+// We might want to also call `UpdateSoftLcpCandidate()` as soon as we emit
+// Soft-nav entry if we already have candidates to report.  Similar to above,
+// there are concerns with reporting Candidates after Paint but before
+// Presentation.
+void SoftNavigationContext::UpdateSoftLcpCandidate() {
+  if (!WasEmitted()) {
+    return;
+  }
+  lcp_calculator_->UpdateWebExposedLargestContentfulPaintIfNeeded(
+      largest_text_, largest_image_, true);
+}
+
 void SoftNavigationContext::WriteIntoTrace(
     perfetto::TracedValue context) const {
   perfetto::TracedDictionary dict = std::move(context).WriteDictionary();
@@ -153,13 +213,15 @@
   dict.Add("domModifications", num_modified_dom_nodes_);
   dict.Add("paintedArea", painted_area_);
   dict.Add("repaintedArea", repainted_area_);
-  dict.Add("unattributedPaintedArea", unattributed_area_);
 }
 
 void SoftNavigationContext::Trace(Visitor* visitor) const {
   visitor->Trace(modified_nodes_);
   visitor->Trace(already_painted_modified_nodes_);
   visitor->Trace(known_not_related_parent_);
+  visitor->Trace(lcp_calculator_);
+  visitor->Trace(largest_text_);
+  visitor->Trace(largest_image_);
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/timing/soft_navigation_context.h b/third_party/blink/renderer/core/timing/soft_navigation_context.h
index 0cd3757..9236818 100644
--- a/third_party/blink/renderer/core/timing/soft_navigation_context.h
+++ b/third_party/blink/renderer/core/timing/soft_navigation_context.h
@@ -19,12 +19,17 @@
 namespace blink {
 
 class Node;
+class TextRecord;
+class ImageRecord;
+class LargestContentfulPaintCalculator;
+
 class CORE_EXPORT SoftNavigationContext
     : public GarbageCollected<SoftNavigationContext> {
   static uint64_t last_context_id_;
 
  public:
-  explicit SoftNavigationContext(features::SoftNavigationHeuristicsMode);
+  explicit SoftNavigationContext(LocalDOMWindow& window,
+                                 features::SoftNavigationHeuristicsMode);
 
   bool IsMostRecentlyCreatedContext() const {
     return context_id_ == last_context_id_;
@@ -57,15 +62,16 @@
 
   uint64_t PaintedArea() const { return painted_area_; }
 
+  // Returns true if this Context is involved in modifying the container root
+  // for this Node*.
+  bool IsNeededForTiming(Node* node);
   // Reports a new contentful paint area to this context, and the Node painted.
-  // Returns true if we update the total attributed area (meaning this context
-  // was involved in modifying this dom node, and we grew the painted region).
-  // Return value is used to check if it is worthwhile to check for "sufficient
-  // paints" (to emit a new soft-nav entry).
-  bool AddPaintedArea(Node* node,
-                      const gfx::RectF& rect,
-                      bool is_newest_context);
+  bool AddPaintedArea(TextRecord*);
+  bool AddPaintedArea(ImageRecord*);
+  // Returns true if we update the total attributed area this animation frame.
+  // Used to check if it is worthwhile to call `SatisfiesSoftNavPaintCriteria`.
   bool OnPaintFinished();
+  void UpdateSoftLcpCandidate();
 
   bool SatisfiesSoftNavNonPaintCriteria() const;
   bool SatisfiesSoftNavPaintCriteria(uint64_t required_paint_area) const;
@@ -75,6 +81,8 @@
   void Trace(Visitor* visitor) const;
 
  private:
+  bool AddPaintedAreaInternal(Node* node, const gfx::RectF& rect);
+
   // Pre-Increment `last_context_id_` such that the newest context uses the
   // largest value and can be used to identify the most recent context.
   const uint64_t context_id_ = ++last_context_id_;
@@ -88,12 +96,15 @@
   blink::HeapHashSet<WeakMember<Node>> modified_nodes_;
   blink::HeapHashSet<WeakMember<Node>> already_painted_modified_nodes_;
 
+  Member<LargestContentfulPaintCalculator> lcp_calculator_;
+  Member<TextRecord> largest_text_;
+  Member<ImageRecord> largest_image_;
+
   // Elements of `modified_nodes_` can get GC-ed, so we need to keep a count of
   // the total nodes modified.
   size_t num_modified_dom_nodes_ = 0;
   uint64_t painted_area_ = 0;
   uint64_t repainted_area_ = 0;
-  uint64_t unattributed_area_ = 0;
 
   size_t num_modified_dom_nodes_last_animation_frame_ = 0;
   size_t num_live_nodes_last_animation_frame_ = 0;
diff --git a/third_party/blink/renderer/core/timing/soft_navigation_heuristics.cc b/third_party/blink/renderer/core/timing/soft_navigation_heuristics.cc
index c37235e..21e979a 100644
--- a/third_party/blink/renderer/core/timing/soft_navigation_heuristics.cc
+++ b/third_party/blink/renderer/core/timing/soft_navigation_heuristics.cc
@@ -399,8 +399,6 @@
   auto* performance = DOMWindowPerformance::performance(*window_.Get());
   performance->AddSoftNavigationEntry(AtomicString(context->InitialUrl()),
                                       context->UserInteractionTimestamp());
-
-  CommitPreviousPaintTimings(frame);
   ReportSoftNavigationToMetrics(frame, context);
 
   TRACE_EVENT_INSTANT("scheduler,devtools.timeline,loading",
@@ -411,12 +409,13 @@
   return true;
 }
 
-void SoftNavigationHeuristics::RecordPaint(LocalFrame* frame,
-                                           const gfx::RectF& rect,
-                                           Node* node) {
-  if (context_for_current_url_) {
-    context_for_current_url_->AddPaintedArea(node, rect, true);
+SoftNavigationContext*
+SoftNavigationHeuristics::MaybeGetSoftNavigationContextForTiming(Node* node) {
+  if (context_for_current_url_ &&
+      context_for_current_url_->IsNeededForTiming(node)) {
+    return context_for_current_url_;
   }
+  return nullptr;
 }
 
 void SoftNavigationHeuristics::OnPaintFinished() {
@@ -427,6 +426,17 @@
   }
 }
 
+void SoftNavigationHeuristics::UpdateSoftLcpCandidate() {
+  if (!context_for_current_url_) {
+    return;
+  }
+  // Performance timeline won't allow emitting LCP entries without this flag,
+  // but we can save a lot of needless work by also just not even trying.
+  if (RuntimeEnabledFeatures::SoftNavigationHeuristicsEnabled(window_)) {
+    context_for_current_url_->UpdateSoftLcpCandidate();
+  }
+}
+
 void SoftNavigationHeuristics::ReportSoftNavigationToMetrics(
     LocalFrame* frame,
     SoftNavigationContext* context) const {
@@ -456,34 +466,6 @@
                                 SoftNavigationOutcome::kSoftNavigationDetected);
 }
 
-void SoftNavigationHeuristics::ResetPaintTimingsIfNeeded() {
-  LocalFrame* frame = GetLocalFrameIfOutermostAndNotDetached();
-  if (!frame) {
-    return;
-  }
-  LocalFrameView* local_frame_view = frame->View();
-  CHECK(local_frame_view);
-  if (RuntimeEnabledFeatures::SoftNavigationHeuristicsEnabled(window_)) {
-    local_frame_view->GetPaintTimingDetector().RestartRecordingLCP();
-  }
-
-  local_frame_view->GetPaintTimingDetector().RestartRecordingLCPToUkm();
-}
-
-// Once all the soft navigation conditions are met (verified in
-// `EmitSoftNavigationEntryIfAllConditionsMet()`), the previous paints are
-// committed, to make sure accumulated FP, FCP and LCP entries are properly
-// fired.
-void SoftNavigationHeuristics::CommitPreviousPaintTimings(LocalFrame* frame) {
-  CHECK(frame && frame->IsOutermostMainFrame());
-  LocalFrameView* local_frame_view = frame->View();
-
-  CHECK(local_frame_view);
-
-  local_frame_view->GetPaintTimingDetector().SoftNavigationDetected(
-      window_.Get());
-}
-
 void SoftNavigationHeuristics::Trace(Visitor* visitor) const {
   visitor->Trace(active_interaction_context_);
   visitor->Trace(context_for_current_url_);
@@ -590,17 +572,13 @@
     // "new interaction" (i.e. keydown), but will create a new one if that has
     // been cleared, which can happen in tests.
     if (IsInteractionStart(type) || !active_interaction_context_) {
-      active_interaction_context_ =
-          MakeGarbageCollected<SoftNavigationContext>(paint_attribution_mode_);
+      active_interaction_context_ = MakeGarbageCollected<SoftNavigationContext>(
+          *window_, paint_attribution_mode_);
       potential_soft_navigations_.push_back(active_interaction_context_);
       TRACE_EVENT_INSTANT(TRACE_DISABLED_BY_DEFAULT("loading"),
                           "SoftNavigationHeuristics::CreateNewContext",
                           "context", *active_interaction_context_);
     }
-
-    // Ensure that paints would be reset, so that paint recording would continue
-    // despite the user interaction.
-    ResetPaintTimingsIfNeeded();
   }
   CHECK(active_interaction_context_.Get());
 
diff --git a/third_party/blink/renderer/core/timing/soft_navigation_heuristics.h b/third_party/blink/renderer/core/timing/soft_navigation_heuristics.h
index 8d143c3d..45821f05 100644
--- a/third_party/blink/renderer/core/timing/soft_navigation_heuristics.h
+++ b/third_party/blink/renderer/core/timing/soft_navigation_heuristics.h
@@ -100,9 +100,9 @@
   // TaskAttributionTracker::Observer's implementation.
   void OnCreateTaskScope(scheduler::TaskAttributionInfo&) override;
 
-  void RecordPaint(LocalFrame*, const gfx::RectF& rect, Node* node);
-
+  SoftNavigationContext* MaybeGetSoftNavigationContextForTiming(Node* node);
   void OnPaintFinished();
+  void UpdateSoftLcpCandidate();
 
   // Returns an `EventScope` suitable for navigation. Used for navigations not
   // yet associated with an event.
@@ -130,8 +130,6 @@
       SoftNavigationContext*) const;
   SoftNavigationContext* GetSoftNavigationContextForCurrentTask() const;
 
-  void ResetPaintTimingsIfNeeded();
-  void CommitPreviousPaintTimings(LocalFrame*);
   bool EmitSoftNavigationEntryIfAllConditionsMet(SoftNavigationContext*);
   LocalFrame* GetLocalFrameIfOutermostAndNotDetached() const;
   void OnSoftNavigationEventScopeDestroyed(const EventScope&);
diff --git a/third_party/blink/renderer/core/timing/soft_navigation_heuristics_test.cc b/third_party/blink/renderer/core/timing/soft_navigation_heuristics_test.cc
index e2dd01a..593d2d0 100644
--- a/third_party/blink/renderer/core/timing/soft_navigation_heuristics_test.cc
+++ b/third_party/blink/renderer/core/timing/soft_navigation_heuristics_test.cc
@@ -19,6 +19,7 @@
 #include "third_party/blink/renderer/core/events/mouse_event.h"
 #include "third_party/blink/renderer/core/html/html_body_element.h"
 #include "third_party/blink/renderer/core/html/html_div_element.h"
+#include "third_party/blink/renderer/core/paint/timing/text_paint_timing_detector.h"
 #include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
 #include "third_party/blink/renderer/core/timing/soft_navigation_context.h"
 #include "third_party/blink/renderer/platform/bindings/script_state.h"
@@ -26,7 +27,6 @@
 #include "third_party/blink/renderer/platform/scheduler/public/task_attribution_info.h"
 #include "third_party/blink/renderer/platform/scheduler/public/task_attribution_tracker.h"
 #include "third_party/blink/renderer/platform/testing/task_environment.h"
-
 namespace blink {
 
 using TaskScope = scheduler::TaskAttributionTracker::TaskScope;
@@ -56,16 +56,6 @@
     return document->CreateRawElement(html_names::kDivTag);
   }
 
-  void ReportPaintRectForTest(SoftNavigationHeuristics* heuristics,
-                              Node* node) {
-    ScriptState* script_state = GetScriptStateForTest();
-    LocalDOMWindow* window = LocalDOMWindow::From(script_state);
-    LocalFrame* frame = window->GetFrame();
-    gfx::RectF rect{1000, 1000};
-    heuristics->RecordPaint(frame, rect, node);
-    return heuristics->OnPaintFinished();
-  }
-
   ScriptState* GetScriptStateForTest() {
     return ToScriptStateForMainWorld(page_holder_->GetDocument().GetFrame());
   }
@@ -353,7 +343,13 @@
 
   // Simulate a paint in a separate task.
   {
-    ReportPaintRectForTest(heuristics, node1);
+    TextRecord* record = MakeGarbageCollected<TextRecord>(
+        *node1, 0, gfx::RectF(1000, 1000), gfx::Rect(1000, 1000),
+        gfx::RectF(1000, 1000),
+        /* frame_index= */ 0,
+        /* is_needed_for_timing= */ false, context);
+    context->AddPaintedArea(record);
+    heuristics->OnPaintFinished();
     EXPECT_TRUE(context->SatisfiesSoftNavPaintCriteria(1));
     EXPECT_EQ(heuristics->SoftNavigationCount(), 1u);
   }
@@ -369,13 +365,19 @@
 
   // And another paint
   {
-    ReportPaintRectForTest(heuristics, node2);
+    TextRecord* record = MakeGarbageCollected<TextRecord>(
+        *node2, 0, gfx::RectF(1000, 1000), gfx::Rect(1000, 1000),
+        gfx::RectF(1000, 1000),
+        /* frame_index= */ 0,
+        /* is_needed_for_timing= */ false, context);
+    context->AddPaintedArea(record);
+    heuristics->OnPaintFinished();
+    EXPECT_TRUE(context->SatisfiesSoftNavPaintCriteria(1));
+    // Should still just have one single soft-nav because a single context
+    // with a single Interaction should only emit once, even if it e.g.
+    // navigates twice (i.e. client-side redirects).
+    EXPECT_EQ(heuristics->SoftNavigationCount(), 1u);
   }
-
-  // Should still just have one single soft-nav because a single context
-  // with a single Interaction should only emit once, even if it e.g. navigates
-  // twice (i.e. client-side redirects).
-  EXPECT_EQ(heuristics->SoftNavigationCount(), 1u);
 }
 
 TEST_F(SoftNavigationHeuristicsTest, AsyncSameDocumentNavigation) {
diff --git a/third_party/blink/renderer/core/timing/window_performance.cc b/third_party/blink/renderer/core/timing/window_performance.cc
index 81c4fd9..3e46f9ff 100644
--- a/third_party/blink/renderer/core/timing/window_performance.cc
+++ b/third_party/blink/renderer/core/timing/window_performance.cc
@@ -1304,17 +1304,15 @@
 }
 
 void WindowPerformance::AddFirstPaintTiming(
-    const DOMPaintTimingInfo& paint_timing_info,
-    bool is_triggered_by_soft_navigation) {
+    const DOMPaintTimingInfo& paint_timing_info) {
   AddPaintTiming(PerformancePaintTiming::PaintType::kFirstPaint,
-                 paint_timing_info, is_triggered_by_soft_navigation);
+                 paint_timing_info);
 }
 
 void WindowPerformance::AddFirstContentfulPaintTiming(
-    const DOMPaintTimingInfo& paint_timing_info,
-    bool is_triggered_by_soft_navigation) {
+    const DOMPaintTimingInfo& paint_timing_info) {
   AddPaintTiming(PerformancePaintTiming::PaintType::kFirstContentfulPaint,
-                 paint_timing_info, is_triggered_by_soft_navigation);
+                 paint_timing_info);
 }
 
 void WindowPerformance::AddLongAnimationFrameEntry(PerformanceEntry* entry) {
@@ -1487,6 +1485,11 @@
     const String& url,
     Element* element,
     bool is_triggered_by_soft_navigation) {
+  if (is_triggered_by_soft_navigation &&
+      !RuntimeEnabledFeatures::SoftNavigationHeuristicsEnabled(
+          GetExecutionContext())) {
+    return;
+  }
   DOMHighResTimeStamp load_timestamp =
       MonotonicTimeToDOMHighResTimeStamp(load_time);
 
diff --git a/third_party/blink/renderer/core/timing/window_performance.h b/third_party/blink/renderer/core/timing/window_performance.h
index 00040ed..803c010 100644
--- a/third_party/blink/renderer/core/timing/window_performance.h
+++ b/third_party/blink/renderer/core/timing/window_performance.h
@@ -141,12 +141,10 @@
   void QueueLongAnimationFrameTiming(
       AnimationFrameTimingInfo*,
       std::optional<DOMPaintTimingInfo> paint_timing_info = std::nullopt);
-  void AddFirstPaintTiming(const DOMPaintTimingInfo& paint_timing_info,
-                           bool is_triggered_by_soft_navigation);
+  void AddFirstPaintTiming(const DOMPaintTimingInfo& paint_timing_info);
 
   void AddFirstContentfulPaintTiming(
-      const DOMPaintTimingInfo& paint_timing_info,
-      bool is_triggered_by_soft_navigation);
+      const DOMPaintTimingInfo& paint_timing_info);
 
   // PerformanceMonitor::Client implementation.
   void ReportLongTask(base::TimeTicks start_time,
diff --git a/third_party/blink/renderer/platform/math_transform_fuzzer.cc b/third_party/blink/renderer/platform/math_transform_fuzzer.cc
index ad46f3a..5bdd7f0 100644
--- a/third_party/blink/renderer/platform/math_transform_fuzzer.cc
+++ b/third_party/blink/renderer/platform/math_transform_fuzzer.cc
@@ -22,7 +22,7 @@
   blink::UTF16TextIterator text_iterator(content.Span16());
   UChar32 code_point;
   while (text_iterator.Consume(code_point)) {
-    WTF::unicode::ItalicMathVariant(code_point);
+    blink::unicode::ItalicMathVariant(code_point);
     text_iterator.Advance();
   }
   return 0;
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 3ced760..7ce65e0 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -2898,6 +2898,10 @@
       status: "stable",
     },
     {
+      name: "LayoutNewReplacedLogic",
+      status: "stable",
+    },
+    {
       name: "LayoutNGShapeCache",
       status: "stable",
       base_feature: "LayoutNGShapeCache",
diff --git a/third_party/blink/renderer/platform/wtf/text/math_transform.cc b/third_party/blink/renderer/platform/wtf/text/math_transform.cc
index fdd96c2..975432b9 100644
--- a/third_party/blink/renderer/platform/wtf/text/math_transform.cc
+++ b/third_party/blink/renderer/platform/wtf/text/math_transform.cc
@@ -8,7 +8,7 @@
 #include "third_party/blink/renderer/platform/wtf/text/ascii_ctype.h"
 #include "third_party/blink/renderer/platform/wtf/text/character_names.h"
 
-namespace WTF {
+namespace blink {
 namespace unicode {
 
 static UChar32 mathVariantGreek(UChar32 code_point, UChar32 base_char) {
@@ -124,4 +124,4 @@
 }
 
 }  // namespace unicode
-}  // namespace WTF
+}  // namespace blink
diff --git a/third_party/blink/renderer/platform/wtf/text/math_transform.h b/third_party/blink/renderer/platform/wtf/text/math_transform.h
index 6a994c0..d12f782 100644
--- a/third_party/blink/renderer/platform/wtf/text/math_transform.h
+++ b/third_party/blink/renderer/platform/wtf/text/math_transform.h
@@ -9,7 +9,7 @@
 #include "third_party/blink/renderer/platform/wtf/wtf_export.h"
 #include "third_party/blink/renderer/platform/wtf/wtf_size_t.h"
 
-namespace WTF {
+namespace blink {
 namespace unicode {
 
 // Performs the character mapping needed to implement MathML's mathvariant
@@ -28,8 +28,6 @@
 WTF_EXPORT UChar32 ItalicMathVariant(UChar32 code_point);
 
 }  // namespace unicode
-}  // namespace WTF
-
-using WTF::unicode::ItalicMathVariant;
+}  // namespace blink
 
 #endif  // THIRD_PARTY_BLINK_RENDERER_PLATFORM_WTF_TEXT_MATH_TRANSFORM_H_
diff --git a/third_party/blink/renderer/platform/wtf/text/math_transform_test.cc b/third_party/blink/renderer/platform/wtf/text/math_transform_test.cc
index abe7513..b5ed658 100644
--- a/third_party/blink/renderer/platform/wtf/text/math_transform_test.cc
+++ b/third_party/blink/renderer/platform/wtf/text/math_transform_test.cc
@@ -6,10 +6,9 @@
 
 #include "testing/gtest/include/gtest/gtest.h"
 
-namespace WTF {
+namespace blink {
 
 namespace {
-
 // https://w3c.github.io/mathml-core/#italic-mappings
 TEST(MathTransform, Italics) {
   static struct ItalicsTestData {
@@ -55,9 +54,11 @@
       {0x03F0, 0x1D718}, {0x03F1, 0x1D71A}, {0x03F4, 0x1D6F3},
       {0x03F5, 0x1D716}};
 
-  for (auto& test_data : italics_test_data)
-    EXPECT_EQ(ItalicMathVariant(test_data.code_point), test_data.expected);
+  for (auto& test_data : italics_test_data) {
+    EXPECT_EQ(unicode::ItalicMathVariant(test_data.code_point),
+              test_data.expected);
+  }
 }
 
 }  // anonymous namespace
-}  // namespace WTF
+}  // namespace blink
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index c16bb01..e8fe969 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -5275,6 +5275,7 @@
       "external/wpt/webnn/validation_tests/lstm.https.any.html?cpu",
       "external/wpt/webnn/validation_tests/lstmCell.https.any.html?cpu",
       "external/wpt/webnn/validation_tests/matmul.https.any.html?cpu",
+      "external/wpt/webnn/validation_tests/opSupportLimits.https.any.html?cpu",
       "external/wpt/webnn/validation_tests/pad.https.any.html?cpu",
       "external/wpt/webnn/validation_tests/pooling-and-reduction-keep-dims.https.any.html?cpu",
       "external/wpt/webnn/validation_tests/pooling-and-reduction-keep-dims.https.any.serviceworker.html?cpu",
@@ -5536,6 +5537,7 @@
       "external/wpt/webnn/validation_tests/lstm.https.any.html?npu",
       "external/wpt/webnn/validation_tests/lstmCell.https.any.html?npu",
       "external/wpt/webnn/validation_tests/matmul.https.any.html?npu",
+      "external/wpt/webnn/validation_tests/opSupportLimits.https.any.html?npu",
       "external/wpt/webnn/validation_tests/pad.https.any.html?npu",
       "external/wpt/webnn/validation_tests/pooling-and-reduction-keep-dims.https.any.html?npu",
       "external/wpt/webnn/validation_tests/pooling-and-reduction-keep-dims.https.any.serviceworker.html?npu",
@@ -5797,6 +5799,7 @@
       "external/wpt/webnn/validation_tests/lstm.https.any.html?gpu",
       "external/wpt/webnn/validation_tests/lstmCell.https.any.html?gpu",
       "external/wpt/webnn/validation_tests/matmul.https.any.html?gpu",
+      "external/wpt/webnn/validation_tests/opSupportLimits.https.any.html?gpu",
       "external/wpt/webnn/validation_tests/pad.https.any.html?gpu",
       "external/wpt/webnn/validation_tests/pooling-and-reduction-keep-dims.https.any.html?gpu",
       "external/wpt/webnn/validation_tests/pooling-and-reduction-keep-dims.https.any.serviceworker.html?gpu",
diff --git a/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json b/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json
index 9bea152..6947c2e 100644
--- a/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json
+++ b/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json
@@ -26607,7 +26607,7 @@
    },
    "apng": {
     "animated-png-timeout.html": [
-     "6975bd9c4e7339ceceb9f078bbf0c356f9949f3e",
+     "2a10f8df06ccce550ebd642f7602c3b55dad9a8b",
      [
       null,
       [
@@ -361973,7 +361973,7 @@
        []
       ],
       "position-sticky-fractional-offset-ref.html": [
-       "8b7a1f8a195843b2b943e39813b5e24781955b8d",
+       "37a087b7be7182c2bdefc3a39124132360c1bab0",
        []
       ],
       "position-sticky-grid-ref.html": [
@@ -365525,6 +365525,10 @@
       "29a0fe8013dcbda6c44369794dfe6e7cde21c1a8",
       []
      ],
+     "intrinsic-size-fallback-video-expected.txt": [
+      "bd9d06061c0cb27839d535ca89335f8eabc527a4",
+      []
+     ],
      "max-content-input-001-ref.html": [
       "c01e46550a7f0b5b92321d5893a8fe2a1ea0c9ac",
       []
@@ -382757,10 +382761,6 @@
       "de22e864d7d14a9e0280ec2f41678dbc90ee2e58",
       []
      ],
-     "define-expected.txt": [
-      "5fa6248617c90049e42ebbd8c7d7dc77edb056af",
-      []
-     ],
      "initial-about-blank.window-expected.txt": [
       "a715c1e2339881b9cc061246cf1c0b71cdf4d371",
       []
@@ -404704,11 +404704,11 @@
         ]
        },
        "video-aspect-ratio-expected.txt": [
-        "76fe81a9a006565441d57d28db0bd60b76914cfc",
+        "f3d1ccdb2c613268fbcc76e618d8dea71aa6b534",
         []
        ],
        "video-intrinsic-width-height-expected.txt": [
-        "c67465b3c1e392467370be8dc84d7b03fd21fd15",
+        "2ecf1dc355df97e43cb14ad17bcab3aee06ac732",
         []
        ]
       },
@@ -406775,6 +406775,10 @@
         "399cf2aa213438f79a37707a56cb751b07da869e",
         []
        ],
+       "intrinsic_sizes-expected.txt": [
+        "028e8945ef2dcbb9320c19404557e8bd087cb604",
+        []
+       ],
        "video-poster-shown-preload-auto-ref.html": [
         "66b42e40253f1a47c90ac781e9a13cc94996f52b",
         []
@@ -484192,7 +484196,7 @@
      ]
     ],
     "compute_pressure_duplicate_updates.https.window.js": [
-     "abf53854b40637d17102132d39e8a5e0aeaa438d",
+     "b2968375db06ad348bd92b403f56828b84ff5fb9",
      [
       "compute-pressure/compute_pressure_duplicate_updates.https.window.html?globalScope=dedicated_worker",
       {
@@ -542000,7 +542004,7 @@
       ]
      ],
      "define.html": [
-      "d3d923bd916fd04576d0de1d809e43a5395b9edb",
+      "52193ebd17e4ea01e2dccf0656ad28915894d65b",
       [
        null,
        {}
@@ -542062,7 +542066,7 @@
        {}
       ]
      ],
-     "valid-custom-element-names.tentative.html": [
+     "valid-custom-element-names.html": [
       "72a5999e3a9a76bd35b3f17402cf13f6ef43dc4d",
       [
        null,
@@ -645486,7 +645490,7 @@
         ]
        ],
        "video-intrinsic-width-height.html": [
-        "047c832da35246278ce58a255037c64bb1d8b723",
+        "836fadb24fda2f90b5730babe801b212d3125fea",
         [
          null,
          {}
diff --git a/third_party/blink/web_tests/external/wpt/apng/animated-png-timeout.html b/third_party/blink/web_tests/external/wpt/apng/animated-png-timeout.html
index 6975bd9..2a10f8d 100644
--- a/third_party/blink/web_tests/external/wpt/apng/animated-png-timeout.html
+++ b/third_party/blink/web_tests/external/wpt/apng/animated-png-timeout.html
@@ -1,7 +1,6 @@
 <html class="reftest-wait">
 <title>APNG: Second frame displays quickly, replacing red with green.</title>
 <link rel="match" href="animated-png-timeout-ref.html"/>
-<img src=../images/apng.png onload="loaded()"/>
 <script>
   function loaded() {
     setTimeout(function() {
@@ -9,3 +8,4 @@
     }, 1000);
   }
 </script>
+<img src=../images/apng.png onload="loaded()"/>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-001.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-001.html
new file mode 100644
index 0000000..d5356ea
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-001.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>
+  CSS Gap Decorations: Gap decorations are painted with the grid subgridded on both axes.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-gaps-1/">
+<link rel="match" href="../../../reference/ref-filled-green-100px-square.xht">
+<link rel="author" title="Sam Davis Omekara Jr." href="mailto:samomekarajr@microsoft.com">
+<style>
+    .grid-container {
+      display: grid;
+      grid-template-columns: repeat(4, 1fr);
+      grid-template-rows: repeat(3, 1fr);
+      gap: 10px;
+      width: 100px;
+      height: 100px;
+    }
+    .subgrid {
+      display: grid;
+      grid-template-columns: subgrid;
+      grid-template-rows: subgrid;
+
+      background: red;
+      grid-column: 1 / -1;
+      grid-row: 1 / -1;
+      column-rule: green solid 10px;
+      row-rule: green solid 10px;
+    }
+    .subgrid-item {
+      background: green;
+    }
+  </style>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div class="grid-container">
+  <div class="subgrid">
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+  </div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-002.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-002.html
new file mode 100644
index 0000000..82df19b2
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-002.html
@@ -0,0 +1,47 @@
+
+<!DOCTYPE html>
+<title>
+  CSS Gap Decorations: Gap decorations are painted with the grid subgridded on column axis.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-gaps-1/">
+<link rel="match" href="../../../reference/ref-filled-green-100px-square.xht">
+<link rel="author" title="Sam Davis Omekara Jr." href="mailto:samomekarajr@microsoft.com">
+<style>
+  .grid-container {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    grid-template-rows: repeat(3, 1fr);
+    gap: 10px;
+    width: 100px;
+    height: 100px;
+  }
+  .subgrid {
+    display: grid;
+    grid-template-columns: subgrid;
+    grid-template-rows: repeat(2, 1fr);
+
+    grid-column: 1 / -1;
+    grid-row: 1 / -1;
+    column-rule: green solid 10px;
+    row-gap: 20px;
+    row-rule: green solid 20px;
+
+    background: red;
+  }
+  .subgrid-item {
+    background: green;
+  }
+</style>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div class="grid-container">
+  <div class="subgrid">
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+  </div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-003.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-003.html
new file mode 100644
index 0000000..2119926
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-003.html
@@ -0,0 +1,47 @@
+
+<!DOCTYPE html>
+<title>
+  CSS Gap Decorations: Gap decorations are painted with the grid subgridded on row axis.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-gaps-1/">
+<link rel="match" href="../../../reference/ref-filled-green-100px-square.xht">
+<link rel="author" title="Sam Davis Omekara Jr." href="mailto:samomekarajr@microsoft.com">
+<style>
+  .grid-container {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    grid-template-rows: repeat(3, 1fr);
+    gap: 10px;
+    width: 100px;
+    height: 100px;
+  }
+  .subgrid {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    grid-template-rows: subgrid;
+
+    grid-column: 1 / -1;
+    grid-row: 1 / -1;
+    column-gap: 20px;
+    column-rule: green solid 20px;
+    row-rule: green solid 10px;
+
+    background: red;
+  }
+  .subgrid-item {
+    background: green;
+  }
+</style>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div class="grid-container">
+  <div class="subgrid">
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+    <div class="subgrid-item"></div>
+  </div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-004.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-004.html
new file mode 100644
index 0000000..20fb140
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-004.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>
+  CSS Gap Decorations: *rule-break defaults to spanning-item and avoids painting behind spanners in subgrid.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-gaps-1/#break">
+<link rel="match" href="../grid-gap-decorations-006-ref.html">
+<link rel="author" title="Sam Davis Omekara Jr." href="mailto:samomekarajr@microsoft.com">
+<style>
+  body {
+    margin: 0;
+  }
+  .grid-container {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    grid-template-rows: repeat(4, 1fr);
+    gap: 10px;
+    width: 430px;
+    height: 430px;
+  }
+  .subgrid {
+    display: grid;
+    grid-column: 1 / -1;
+    grid-row: 1 / -1;
+    grid-template-columns: subgrid;
+    grid-template-rows: subgrid;
+    column-rule: blue solid 5px;
+    row-rule: red solid 5px;
+  }
+  .grid-item {
+    background-color: gray;
+    opacity: 0.5;
+    border: 1px solid #000;
+  }
+</style>
+<div class="grid-container">
+  <div class="subgrid">
+    <div class="grid-item" style="grid-column: 1 / 3; grid-row: 1 / 2;"></div>
+    <div class="grid-item" style="grid-column: 3 / 4; grid-row: 1 / 3;"></div>
+    <div class="grid-item" style="grid-row: 2 / 4;"></div>
+    <div class="grid-item" style="grid-column: 2 / 4; grid-row: 3 / 4;"></div>
+    <div class="grid-item" style="grid-column: 2 / 3; grid-row: 2 / 3;"></div>
+    <div class="grid-item" style="grid-column: 4 / 5; grid-row: 1 / 4;"></div>
+    <div class="grid-item" style="grid-column: 1 / 4; grid-row: 4 / 5;"></div>
+    <div class="grid-item" style="grid-column: 4 / 5; grid-row: 4 / 5;"></div>
+  </div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-005.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-005.html
new file mode 100644
index 0000000..7c92642
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-005.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<title>
+  CSS Gap Decorations: setting *rule-break to intersection stops painting decorations at each intersection in subgrid.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-gaps-1/#break">
+<link rel="match" href="../grid-gap-decorations-007-ref.html">
+<link rel="author" title="Sam Davis Omekara Jr." href="mailto:samomekarajr@microsoft.com">
+<style>
+  body {
+    margin: 0;
+  }
+  .grid-container {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    grid-template-rows: repeat(4, 1fr);
+    gap: 10px;
+    width: 430px;
+    height: 430px;
+  }
+  .subgrid {
+    display: grid;
+    grid-column: 1 / -1;
+    grid-row: 1 / -1;
+    grid-template-columns: subgrid;
+    grid-template-rows: subgrid;
+    column-rule: blue solid 5px;
+    row-rule: red solid 5px;
+
+    column-rule-break: intersection;
+    column-rule-outset: 0px;
+
+    row-rule-break: intersection;
+    row-rule-outset: 0px;
+  }
+  .grid-item {
+    background-color: gray;
+    opacity: 0.5;
+    border: 1px solid #000;
+  }
+</style>
+<div class="grid-container">
+  <div class="subgrid">
+    <div class="grid-item" style="grid-column: 1 / 3; grid-row: 1 / 2;"></div>
+    <div class="grid-item" style="grid-column: 3 / 4; grid-row: 1 / 3;"></div>
+    <div class="grid-item" style="grid-row: 2 / 4;"></div>
+    <div class="grid-item" style="grid-column: 2 / 4; grid-row: 3 / 4;"></div>
+    <div class="grid-item" style="grid-column: 2 / 3; grid-row: 2 / 3;"></div>
+    <div class="grid-item" style="grid-column: 4 / 5; grid-row: 1 / 4;"></div>
+    <div class="grid-item" style="grid-column: 1 / 4; grid-row: 4 / 5;"></div>
+    <div class="grid-item" style="grid-column: 4 / 5; grid-row: 4 / 5;"></div>
+  </div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-006.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-006.html
new file mode 100644
index 0000000..8ac8b7a9
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-006.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>
+  CSS Gap Decorations: setting *rule-break to none paints decorations from first grid line to last grid line in subgrid.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-gaps-1/#break">
+<link rel="match" href="../grid-gap-decorations-008-ref.html">
+<link rel="author" title="Sam Davis Omekara Jr." href="mailto:samomekarajr@microsoft.com">
+<style>
+  body {
+    margin: 0;
+  }
+  .grid-container {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    grid-template-rows: repeat(4, 1fr);
+    gap: 10px;
+    width: 430px;
+    height: 430px;
+  }
+  .subgrid {
+    display: grid;
+    grid-column: 1 / -1;
+    grid-row: 1 / -1;
+    grid-template-columns: subgrid;
+    grid-template-rows: subgrid;
+    column-rule: blue solid 5px;
+    row-rule: red solid 5px;
+
+    column-rule-break: none;
+    row-rule-break: none;
+  }
+  .grid-item {
+    background-color: gray;
+    opacity: 0.5;
+    border: 1px solid #000;
+  }
+</style>
+<div class="grid-container">
+  <div class="subgrid">
+    <div class="grid-item" style="grid-column: 1 / 3; grid-row: 1 / 2;"></div>
+    <div class="grid-item" style="grid-column: 3 / 4; grid-row: 1 / 3;"></div>
+    <div class="grid-item" style="grid-row: 2 / 4;"></div>
+    <div class="grid-item" style="grid-column: 2 / 4; grid-row: 3 / 4;"></div>
+    <div class="grid-item" style="grid-column: 2 / 3; grid-row: 2 / 3;"></div>
+    <div class="grid-item" style="grid-column: 4 / 5; grid-row: 1 / 4;"></div>
+    <div class="grid-item" style="grid-column: 1 / 4; grid-row: 4 / 5;"></div>
+    <div class="grid-item" style="grid-column: 4 / 5; grid-row: 4 / 5;"></div>
+  </div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-007.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-007.html
new file mode 100644
index 0000000..64a87d62
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-007.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>
+  CSS Gap Decorations: An outset of 0px aligns the ends of gap decorations with adjacent item in subgrid.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-gaps-1/">
+<link rel="match" href="../grid-gap-decorations-010-ref.html">
+<link rel="author" title="Sam Davis Omekara Jr." href="mailto:samomekarajr@microsoft.com">
+<style>
+  body {
+    margin: 0;
+  }
+  .grid-container {
+    display: grid;
+    grid-template-columns: repeat(3, 100px);
+    grid-template-rows: repeat(3, 100px);
+    gap: 10px;
+  }
+  .subgrid {
+    display: grid;
+    grid-column: 1 / -1;
+    grid-row: 1 / -1;
+    grid-template-columns: subgrid;
+    grid-template-rows: subgrid;
+
+    column-rule: blue solid 5px;
+    row-rule: red solid 5px;
+
+    column-rule-outset: 0px;
+    column-rule-break: intersection;
+  }
+  .item {
+    background: gray;
+    opacity: 0.5;
+  }
+</style>
+<div class="grid-container">
+  <div class="subgrid">
+    <div class="item"></div>
+    <div class="item"></div>
+    <div class="item"></div>
+    <div class="item"></div>
+    <div class="item"></div>
+    <div class="item"></div>
+    <div class="item"></div>
+    <div class="item"></div>
+    <div class="item"></div>
+  </div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-008.html b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-008.html
new file mode 100644
index 0000000..b43dea71
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-gaps/grid/subgrid/subgrid-gap-decorations-008.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>
+  CSS Gap Decorations: An outset of 5px extends decorations slightly beyond the edges of adjacent items in subgrid.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-gaps-1/">
+<link rel="match" href="../grid-gap-decorations-012-ref.html">
+<link rel="author" title="Sam Davis Omekara Jr." href="mailto:samomekarajr@microsoft.com">
+<style>
+  .grid-container {
+    display: grid;
+    grid-template-columns: repeat(3, 100px);
+    grid-template-rows: repeat(3, 100px);
+    gap: 10px;
+  }
+  .subgrid {
+    display: grid;
+    grid-column: 1 / -1;
+    grid-row: 1 / -1;
+    grid-template-columns: subgrid;
+    grid-template-rows: subgrid;
+
+    column-rule: blue solid 5px;
+    row-rule: red solid 5px;
+
+    column-rule-outset: 5px;
+    column-rule-break: intersection;
+  }
+  .item {
+    background: gray;
+    opacity: 0.5;
+  }
+</style>
+<body>
+  <div class="grid-container">
+    <div class="subgrid">
+      <div class="item"></div>
+      <div class="item"></div>
+      <div class="item"></div>
+      <div class="item"></div>
+      <div class="item"></div>
+      <div class="item"></div>
+      <div class="item"></div>
+      <div class="item"></div>
+      <div class="item"></div>
+    </div>
+  </div>
+</body>
diff --git a/third_party/blink/web_tests/external/wpt/largest-contentful-paint/background-image-set-image.html b/third_party/blink/web_tests/external/wpt/largest-contentful-paint/background-image-set-image.html
index 4dbb97c..27a70c7 100644
--- a/third_party/blink/web_tests/external/wpt/largest-contentful-paint/background-image-set-image.html
+++ b/third_party/blink/web_tests/external/wpt/largest-contentful-paint/background-image-set-image.html
@@ -1,41 +1,42 @@
-<!DOCTYPE html>
-<html>
-
-<head>
-  <title>Background image-set images should be LCP candidates</title>
-  <script src="/resources/testharness.js"></script>
-  <script src="/resources/testharnessreport.js"></script>
-  <style>
-    .background {
-      width: calc(100vw - 40px);
-      height: calc(100vw - 40px);
-      max-width: 100px;
-      max-height: 100px;
-      background: #eee image-set('/images/lcp-100x50.png' type('image/png')) center center no-repeat;
-      background-size: cover;
-    }
-  </style>
-</head>
-
+<!doctype html>
+<title>Background image-set images should be LCP candidates</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+  .background {
+    width: calc(100vw - 40px);
+    height: calc(100vw - 40px);
+    max-width: 100px;
+    max-height: 100px;
+    background: #eee image-set("/images/lcp-100x50.png" type("image/png")) center center no-repeat;
+    background-size: cover;
+  }
+</style>
 <body>
   <div class="background"></div>
   <p>fallback</p>
-
-  <script>
-    promise_test(async (t) => {
-      t.step_timeout(async () => {
-        let lcpEntryNames =
-          await new Promise(resolve => {
-            new PerformanceObserver((list) => {
-              resolve(list.getEntries().map(e => e.url));
-            }).observe({ type: "largest-contentful-paint", buffered: true });
-          });
-
-        assert_true(lcpEntryNames.find(e => e.includes('lcp-100x50')),
-          'Background image-set image should be an LCP candidate.');
-      }, 100);
-    }, "Background image-set images should be eligible for LCP candidates");
-  </script>
 </body>
 
-</html>
\ No newline at end of file
+<script>
+  promise_test(async (t) => {
+    await Promise.race([
+      new Promise((resolve) => {
+        const entries = [];
+        new PerformanceObserver((list) => {
+          entries.push(...list.getEntries());
+          for (const entry of entries) {
+            if (entry.url.includes("lcp-100x50")) {
+              resolve(entry.url);
+              return;
+            }
+          }
+        }).observe({ type: "largest-contentful-paint", buffered: true });
+      }),
+      new Promise((resolve, reject) => {
+        t.step_timeout(() => {
+          reject(new Error("Timed out waiting for LCP entry for background image-set image."));
+        }, 3000);
+      }),
+    ]);
+  }, "Background image-set images should be eligible for LCP candidates");
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/soft-navigation-heuristics/lcp/tentative/broken-image-icon.html b/third_party/blink/web_tests/external/wpt/soft-navigation-heuristics/lcp/tentative/broken-image-icon.html
new file mode 100644
index 0000000..fbfb228
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/soft-navigation-heuristics/lcp/tentative/broken-image-icon.html
@@ -0,0 +1,78 @@
+<!doctype html>
+<!--
+The soft navigation version of the identically named test in
+/largest-contentful-paint/broken-image-icon.html.
+Notes:
+ - Awaits trivial soft navigation with same page contents as original test.
+ - Viewport is very small so that the small icon below (16x8) is
+   sufficiently large to trigger a soft navigation.
+ - Original test was awaiting FCP, but we don't support that yet
+   for soft navs; so now we await LCP for the hard navigation, and then
+   LCP and soft nav for the soft navigation, with the promise set up prior
+   to the click.
+-->
+<meta name="viewport" content="width=50, height=50, initial-scale=1" />
+<title>Broken Image Icon Should Not Be LCP after soft navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/soft-navigation-heuristics/resources/soft-navigation-test-helper.js"></script>
+<script>
+  function clickHandler() {
+    document.body.innerHTML = `
+          <img src="../non-existent-image.jpg">
+          <img src="/css/css-images/support/colors-16x8.png">
+        `;
+    history.pushState({}, "", "/test");
+  }
+</script>
+<body>
+  <div id="click-target" onclick="clickHandler()">Click!</div>
+</body>
+<script>
+  promise_test(async (t) => {
+    assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+    const helper = new SoftNavigationTestHelper(t);
+    const lcpEntries = await helper.getBufferedPerformanceEntriesWithTimeout(
+      /*type=*/ "largest-contentful-paint",
+      /*includeSoftNavigationObservations=*/ false,
+      /*minNumEntries=*/ 1,
+    );
+    assert_equals(lcpEntries.length, 1);
+    assert_equals(lcpEntries[0].id, "click-target", "The first entry should be the button");
+
+    const promise = Promise.all([
+      SoftNavigationTestHelper.getPerformanceEntries(
+        /*type=*/ "largest-contentful-paint",
+        /*includeSoftNavigationObservations=*/ true,
+        /*minNumEntries=*/ 1,
+      ),
+      SoftNavigationTestHelper.getPerformanceEntries(
+        /*type=*/ "soft-navigation",
+        /*includeSoftNavigationObservations=*/ true,
+        /*minNumEntries=*/ 1,
+      ),
+    ]);
+
+    if (test_driver) {
+      test_driver.click(document.getElementById("click-target"));
+    }
+
+    const [softLcpEntries, softNavigationEntries] = await promise;
+    assert_equals(softNavigationEntries.length, 1, "One soft navigation entry.");
+    assert_true(
+      softNavigationEntries[0].name.endsWith("test"),
+      "Soft navigation should be to test page.",
+    );
+
+    // There should be only 1 LCP entry and it should be the colors-16x8.png though
+    // being smaller than the broken image icon. The broken image icon should not
+    // emit an LCP entry.
+    assert_equals(softLcpEntries.length, 1, "There should be one and only one LCP entry.");
+    assert_true(
+      softLcpEntries[0].url.includes("colors-16x8.png"),
+      "The LCP entry should be the colors-16x8.png",
+    );
+  }, "The broken image icon should not emit an LCP entry after soft navigation.");
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/soft-navigation-heuristics/lcp/tentative/contracted-image.html b/third_party/blink/web_tests/external/wpt/soft-navigation-heuristics/lcp/tentative/contracted-image.html
new file mode 100644
index 0000000..46d359a
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/soft-navigation-heuristics/lcp/tentative/contracted-image.html
@@ -0,0 +1,63 @@
+<!doctype html>
+<!--
+      The soft navigation version of the identically named
+      test in /largest-contentful-paint/contracted-image.html.
+      Notes:
+      - Awaits trivial soft navigation with same page contents as original test.
+      - Uses promise_test and slightly revised HTML tags, to make it easy to
+        observe the initial LCP before the soft navigation (the click target)
+        and distinguish it from the interesting LCP after the soft navigation.
+-->
+<meta charset="utf-8" />
+<title>
+  Largest Contentful Paint: contracted image bounded by display size after soft navigation.
+</title>
+<style type="text/css">
+  #image_id {
+    width: 50px;
+    height: 50px;
+  }
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/largest-contentful-paint/resources/largest-contentful-paint-helpers.js"></script>
+<script src="/soft-navigation-heuristics/resources/soft-navigation-test-helper.js"></script>
+<script>
+  function clickHandler() {
+    document.body.innerHTML = `<img src="/images/black-rectangle.png" id="image_id" />`;
+    history.pushState({}, "", "/test");
+  }
+</script>
+<body>
+  <div id="click-target" onclick="clickHandler()">Click!</div>
+</body>
+<script>
+  promise_test(async (t) => {
+    assert_implements(window.LargestContentfulPaint, "LargestContentfulPaint is not implemented");
+    if (test_driver) {
+      test_driver.click(document.getElementById("click-target"));
+    }
+    const beforeLoad = performance.now();
+    const helper = new SoftNavigationTestHelper(t);
+    await helper.getBufferedPerformanceEntriesWithTimeout(
+      /*type=*/ "soft-navigation",
+      /*includeSoftNavigationObservations=*/ false,
+      /*minNumEntries=*/ 1,
+    );
+    const entries = await helper.getBufferedPerformanceEntriesWithTimeout(
+      /*type=*/ "largest-contentful-paint",
+      /*includeSoftNavigationObservations=*/ true,
+      /*minNumEntries=*/ 2,
+    );
+    assert_equals(entries.length, 2);
+    assert_equals(entries[0].id, "click-target", "The first entry should be the button");
+
+    const entry = entries[1];
+    const url = window.location.origin + "/images/black-rectangle.png";
+    // black-rectangle.png is 100 x 50. It occupies 50 x 50 so size will be bounded by the displayed size.
+    const size = 50 * 50;
+    checkImage(entry, url, "image_id", size, beforeLoad);
+  }, "Largest Contentful Paint: |size| attribute is bounded by display size after soft navigation.");
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/soft-navigation-heuristics/softnav-before-lcp-paint.tentative-expected.txt b/third_party/blink/web_tests/external/wpt/soft-navigation-heuristics/softnav-before-lcp-paint.tentative-expected.txt
new file mode 100644
index 0000000..76a2182
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/soft-navigation-heuristics/softnav-before-lcp-paint.tentative-expected.txt
@@ -0,0 +1,5 @@
+This is a testharness.js-based test.
+[FAIL] Detect simple soft navigation.
+  assert_greater_than_equal: Got at least 3 LCP entries expected a number greater than or equal to 3 but got 2
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/external/wpt/webnn/validation_tests/opSupportLimits.https.any.js b/third_party/blink/web_tests/external/wpt/webnn/validation_tests/opSupportLimits.https.any.js
new file mode 100644
index 0000000..cd28df9
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/webnn/validation_tests/opSupportLimits.https.any.js
@@ -0,0 +1,54 @@
+// META: title=validation context.opSupportLimits() interface
+// META: global=window
+// META: variant=?cpu
+// META: variant=?gpu
+// META: variant=?npu
+// META: script=../resources/utils_validation.js
+
+'use strict';
+
+
+const tests = [
+  {
+    operator: 'logicalAnd',
+    limits: {
+      a: {dataTypes: ['uint8']},
+      b: {dataTypes: ['uint8']},
+      output: {dataTypes: ['uint8']},
+    }
+  },
+  {
+    operator: 'logicalOr',
+    limits: {
+      a: {dataTypes: ['uint8']},
+      b: {dataTypes: ['uint8']},
+      output: {dataTypes: ['uint8']},
+    }
+  },
+  {
+    operator: 'logicalXor',
+    limits: {
+      a: {dataTypes: ['uint8']},
+      b: {dataTypes: ['uint8']},
+      output: {dataTypes: ['uint8']},
+    }
+  },
+  {
+    operator: 'logicalNot',
+    limits: {
+      a: {dataTypes: ['uint8']},
+      output: {dataTypes: ['uint8']},
+    }
+  }
+];
+
+tests.forEach(test => promise_test(async t => {
+                const limits = context.opSupportLimits()[test.operator];
+                for (let [name, expected] of Object.entries(test.limits)) {
+                  for (let actualDataType of limits[name].dataTypes) {
+                    assert_in_array(
+                        actualDataType, expected.dataTypes,
+                        `${test.operator}.${name}.dataTypes`);
+                  }
+                }
+              }, `check opSupportLimits data types of ${test.operator}`));
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/tracing/soft-navigations-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/tracing/soft-navigations-expected.txt
index 8a128648..42d43ea2 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/tracing/soft-navigations-expected.txt
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/tracing/soft-navigations-expected.txt
@@ -37,7 +37,6 @@
 			paintedArea: number
 			repaintedArea: number
 			softNavContextId: number
-			unattributedPaintedArea: number
 			wasEmitted: boolean
 		}
 		frame: string
diff --git a/third_party/blink/web_tests/images/zoomed-img-size-expected.txt b/third_party/blink/web_tests/images/zoomed-img-size-expected.txt
deleted file mode 100644
index fba986e..0000000
--- a/third_party/blink/web_tests/images/zoomed-img-size-expected.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-This is a testharness.js-based test.
-[FAIL] Zoom 1% should not affect width/height of images.
-  assert_equals: zoom: 1% expected 37 but got 100
-[FAIL] Zoom 2% should not affect width/height of images.
-  assert_equals: zoom: 2% expected 37 but got 50
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/images/zoomed-img-size.html b/third_party/blink/web_tests/images/zoomed-img-size.html
index 4ccb480f..01b71399 100644
--- a/third_party/blink/web_tests/images/zoomed-img-size.html
+++ b/third_party/blink/web_tests/images/zoomed-img-size.html
@@ -5,25 +5,22 @@
     <script src="../resources/testharnessreport.js"></script>
   </head>
   <body>
-    <img id="oval0" src="resources/oval.png" style="zoom: 1%">
-    <img id="oval1" src="resources/oval.png" style="zoom: 2%">
-    <img id="oval2" src="resources/oval.png" style="zoom: 3%">
-    <img id="oval3" src="resources/oval.png" style="zoom: 4%">
-    <img id="oval4" src="resources/oval.png" style="zoom: 5%">
-    <img id="oval5" src="resources/oval.png" style="zoom: 30%">
-    <img id="oval6" src="resources/oval.png" style="zoom: 33%">
-    <img id="oval7" src="resources/oval.png" style="zoom: 50%">
-    <img id="oval8" src="resources/oval.png" style="zoom: 70%">
-    <img id="oval9" src="resources/oval.png" style="zoom: 100%">
-    <img id="oval10" src="resources/oval.png" style="zoom: 111%">
-    <img id="oval11" src="resources/oval.png" style="zoom: 150%">
-    <img id="oval12" src="resources/oval.png" style="zoom: 333%">
-    <img id="oval13" src="resources/oval.png" style="zoom: 400%">
-    <img id="oval14" src="resources/oval.png" style="zoom: 1234%">
+    <img id="oval0" src="resources/oval.png" style="zoom: 4%">
+    <img id="oval1" src="resources/oval.png" style="zoom: 5%">
+    <img id="oval2" src="resources/oval.png" style="zoom: 30%">
+    <img id="oval3" src="resources/oval.png" style="zoom: 33%">
+    <img id="oval4" src="resources/oval.png" style="zoom: 50%">
+    <img id="oval5" src="resources/oval.png" style="zoom: 70%">
+    <img id="oval6" src="resources/oval.png" style="zoom: 100%">
+    <img id="oval7" src="resources/oval.png" style="zoom: 111%">
+    <img id="oval8" src="resources/oval.png" style="zoom: 150%">
+    <img id="oval9" src="resources/oval.png" style="zoom: 333%">
+    <img id="oval10" src="resources/oval.png" style="zoom: 400%">
+    <img id="oval11" src="resources/oval.png" style="zoom: 1234%">
 <script>
 const EXPECTED_WIDTH = 37;
 const EXPECTED_HEIGHT = 33;
-for (i = 0; i < 15; ++i) {
+for (i = 0; i < 12; ++i) {
   const oval = document.getElementById('oval' + i);
   const testDesc = `Zoom ${oval.style.zoom} should not affect width/height of images.`;
   const testBody = () => {
diff --git a/third_party/blink/web_tests/platform/linux/svg/zoom/page/zoom-svg-through-object-with-percentage-size-expected.png b/third_party/blink/web_tests/platform/linux/svg/zoom/page/zoom-svg-through-object-with-percentage-size-expected.png
index d3cea2d..1ee7b99 100644
--- a/third_party/blink/web_tests/platform/linux/svg/zoom/page/zoom-svg-through-object-with-percentage-size-expected.png
+++ b/third_party/blink/web_tests/platform/linux/svg/zoom/page/zoom-svg-through-object-with-percentage-size-expected.png
Binary files differ
diff --git a/third_party/dawn b/third_party/dawn
index 9635124..223318d 160000
--- a/third_party/dawn
+++ b/third_party/dawn
@@ -1 +1 @@
-Subproject commit 9635124871e8ab6d38d416e78759a447e68c35e5
+Subproject commit 223318d090e36d901f7c7a83b764e0a336b19385
diff --git a/third_party/devtools-frontend/src b/third_party/devtools-frontend/src
index d7cb23e..fd0c28c 160000
--- a/third_party/devtools-frontend/src
+++ b/third_party/devtools-frontend/src
@@ -1 +1 @@
-Subproject commit d7cb23eac0fda3c06441034ccea0c4bb9c947add
+Subproject commit fd0c28cbf06b695f677c525b6630f9f9e4a90eee
diff --git a/third_party/llvm-libc/src b/third_party/llvm-libc/src
index 1f64d05..ff75100 160000
--- a/third_party/llvm-libc/src
+++ b/third_party/llvm-libc/src
@@ -1 +1 @@
-Subproject commit 1f64d055dbb79dfcef1a63588341d171f327cd73
+Subproject commit ff75100b45027c70e17770cb80e720193b742c76
diff --git a/third_party/perfetto b/third_party/perfetto
index 3231268..d7984eb 160000
--- a/third_party/perfetto
+++ b/third_party/perfetto
@@ -1 +1 @@
-Subproject commit 3231268f2960b13cb52788389e88d7932117f57e
+Subproject commit d7984ebb216b56e0eb99ba29adfae91f37819602
diff --git a/third_party/skia b/third_party/skia
index dc1958e..f8cd9fe 160000
--- a/third_party/skia
+++ b/third_party/skia
@@ -1 +1 @@
-Subproject commit dc1958e7118c351cef5adae548337f7f11746b95
+Subproject commit f8cd9fe75f21d3be759cbf9491ddc582efcf1e2a
diff --git a/third_party/vulkan-deps b/third_party/vulkan-deps
index 222889e..5f9183d 160000
--- a/third_party/vulkan-deps
+++ b/third_party/vulkan-deps
@@ -1 +1 @@
-Subproject commit 222889ea039509b86fa6dd90fcb6acf47e79cdb9
+Subproject commit 5f9183df4e99d153dcf2de173ddfd47034f72601
diff --git a/third_party/vulkan-headers/src b/third_party/vulkan-headers/src
index 1d6c53f..b39ab38 160000
--- a/third_party/vulkan-headers/src
+++ b/third_party/vulkan-headers/src
@@ -1 +1 @@
-Subproject commit 1d6c53f65443ceeb97d3bdc695aaecc7ea6cc441
+Subproject commit b39ab380a44b6c8df462c34e976ea9ce2d2c336b
diff --git a/third_party/vulkan-loader/src b/third_party/vulkan-loader/src
index 63b05d2..22c0f13 160000
--- a/third_party/vulkan-loader/src
+++ b/third_party/vulkan-loader/src
@@ -1 +1 @@
-Subproject commit 63b05d2b087952662623de7eddd44f2e57d71a1e
+Subproject commit 22c0f133e6675f9313c12fb5e58337f8fa9b9195
diff --git a/third_party/vulkan-tools/src b/third_party/vulkan-tools/src
index fbe7226..d6719230 160000
--- a/third_party/vulkan-tools/src
+++ b/third_party/vulkan-tools/src
@@ -1 +1 @@
-Subproject commit fbe722654b7173da961398cf78bd4a62d1839b65
+Subproject commit d671923090e4dc74c0ebdb10c6e09fa0826e1fe9
diff --git a/third_party/vulkan-utility-libraries/src b/third_party/vulkan-utility-libraries/src
index e48ae20..54c9baf 160000
--- a/third_party/vulkan-utility-libraries/src
+++ b/third_party/vulkan-utility-libraries/src
@@ -1 +1 @@
-Subproject commit e48ae20a7938b01aee62806bfcdafe8a0883b1e4
+Subproject commit 54c9baf20802a13279e23fa4cb0528dd5cf16064
diff --git a/third_party/vulkan-validation-layers/src b/third_party/vulkan-validation-layers/src
index 6d6e6ec..4b8f3ca 160000
--- a/third_party/vulkan-validation-layers/src
+++ b/third_party/vulkan-validation-layers/src
@@ -1 +1 @@
-Subproject commit 6d6e6ec8e51cd219498359cbc48e4762d1a80616
+Subproject commit 4b8f3caab2cf0ace618ba857364346aed6297d29
diff --git a/third_party/webrtc b/third_party/webrtc
index cf71427..70347d9 160000
--- a/third_party/webrtc
+++ b/third_party/webrtc
@@ -1 +1 @@
-Subproject commit cf714279e2a0d54e03b4065f14748dea1dc24606
+Subproject commit 70347d915e9b3d74cfbcc1323f35edc834f097e0
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index bc7512c6..4c2c206 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -13822,6 +13822,7 @@
   <int value="-42175674" label="ShareUsageRankingFixedMore:disabled"/>
   <int value="-41548966" label="AutocorrectUseReplaceSurroundingText:disabled"/>
   <int value="-41460305" label="InfobarScrollOptimization:disabled"/>
+  <int value="-41263570" label="OmniboxToolbelt:enabled"/>
   <int value="-41254374" label="WebRtcMetronomeTaskQueue:enabled"/>
   <int value="-40935502" label="ContextualSuggestionsSlimPeekUI:enabled"/>
   <int value="-38525285" label="AccessibilityManifestV3SelectToSpeak:enabled"/>
@@ -14535,6 +14536,7 @@
   <int value="228024890" label="AutofillEnableSaveAndFill:enabled"/>
   <int value="228107154"
       label="OmniboxUpdatedConnectionSecurityIndicators:enabled"/>
+  <int value="228814932" label="OmniboxToolbelt:disabled"/>
   <int value="228845546" label="ShimlessRMAOsUpdate:enabled"/>
   <int value="229312162" label="PriceInsights:disabled"/>
   <int value="229476202" label="CryptAuthV2Enrollment:enabled"/>
diff --git a/tools/metrics/histograms/metadata/autofill/enums.xml b/tools/metrics/histograms/metadata/autofill/enums.xml
index 61806bf..64cec22a 100644
--- a/tools/metrics/histograms/metadata/autofill/enums.xml
+++ b/tools/metrics/histograms/metadata/autofill/enums.xml
@@ -3644,6 +3644,8 @@
   <int value="5" label="Missing href"/>
   <int value="6" label="Missing form data"/>
   <int value="7" label="Form extraction failure"/>
+  <int value="8" label="Renderer error"/>
+  <int value="9" label="Unparsed renderer error"/>
 </enum>
 
 <!-- LINT.ThenChange(//components/autofill/ios/form_util/form_activity_tab_helper.mm:FormSubmissionOutcomeIOS) -->
diff --git a/tools/metrics/histograms/metadata/autofill/histograms.xml b/tools/metrics/histograms/metadata/autofill/histograms.xml
index 7284442..549f500e 100644
--- a/tools/metrics/histograms/metadata/autofill/histograms.xml
+++ b/tools/metrics/histograms/metadata/autofill/histograms.xml
@@ -3770,7 +3770,19 @@
   </summary>
 </histogram>
 
-<histogram name="Autofill.iOS.FormSubmission.Outcome"
+<histogram name="Autofill.iOS.FormSubmission.Outcome.InvalidFormReason"
+    enum="InvalidSubmittedFormReasonIOS" expires_after="2025-11-16">
+  <owner>eic@google.com</owner>
+  <owner>vincb@google.com</owner>
+  <owner>bling-transactions@google.com</owner>
+  <summary>
+    Records the reason why a submitted form data was found invalid by the
+    browser process. Recorded when the browser process receives a form
+    submission message from the renderer. iOS only.
+  </summary>
+</histogram>
+
+<histogram name="Autofill.iOS.FormSubmission.OutcomeV2"
     enum="FormSubmissionOutcomeIOS" expires_after="2025-11-16">
   <owner>eic@google.com</owner>
   <owner>vincb@google.com</owner>
@@ -3784,18 +3796,6 @@
   </summary>
 </histogram>
 
-<histogram name="Autofill.iOS.FormSubmission.Outcome.InvalidFormReason"
-    enum="InvalidSubmittedFormReasonIOS" expires_after="2025-11-16">
-  <owner>eic@google.com</owner>
-  <owner>vincb@google.com</owner>
-  <owner>bling-transactions@google.com</owner>
-  <summary>
-    Records the reason why a submitted form data was found invalid by the
-    browser process. Recorded when the browser process receives a form
-    submission message from the renderer. iOS only.
-  </summary>
-</histogram>
-
 <histogram name="Autofill.iOS.TriggeredFormExtractionFromDriver.{Range}"
     units="count" expires_after="2025-10-12">
   <owner>vincb@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/lens/enums.xml b/tools/metrics/histograms/metadata/lens/enums.xml
index 101f00b..543ddf58 100644
--- a/tools/metrics/histograms/metadata/lens/enums.xml
+++ b/tools/metrics/histograms/metadata/lens/enums.xml
@@ -143,6 +143,19 @@
 
 <!-- LINT.ThenChange(//components/lens/lens_overlay_side_panel_menu_option.h:LensOverlaySidePanelMenuOption) -->
 
+<!-- LINT.IfChange(LensOverlayTextDirectiveResult) -->
+
+<enum name="LensOverlayTextDirectiveResult">
+  <summary>
+    The result of handling a text directive in the Lens Overlay.
+  </summary>
+  <int value="0" label="Found on page"/>
+  <int value="1" label="Opened in new tab"/>
+  <int value="2" label="Not found on page"/>
+</enum>
+
+<!-- LINT.ThenChange(//components/lens/lens_overlay_metrics.h:LensOverlayTextDirectiveResult) -->
+
 <!-- LINT.IfChange(LensOverlayUserAction) -->
 
 <enum name="LensOverlayUserAction">
diff --git a/tools/metrics/histograms/metadata/lens/histograms.xml b/tools/metrics/histograms/metadata/lens/histograms.xml
index b0fc0b6..b59e5ebf 100644
--- a/tools/metrics/histograms/metadata/lens/histograms.xml
+++ b/tools/metrics/histograms/metadata/lens/histograms.xml
@@ -732,6 +732,17 @@
   </summary>
 </histogram>
 
+<histogram name="Lens.Overlay.TextDirectiveResult"
+    enum="LensOverlayTextDirectiveResult" expires_after="2026-01-31">
+  <owner>juanmojica@google.com</owner>
+  <owner>lens-chrome@google.com</owner>
+  <summary>
+    Recorded once whenever the text finder is invoked on the Lens overlay.
+    Indicates whether the text finder was successful in finding text in the
+    page.
+  </summary>
+</histogram>
+
 <histogram name="Lens.Overlay.TimeToFirstInteraction" units="ms"
     expires_after="2025-11-09">
   <owner>jdonnelly@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/network/histograms.xml b/tools/metrics/histograms/metadata/network/histograms.xml
index f9c9cb0..2562d88 100644
--- a/tools/metrics/histograms/metadata/network/histograms.xml
+++ b/tools/metrics/histograms/metadata/network/histograms.xml
@@ -5785,6 +5785,39 @@
   <token key="Result" variants="HTTPRequestResult"/>
 </histogram>
 
+<histogram name="NetworkService.Scheduler.IOThread.TaskOffCpuDuration"
+    units="ms" expires_after="2025-12-25">
+  <owner>hayato@chromium.org</owner>
+  <owner>net-dev@chromium.org</owner>
+  <summary>
+    Records the duration spent off-CPU by tasks running on the Network Service
+    IO thread. Recorded with 0.001 probability when a task scheduled on the IO
+    thread finishes running.
+  </summary>
+</histogram>
+
+<histogram name="NetworkService.Scheduler.IOThread.TaskOnCpuDuration"
+    units="ms" expires_after="2025-12-25">
+  <owner>hayato@chromium.org</owner>
+  <owner>net-dev@chromium.org</owner>
+  <summary>
+    Records the duration spent on-CPU by tasks running on the Network Service IO
+    Thread. Recorded with 0.001 probability when a task scheduled on the IO
+    thread finishes running.
+  </summary>
+</histogram>
+
+<histogram name="NetworkService.Scheduler.IOThread.TaskOnCpuPercentage"
+    units="%" expires_after="2025-12-25">
+  <owner>hayato@chromium.org</owner>
+  <owner>net-dev@chromium.org</owner>
+  <summary>
+    Records the percentage of wall time spent on-CPU by tasks running on the
+    Network Service IO Thread. Recorded with 0.001 probability when a task
+    scheduled on the IO thread finishes running.
+  </summary>
+</histogram>
+
 <histogram name="NetworkService.SlopBucket.DisabledReason"
     enum="SlopBucketDisabledReason" expires_after="2024-10-23">
   <owner>ricea@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/omnibox/histograms.xml b/tools/metrics/histograms/metadata/omnibox/histograms.xml
index 88ca3f4..2c0e5ce 100644
--- a/tools/metrics/histograms/metadata/omnibox/histograms.xml
+++ b/tools/metrics/histograms/metadata/omnibox/histograms.xml
@@ -1287,7 +1287,7 @@
 
 <histogram
     name="Omnibox.FocusToOpenTimeAnyPopupState3.{OmniboxZeroPrefixState}"
-    units="ms" expires_after="2025-06-23">
+    units="ms" expires_after="2025-12-12">
   <owner>khalidpeer@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
   <summary>
@@ -1326,7 +1326,7 @@
 
 <histogram
     name="Omnibox.FocusToOpenTimeAnyPopupState3.{OmniboxZeroPrefixState}.BySummarizedResultType.{OmniboxSummarizedResultType}"
-    units="ms" expires_after="2025-06-23">
+    units="ms" expires_after="2025-12-12">
   <owner>khalidpeer@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
   <summary>
@@ -1347,7 +1347,7 @@
 
 <histogram
     name="Omnibox.FocusToOpenTimeAnyPopupState3.{OmniboxZeroPrefixState}.BySummarizedResultType.{OmniboxSummarizedResultType}.ByPageContext.{OmniboxPageContext}"
-    units="ms" expires_after="2025-06-23">
+    units="ms" expires_after="2025-12-12">
   <owner>khalidpeer@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
   <summary>
@@ -1666,7 +1666,7 @@
 </histogram>
 
 <histogram name="Omnibox.LocalHistoryPrefixSuggest.SearchTermsExtractionTimeV2"
-    units="ms" expires_after="2025-07-13">
+    units="ms" expires_after="2025-12-12">
   <owner>mahmadi@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
   <summary>
@@ -2253,7 +2253,7 @@
 </histogram>
 
 <histogram name="Omnibox.Search.OffTheRecord" enum="BooleanOffTheRecord"
-    expires_after="2025-09-01">
+    expires_after="2025-12-12">
   <owner>jdonnelly@chromium.org</owner>
   <owner>roagarwal@chromium.org</owner>
   <owner>chrome-incognito@google.com</owner>
@@ -2265,7 +2265,7 @@
 </histogram>
 
 <histogram name="Omnibox.SearchboxStats.Length" units="chars"
-    expires_after="2025-08-24">
+    expires_after="2025-12-12">
   <owner>mahmadi@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
   <summary>
@@ -2501,7 +2501,7 @@
 
 <histogram
     name="Omnibox.SuggestionShown.ClientSummarizedResultType.ByPageContext.{OmniboxPageContext}"
-    enum="ClientSummarizedResultType" expires_after="2025-06-24">
+    enum="ClientSummarizedResultType" expires_after="2025-12-12">
   <owner>khalidpeer@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
   <summary>
@@ -2551,7 +2551,7 @@
 
 <histogram
     name="Omnibox.SuggestionShown.{OmniboxZeroPrefixState}.ClientSummarizedResultType.ByPageContext.{OmniboxPageContext}"
-    enum="ClientSummarizedResultType" expires_after="2025-06-24">
+    enum="ClientSummarizedResultType" expires_after="2025-12-12">
   <owner>khalidpeer@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
   <summary>
@@ -2866,7 +2866,7 @@
 
 <histogram
     name="Omnibox.SuggestionUsed.URL.Experimental.NavigationToFirstMeaningfulPaint"
-    units="ms" expires_after="2025-08-10">
+    units="ms" expires_after="2025-12-12">
   <owner>jdonnelly@chromium.org</owner>
   <owner>mpearson@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
@@ -3085,7 +3085,7 @@
 </histogram>
 
 <histogram name="Omnibox.SuggestTiles.DeletedTileIndex" units="position"
-    expires_after="2025-09-01">
+    expires_after="2025-12-12">
   <owner>ender@google.com</owner>
   <owner>mahmadi@chromium.org</owner>
   <summary>
@@ -3359,7 +3359,7 @@
 </histogram>
 
 <histogram name="Omnibox.URLScoringModelExecuted.MLScoreCoverage"
-    units="% of matches" expires_after="2025-06-29">
+    units="% of matches" expires_after="2025-12-12">
   <owner>khalidpeer@chromium.org</owner>
   <owner>manukh@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
@@ -3499,7 +3499,7 @@
 </histogram>
 
 <histogram name="Omnibox.Views.PopupFirstPaint" units="ms"
-    expires_after="2025-07-13">
+    expires_after="2025-12-12">
   <owner>tluk@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
   <summary>
@@ -3720,7 +3720,7 @@
 </histogram>
 
 <histogram name="Omnibox.ZeroSuggestProvider.CacheMemoryUsage" units="Bytes"
-    expires_after="2025-05-11">
+    expires_after="2025-12-12">
   <owner>mahmadi@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
   <summary>
@@ -4089,7 +4089,7 @@
 </histogram>
 
 <histogram name="ShortcutsProvider.OldEntryDeletions.OnInit" units="units"
-    expires_after="2025-06-22">
+    expires_after="2025-12-12">
   <owner>jdonnelly@chromium.org</owner>
   <owner>chrome-desktop-search@google.com</owner>
   <summary>
diff --git a/tools/perf/core/perfetto_binary_roller/binary_deps.json b/tools/perf/core/perfetto_binary_roller/binary_deps.json
index 6507934..091fd4b7 100644
--- a/tools/perf/core/perfetto_binary_roller/binary_deps.json
+++ b/tools/perf/core/perfetto_binary_roller/binary_deps.json
@@ -5,8 +5,8 @@
             "full_remote_path": "perfetto-luci-artifacts/v50.1/linux-arm64/trace_processor_shell"
         },
         "win": {
-            "hash": "99424f9df1446b999716c6628e4e3de043dfe39d",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/9da8e356eae50f441bdffb0523c5b2c9a7eba8bc/trace_processor_shell.exe"
+            "hash": "ca11d3bec4379010be7a4e500bc053f203e75797",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/d7984ebb216b56e0eb99ba29adfae91f37819602/trace_processor_shell.exe"
         },
         "linux_arm": {
             "hash": "99f971ca131f6d11c73f4b918099d434bdd8093c",
@@ -21,8 +21,8 @@
             "full_remote_path": "perfetto-luci-artifacts/v50.1/mac-arm64/trace_processor_shell"
         },
         "linux": {
-            "hash": "daab1ca0f1bbb0fb368fdd6fcbe8478f1791b151",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/3231268f2960b13cb52788389e88d7932117f57e/trace_processor_shell"
+            "hash": "eb1ebc49690ee1a5f6be8d0d2b726691af37dea0",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/d7984ebb216b56e0eb99ba29adfae91f37819602/trace_processor_shell"
         }
     },
     "power_profile.sql": {
diff --git a/ui/android/java/src/org/chromium/ui/widget/Toast.java b/ui/android/java/src/org/chromium/ui/widget/Toast.java
index 1f69689..bd7cd83 100644
--- a/ui/android/java/src/org/chromium/ui/widget/Toast.java
+++ b/ui/android/java/src/org/chromium/ui/widget/Toast.java
@@ -21,6 +21,7 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.StringRes;
 import androidx.annotation.StyleRes;
+import androidx.annotation.VisibleForTesting;
 
 import org.chromium.base.SysUtils;
 import org.chromium.build.annotations.NullMarked;
@@ -145,8 +146,9 @@
         mText = text;
     }
 
+    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
     @Nullable
-    CharSequence getText() {
+    public CharSequence getText() {
         return mText;
     }
 
diff --git a/ui/android/java/src/org/chromium/ui/widget/ToastManager.java b/ui/android/java/src/org/chromium/ui/widget/ToastManager.java
index 03fe9813..1c913358 100644
--- a/ui/android/java/src/org/chromium/ui/widget/ToastManager.java
+++ b/ui/android/java/src/org/chromium/ui/widget/ToastManager.java
@@ -14,6 +14,7 @@
 
 import org.jni_zero.JNINamespace;
 
+import org.chromium.base.ResettersForTesting;
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.build.annotations.Nullable;
 
@@ -61,6 +62,13 @@
         return sInstance;
     }
 
+    /** Override the toast manager for use in testing. */
+    public static void setInstanceForTesting(ToastManager manager) {
+        ToastManager previousManager = sInstance;
+        sInstance = manager;
+        ResettersForTesting.register(() -> sInstance = previousManager);
+    }
+
     private ToastManager() {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
             mToastEvent = new ToastEventPreR(this::toastHidden);
diff --git a/ui/webui/resources/cr_components/searchbox/searchbox_compose_button.css b/ui/webui/resources/cr_components/searchbox/searchbox_compose_button.css
index 74b80d65..5d43593 100644
--- a/ui/webui/resources/cr_components/searchbox/searchbox_compose_button.css
+++ b/ui/webui/resources/cr_components/searchbox/searchbox_compose_button.css
@@ -20,7 +20,7 @@
   font-size: inherit;
   border: none;
   position: relative;
-  gap: 0px;
+  gap: 2px;
 }
 
 .compose-container {